@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,139 @@
1
+ /**
2
+ * Permission presets (W9) - the built-in constants and the two files.
3
+ *
4
+ * Model (design approved by the architect):
5
+ * - built-in presets read-only / write-read / full-access are CODE constants;
6
+ * - custom presets live at <data dir>/permissions.json (atomic 0600);
7
+ * - a session's chosen preset lives at <session dir>/permission.json, a file
8
+ * PARALLEL to grants.json (whose v1 shape is deliberately untouched).
9
+ *
10
+ * Same discipline as grants.json: whitelist + validate every field, and a
11
+ * corrupt file means "fall back to the default", never "repair it". A permission
12
+ * is a BASELINE: engine-grants.ts intersects grants into it and a grant can
13
+ * never widen past it.
14
+ */
15
+ import { join } from "node:path";
16
+ import { readJsonIfExists, writeJsonAtomic } from "./fs-json.js";
17
+ export const PERMISSIONS_FILE = "permissions.json";
18
+ export const SESSION_PERMISSION_FILE = "permission.json";
19
+ export const ENV_PERMISSION_DEFAULT = "CELESTEA_PERMISSION_DEFAULT";
20
+ export const ENV_PERMISSION_MAX = "CELESTEA_PERMISSION_MAX";
21
+ export const DEFAULT_PERMISSION_ID = "full-access";
22
+ export const MAX_PRESET_ROOTS = 32;
23
+ export const MAX_PRESET_TOOLS = 64;
24
+ export const MAX_PRESET_LABEL_CHARS = 80;
25
+ export const BUILTIN_PRESET_IDS = ["read-only", "write-read", "full-access"];
26
+ export const BUILTIN_PRESETS = [
27
+ { id: "read-only", label: "Read only", network: false, workspaceWritable: false, toolRootsWritable: false, writeRoots: [], allPaths: false, unsandboxed: false, toolDeny: ["write_file"] },
28
+ { id: "write-read", label: "Write + read (workspace)", network: false, workspaceWritable: true, toolRootsWritable: false, writeRoots: [], allPaths: false, unsandboxed: false, toolDeny: [] },
29
+ { id: "full-access", label: "Full access", network: true, workspaceWritable: true, toolRootsWritable: true, writeRoots: [], allPaths: true, unsandboxed: true, toolDeny: [] },
30
+ ];
31
+ export function isBuiltinPresetId(id) {
32
+ return BUILTIN_PRESET_IDS.includes(id);
33
+ }
34
+ export function builtinPreset(id) {
35
+ for (const preset of BUILTIN_PRESETS)
36
+ if (preset.id === id)
37
+ return preset;
38
+ return null;
39
+ }
40
+ export function defaultPermissionId(env = process.env) {
41
+ const raw = (env[ENV_PERMISSION_DEFAULT] ?? "").trim();
42
+ return raw === "" ? DEFAULT_PERMISSION_ID : raw;
43
+ }
44
+ export function maxPermissionId(env = process.env) {
45
+ const raw = (env[ENV_PERMISSION_MAX] ?? "").trim();
46
+ return raw === "" ? DEFAULT_PERMISSION_ID : raw;
47
+ }
48
+ export function readPermissionsFile(dataDir) {
49
+ const out = readJsonIfExists(join(dataDir, PERMISSIONS_FILE));
50
+ if (!out.exists)
51
+ return { presets: [], warnings: [] };
52
+ if (out.error !== undefined)
53
+ return { presets: [], warnings: ["permissions_unreadable: " + out.error] };
54
+ const value = out.value;
55
+ if (typeof value !== "object" || value === null || Array.isArray(value))
56
+ return { presets: [], warnings: ["permissions.json is not an object"] };
57
+ const rec = value;
58
+ if (rec["version"] !== 1)
59
+ return { presets: [], warnings: ["unknown permissions.json version " + JSON.stringify(rec["version"])] };
60
+ if (!Array.isArray(rec["presets"]))
61
+ return { presets: [], warnings: ["permissions.json has no presets array"] };
62
+ const presets = [];
63
+ for (const raw of rec["presets"]) {
64
+ const parsed = parsePreset(raw);
65
+ if (parsed !== null && !isBuiltinPresetId(parsed.id))
66
+ presets.push(parsed);
67
+ }
68
+ return { presets, warnings: [] };
69
+ }
70
+ export function writePermissionsFile(dataDir, presets, now) {
71
+ writeJsonAtomic(join(dataDir, PERMISSIONS_FILE), { version: 1, updated_at: now, presets: presets.map(sanitizePreset) }, { mode: 0o600 });
72
+ }
73
+ function stringArray(raw, max) {
74
+ if (!Array.isArray(raw))
75
+ return [];
76
+ const out = [];
77
+ for (const value of raw) {
78
+ if (typeof value !== "string" || value.trim() === "")
79
+ continue;
80
+ out.push(value.trim());
81
+ if (out.length >= max)
82
+ break;
83
+ }
84
+ return [...new Set(out)];
85
+ }
86
+ export function parsePreset(raw) {
87
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
88
+ return null;
89
+ const rec = raw;
90
+ const id = rec["id"];
91
+ if (typeof id !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(id))
92
+ return null;
93
+ const label = typeof rec["label"] === "string" ? rec["label"].slice(0, MAX_PRESET_LABEL_CHARS) : id;
94
+ return {
95
+ id,
96
+ label,
97
+ network: rec["network"] === true,
98
+ workspaceWritable: rec["workspaceWritable"] === true,
99
+ toolRootsWritable: rec["toolRootsWritable"] === true,
100
+ writeRoots: stringArray(rec["writeRoots"], MAX_PRESET_ROOTS),
101
+ allPaths: rec["allPaths"] === true,
102
+ unsandboxed: rec["unsandboxed"] === true,
103
+ toolDeny: stringArray(rec["toolDeny"], MAX_PRESET_TOOLS),
104
+ };
105
+ }
106
+ function sanitizePreset(preset) {
107
+ return {
108
+ id: preset.id,
109
+ label: preset.label.slice(0, MAX_PRESET_LABEL_CHARS),
110
+ network: preset.network === true,
111
+ workspaceWritable: preset.workspaceWritable === true,
112
+ toolRootsWritable: preset.toolRootsWritable === true,
113
+ writeRoots: stringArray(preset.writeRoots, MAX_PRESET_ROOTS),
114
+ allPaths: preset.allPaths === true,
115
+ unsandboxed: preset.unsandboxed === true,
116
+ toolDeny: stringArray(preset.toolDeny, MAX_PRESET_TOOLS),
117
+ };
118
+ }
119
+ export function readSessionPermission(dir, expectedSession) {
120
+ const out = readJsonIfExists(join(dir, SESSION_PERMISSION_FILE));
121
+ if (!out.exists)
122
+ return { exists: false };
123
+ if (out.error !== undefined)
124
+ return { exists: true, error: "unparsable permission.json: " + out.error };
125
+ const value = out.value;
126
+ if (typeof value !== "object" || value === null || Array.isArray(value))
127
+ return { exists: true, error: "permission.json is not an object" };
128
+ const rec = value;
129
+ if (rec["version"] !== 1)
130
+ return { exists: true, error: "unknown permission.json version " + JSON.stringify(rec["version"]) };
131
+ if (rec["session"] !== expectedSession)
132
+ return { exists: true, error: "permission.json belongs to " + JSON.stringify(rec["session"]) };
133
+ if (typeof rec["preset"] !== "string" || rec["preset"] === "")
134
+ return { exists: true, error: "permission.json has no preset" };
135
+ return { exists: true, preset: rec["preset"] };
136
+ }
137
+ export function writeSessionPermission(dir, session, preset, now) {
138
+ writeJsonAtomic(join(dir, SESSION_PERMISSION_FILE), { version: 1, session, preset, updated_at: now }, { mode: 0o600 });
139
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `build_gen` — assembling the system prompt from the section registry
3
+ * (`src/prompts.rs:108-130,472-500`).
4
+ *
5
+ * The composed prompt is a DERIVED value: sections are rendered in `(order, id)`
6
+ * order, empty/whitespace templates are dropped, the survivors are joined with
7
+ * a blank line, and the result is truncated to `PROMPT_MAX_LEN` bytes on a char
8
+ * boundary. A `USER_OVERRIDE` (POST /api/config `system_prompt`) bypasses this
9
+ * assembly entirely — it is never a section.
10
+ */
11
+ import { type SessionMode } from "./mode.js";
12
+ import type { PromptScope, PromptsStore } from "./prompts.js";
13
+ import { type PromptVars } from "./prompts-template.js";
14
+ export interface PromptVarInput {
15
+ model: string;
16
+ provider: string;
17
+ base_url: string;
18
+ /** W768: the workspace NAME (`CelesteaTeamAPI`). */
19
+ workspace: string;
20
+ /** W768: the workspace ROOT PATH — the same value the tools run in. */
21
+ workspace_dir: string;
22
+ session: string;
23
+ tools: string;
24
+ context_window: number;
25
+ max_output_tokens: number | null;
26
+ date: string;
27
+ /** W782: `/src/...` repo root of the RUNNING process (derived, never typed). */
28
+ studio_repo: string;
29
+ /** W782: `{{studio_repo}}/apps/web` — the frontend sources. */
30
+ studio_frontend_dir: string;
31
+ /** W782: `config.paths.staticRoot` — where the built frontend is served. */
32
+ studio_static_root: string;
33
+ /** W782: the systemd unit name (derived or centrally configured). */
34
+ studio_service: string;
35
+ /** W782: `config.bind` — host:port the backend listens on. */
36
+ studio_bind: string;
37
+ /** W782: the public site (the one irreducible literal, see deployment.ts). */
38
+ studio_site: string;
39
+ }
40
+ /** Interpolation values; every whitelisted variable gets a string. */
41
+ export declare function toPromptVars(input: PromptVarInput): PromptVars;
42
+ /**
43
+ * Assemble the registry-resolved system prompt for one scope.
44
+ *
45
+ * W729: `mode` selects the `tool_access` VARIANT (K6 — the registry stays 10
46
+ * rows); everything else is mode-independent, and a scope/bound override of
47
+ * `tool_access` still replaces the variant (R4). The default keeps every
48
+ * existing caller on the `standard` text.
49
+ */
50
+ export declare function assembleSystemPrompt(store: PromptsStore, scope: PromptScope, boundId: string | null, vars: PromptVars, mode?: SessionMode): string;
51
+ /**
52
+ * `active_prompt` resolution chain: the active session's bound prompt id wins;
53
+ * a bound id that no longer exists resolves to null (there is NO fallback for a
54
+ * bound-but-missing prompt); otherwise scope default -> global default.
55
+ */
56
+ export declare function resolveActivePrompt(store: PromptsStore, scope: PromptScope, sessionPromptId: string | null): string | null;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `build_gen` — assembling the system prompt from the section registry
3
+ * (`src/prompts.rs:108-130,472-500`).
4
+ *
5
+ * The composed prompt is a DERIVED value: sections are rendered in `(order, id)`
6
+ * order, empty/whitespace templates are dropped, the survivors are joined with
7
+ * a blank line, and the result is truncated to `PROMPT_MAX_LEN` bytes on a char
8
+ * boundary. A `USER_OVERRIDE` (POST /api/config `system_prompt`) bypasses this
9
+ * assembly entirely — it is never a section.
10
+ */
11
+ import { DEFAULT_SESSION_MODE } from "./mode.js";
12
+ import { renderTemplate, truncateToCap } from "./prompts-template.js";
13
+ /** Interpolation values; every whitelisted variable gets a string. */
14
+ export function toPromptVars(input) {
15
+ return {
16
+ model: input.model,
17
+ provider: input.provider,
18
+ base_url: input.base_url,
19
+ workspace: input.workspace,
20
+ workspace_dir: input.workspace_dir,
21
+ session: input.session,
22
+ tools: input.tools,
23
+ context_window: String(input.context_window),
24
+ max_output_tokens: input.max_output_tokens === null ? "" : String(input.max_output_tokens),
25
+ date: input.date,
26
+ studio_repo: input.studio_repo,
27
+ studio_frontend_dir: input.studio_frontend_dir,
28
+ studio_static_root: input.studio_static_root,
29
+ studio_service: input.studio_service,
30
+ studio_bind: input.studio_bind,
31
+ studio_site: input.studio_site,
32
+ };
33
+ }
34
+ /**
35
+ * Assemble the registry-resolved system prompt for one scope.
36
+ *
37
+ * W729: `mode` selects the `tool_access` VARIANT (K6 — the registry stays 10
38
+ * rows); everything else is mode-independent, and a scope/bound override of
39
+ * `tool_access` still replaces the variant (R4). The default keeps every
40
+ * existing caller on the `standard` text.
41
+ */
42
+ export function assembleSystemPrompt(store, scope, boundId, vars, mode = DEFAULT_SESSION_MODE) {
43
+ const parts = [];
44
+ for (const row of store.sections(scope, boundId, mode)) {
45
+ const rendered = renderTemplate(row.template, vars).trim();
46
+ if (rendered === "")
47
+ continue;
48
+ parts.push(rendered);
49
+ }
50
+ return truncateToCap(parts.join("\n\n"));
51
+ }
52
+ /**
53
+ * `active_prompt` resolution chain: the active session's bound prompt id wins;
54
+ * a bound id that no longer exists resolves to null (there is NO fallback for a
55
+ * bound-but-missing prompt); otherwise scope default -> global default.
56
+ */
57
+ export function resolveActivePrompt(store, scope, sessionPromptId) {
58
+ if (sessionPromptId !== null && sessionPromptId !== "") {
59
+ return store.find(scope, sessionPromptId) === undefined ? null : sessionPromptId;
60
+ }
61
+ return store.defaultFor(scope)?.id ?? null;
62
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Prompt template rules (`src/prompts.rs:126-165,472-560`).
3
+ *
4
+ * `{{name}}` is the ONLY syntax: the name is trimmed, there are no aliases and
5
+ * no escaping. Validation and rendering share one scanner so a template can
6
+ * never validate under one rule and render under another.
7
+ */
8
+ /** `PROMPT_MAX_LEN`. */
9
+ export declare const PROMPT_MAX_LEN = 8192;
10
+ /** `ORDER_FALLBACK`: a section that is new to a scope lands after the builtins. */
11
+ export declare const ORDER_FALLBACK = 2000;
12
+ /** The whitelist; anything else is `undefined prompt variable '{{name}}'`. */
13
+ export declare const PROMPT_VARS: readonly ["model", "provider", "base_url", "workspace", "workspace_dir", "session", "tools", "context_window", "max_output_tokens", "date", "studio_repo", "studio_frontend_dir", "studio_static_root", "studio_service", "studio_bind", "studio_site"];
14
+ export type PromptVar = (typeof PROMPT_VARS)[number];
15
+ export type PromptVars = Partial<Record<PromptVar, string>>;
16
+ export interface TemplateError {
17
+ error: string;
18
+ }
19
+ /** Byte length of the UTF-8 encoding (the cap is in BYTES, not chars). */
20
+ export declare function byteLength(text: string): number;
21
+ /** Validate one template against the cap, the whitelist and closure. */
22
+ export declare function validateTemplate(template: string): TemplateError | null;
23
+ /** Render `{{var}}` with the given values; a missing value renders empty. */
24
+ export declare function renderTemplate(template: string, vars: PromptVars): string;
25
+ /** Truncate to the byte cap on a char boundary (`truncate_prompt`). */
26
+ export declare function truncateToCap(text: string): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Prompt template rules (`src/prompts.rs:126-165,472-560`).
3
+ *
4
+ * `{{name}}` is the ONLY syntax: the name is trimmed, there are no aliases and
5
+ * no escaping. Validation and rendering share one scanner so a template can
6
+ * never validate under one rule and render under another.
7
+ */
8
+ /** `PROMPT_MAX_LEN`. */
9
+ export const PROMPT_MAX_LEN = 8192;
10
+ /** `ORDER_FALLBACK`: a section that is new to a scope lands after the builtins. */
11
+ export const ORDER_FALLBACK = 2000;
12
+ /** The whitelist; anything else is `undefined prompt variable '{{name}}'`. */
13
+ export const PROMPT_VARS = [
14
+ "model",
15
+ "provider",
16
+ "base_url",
17
+ "workspace",
18
+ "workspace_dir",
19
+ "session",
20
+ "tools",
21
+ "context_window",
22
+ "max_output_tokens",
23
+ "date",
24
+ // W782: the deployment facts the `environment` section states. Every one is
25
+ // resolved at runtime (see `src/deployment.ts`) so no template has to spell
26
+ // out a path, a port or a unit name that the next move/rename would falsify.
27
+ "studio_repo",
28
+ "studio_frontend_dir",
29
+ "studio_static_root",
30
+ "studio_service",
31
+ "studio_bind",
32
+ "studio_site",
33
+ ];
34
+ /** Split a template into literals and `{{var}}` slots (never throws). */
35
+ function scan(template) {
36
+ const tokens = [];
37
+ let rest = template;
38
+ for (;;) {
39
+ const open = rest.indexOf("{{");
40
+ if (open < 0) {
41
+ tokens.push({ literal: rest });
42
+ return { tokens, unclosed: false };
43
+ }
44
+ const close = rest.indexOf("}}", open + 2);
45
+ if (close < 0) {
46
+ tokens.push({ literal: rest.slice(0, open) });
47
+ return { tokens, unclosed: true };
48
+ }
49
+ tokens.push({ literal: rest.slice(0, open) });
50
+ tokens.push({ literal: "", name: rest.slice(open + 2, close).trim() });
51
+ rest = rest.slice(close + 2);
52
+ }
53
+ }
54
+ /** Byte length of the UTF-8 encoding (the cap is in BYTES, not chars). */
55
+ export function byteLength(text) {
56
+ return Buffer.byteLength(text, "utf8");
57
+ }
58
+ /** Validate one template against the cap, the whitelist and closure. */
59
+ export function validateTemplate(template) {
60
+ const bytes = byteLength(template);
61
+ if (bytes > PROMPT_MAX_LEN)
62
+ return { error: `template exceeds the ${PROMPT_MAX_LEN} byte cap (${bytes} bytes)` };
63
+ const { tokens, unclosed } = scan(template);
64
+ if (unclosed)
65
+ return { error: "unclosed '{{' in template" };
66
+ for (const t of tokens) {
67
+ if (t.name === undefined)
68
+ continue;
69
+ if (!PROMPT_VARS.includes(t.name)) {
70
+ return { error: `undefined prompt variable '{{${t.name}}}'` };
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /** Render `{{var}}` with the given values; a missing value renders empty. */
76
+ export function renderTemplate(template, vars) {
77
+ const { tokens } = scan(template);
78
+ let out = "";
79
+ for (const t of tokens) {
80
+ if (t.name === undefined) {
81
+ out += t.literal;
82
+ continue;
83
+ }
84
+ out += vars[t.name] ?? "";
85
+ }
86
+ return out;
87
+ }
88
+ /** Truncate to the byte cap on a char boundary (`truncate_prompt`). */
89
+ export function truncateToCap(text) {
90
+ if (byteLength(text) <= PROMPT_MAX_LEN)
91
+ return text;
92
+ let acc = "";
93
+ for (const ch of text) {
94
+ if (byteLength(acc + ch) > PROMPT_MAX_LEN)
95
+ break;
96
+ acc += ch;
97
+ }
98
+ return acc;
99
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Prompt registry store — global `prompts.json` + per-workspace
3
+ * `<CELESTEA_HOME>/workspaces/<ws>/prompts.json` (W880; legacy
4
+ * `<ws>/.celestea-prompts.json` stays readable) (`contracts/data-files/prompts.schema.json`,
5
+ * `src/prompts.rs:560-880`).
6
+ *
7
+ * Tolerated-on-read, strict-on-write: a missing OR malformed registry file
8
+ * degrades to an empty one (the registry must never break compose or the UI),
9
+ * while every write validates the id and every override template before the
10
+ * file is touched. There are four registry levels — builtin → global →
11
+ * workspace → the bound prompt's `section_overrides` — and writing is
12
+ * `persist -> hot apply -> roll the file back when the apply fails`.
13
+ */
14
+ import { type StoreResult } from "./result.js";
15
+ import { type SessionMode } from "./mode.js";
16
+ export interface PromptSectionRow {
17
+ id: string;
18
+ name: string;
19
+ template: string;
20
+ order: number;
21
+ source: "builtin" | "global" | "workspace";
22
+ }
23
+ export interface PromptEntry {
24
+ id: string;
25
+ name: string;
26
+ section_overrides: Record<string, string>;
27
+ is_default: boolean;
28
+ }
29
+ export interface PromptListEntry extends PromptEntry {
30
+ scope: "global" | "workspace";
31
+ shadowed: boolean;
32
+ }
33
+ export interface PromptFileData {
34
+ sections: Array<{
35
+ id: string;
36
+ name: string;
37
+ template: string;
38
+ order: number;
39
+ }>;
40
+ prompts: PromptEntry[];
41
+ default_prompt: string | null;
42
+ }
43
+ export interface PromptScope {
44
+ kind: "global" | "workspace";
45
+ workspace: string | null;
46
+ /** Canonical write target (reads fall back through `files`). */
47
+ file: string;
48
+ /**
49
+ * Every file this scope READS, canonical FIRST (W880). `file` === `files[0]`
50
+ * is the single write target; the rest are legacy locations kept readable so a
51
+ * pre-W880 workspace registry is never lost. Writes always land in `file`,
52
+ * which keeps `snapshot`/`restore` correct: they roll the write target back,
53
+ * leaving a legacy source untouched.
54
+ */
55
+ files: readonly string[];
56
+ }
57
+ export interface PromptDefaultRef {
58
+ id: string;
59
+ scope: "global" | "workspace";
60
+ }
61
+ export interface PromptListResponse {
62
+ ok: true;
63
+ scope: "global" | "workspace";
64
+ workspace: string | null;
65
+ global_file: string;
66
+ sections: PromptSectionRow[];
67
+ prompts: PromptListEntry[];
68
+ default_prompt: PromptDefaultRef | null;
69
+ active_prompt: string | null;
70
+ }
71
+ export interface PromptUpsertRequest {
72
+ id: string;
73
+ name: string;
74
+ section_overrides?: Record<string, string>;
75
+ is_default?: boolean;
76
+ }
77
+ export declare class PromptsStore {
78
+ private readonly globalFile;
79
+ constructor(globalFile: string);
80
+ scopeGlobal(): PromptScope;
81
+ scopeWorkspace(name: string, wsPath: string): PromptScope;
82
+ /**
83
+ * Tolerant read: missing OR malformed both degrade to an empty registry. The
84
+ * first EXISTING candidate wins (canonical, then the legacy locations).
85
+ */
86
+ read(scope: PromptScope): PromptFileData;
87
+ /** Raw file text (the rollback snapshot for a failed hot apply). */
88
+ snapshot(scope: PromptScope): string | null;
89
+ /**
90
+ * Roll a failed hot apply back to its snapshot.
91
+ *
92
+ * W815-1: a null snapshot means the scope file did NOT exist before the
93
+ * mutate, so "back to the snapshot" is "remove the file" — returning early
94
+ * left the fresh content on disk while the engine had rejected it (registry
95
+ * ahead of the engine). A present snapshot is restored verbatim.
96
+ */
97
+ restore(scope: PromptScope, text: string | null): void;
98
+ private persist;
99
+ find(scope: PromptScope, id: string): PromptEntry | undefined;
100
+ /** `default_prompt` of this scope, falling back to the global one. */
101
+ defaultFor(scope: PromptScope): PromptDefaultRef | null;
102
+ private promptList;
103
+ /** GET /api/prompts body (handler fills `active_prompt`). */
104
+ list(scope: PromptScope, activePrompt: string | null): PromptListResponse;
105
+ private effective;
106
+ /**
107
+ * The registry-resolved sections (used by the sections listing and by
108
+ * `build_gen`). `mode` swaps the `tool_access` builtin VARIANT (W729/K6); it
109
+ * never adds or removes a row.
110
+ */
111
+ sections(scope: PromptScope, boundId?: string | null, mode?: SessionMode): PromptSectionRow[];
112
+ upsert(scope: PromptScope, req: PromptUpsertRequest): StoreResult<{
113
+ id: string;
114
+ scope: "global" | "workspace";
115
+ hot_applied: true;
116
+ }>;
117
+ remove(scope: PromptScope, rawId: string): StoreResult<{
118
+ scope: "global" | "workspace";
119
+ hot_applied: true;
120
+ }>;
121
+ setDefault(scope: PromptScope, rawId: string): StoreResult<{
122
+ default_prompt: string;
123
+ scope: "global" | "workspace";
124
+ hot_applied: true;
125
+ }>;
126
+ }
127
+ export interface SectionComposeInput {
128
+ global: PromptFileData;
129
+ workspace: PromptFileData | null;
130
+ bound: PromptEntry | null;
131
+ /** Builtin `tool_access` variant to start the overlay from (default standard). */
132
+ mode?: SessionMode;
133
+ }
134
+ /**
135
+ * Four-level overlay: builtin -> global -> workspace -> bound overrides.
136
+ * A known id keeps its ORIGINAL order and only swaps its template; an unknown
137
+ * id is appended with ORDER_FALLBACK and sorted by `(order, id)`.
138
+ */
139
+ export declare function composeSections(input: SectionComposeInput): {
140
+ rows: PromptSectionRow[];
141
+ templates: Map<string, string>;
142
+ };