@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,92 @@
1
+ /**
2
+ * effectivePermissionOf (W9) - the ONE reader of a session's permission
3
+ * BASELINE. engine-grants.ts intersects grants into it.
4
+ *
5
+ * Resolution of the requested preset:
6
+ * 1. the caller hint (worker sessions have no directory and carry it in meta);
7
+ * 2. <session dir>/permission.json;
8
+ * 3. CELESTEA_PERMISSION_DEFAULT (default "full-access").
9
+ * The baseline capabilities are then CLAMPED by CELESTEA_PERMISSION_MAX
10
+ * (default "full-access"). The operator env gate for "unsandboxed" is applied
11
+ * by engine-grants.ts when it builds the final EffectiveGrants, so this module
12
+ * never imports engine-grants (no cycle).
13
+ */
14
+ import { dirname } from "node:path";
15
+ import { parseToolRoots } from "@celestea/tools";
16
+ import { loadStudioConfig } from "../config.js";
17
+ import { builtinPreset, defaultPermissionId, maxPermissionId, readPermissionsFile, readSessionPermission, } from "../store/permissions.js";
18
+ /** The data dir of the studio (same one workspaces.json / auth secret use). */
19
+ export function permissionDataDir(env) {
20
+ return dirname(loadStudioConfig({ env }).paths.workspacesFile);
21
+ }
22
+ function presetById(id, custom) {
23
+ const builtin = builtinPreset(id);
24
+ if (builtin !== null)
25
+ return builtin;
26
+ for (const preset of custom)
27
+ if (preset.id === id)
28
+ return preset;
29
+ return null;
30
+ }
31
+ /** Capability-wise clamp by the operator MAX preset (a grant/档 can only narrow). */
32
+ function clampByMax(preset, max) {
33
+ const writesAllowed = max.allPaths || max.workspaceWritable || max.toolRootsWritable || max.writeRoots.length > 0;
34
+ return {
35
+ id: preset.id,
36
+ label: preset.label,
37
+ network: preset.network && max.network,
38
+ workspaceWritable: preset.workspaceWritable && max.workspaceWritable,
39
+ toolRootsWritable: preset.toolRootsWritable && max.toolRootsWritable,
40
+ writeRoots: writesAllowed ? preset.writeRoots.filter((root) => max.writeRoots.includes(root)) : [],
41
+ allPaths: preset.allPaths && max.allPaths,
42
+ unsandboxed: preset.unsandboxed && max.unsandboxed,
43
+ toolDeny: [...new Set([...preset.toolDeny, ...max.toolDeny])],
44
+ };
45
+ }
46
+ export function effectivePermissionOf(sessionDir, sessionId, env, presetHint) {
47
+ const warnings = [];
48
+ const dataDir = permissionDataDir(env);
49
+ const customRead = readPermissionsFile(dataDir);
50
+ warnings.push(...customRead.warnings);
51
+ const custom = customRead.presets;
52
+ let requested = presetHint ?? null;
53
+ // W878: the sidecar is self-describing — use the TRUSTED id, never a path
54
+ // inference. A null id with a real directory degrades fail-closed.
55
+ if (requested === null && sessionDir !== null && sessionId !== null) {
56
+ const file = readSessionPermission(sessionDir, sessionId);
57
+ if (file.error !== undefined)
58
+ warnings.push("permission_unreadable: " + file.error);
59
+ if (file.preset !== undefined)
60
+ requested = file.preset;
61
+ }
62
+ if (requested === null || requested === "")
63
+ requested = defaultPermissionId(env);
64
+ const fallback = presetById(defaultPermissionId(env), custom) ?? builtinPreset("full-access");
65
+ const max = presetById(maxPermissionId(env), custom) ?? builtinPreset("full-access");
66
+ let preset = presetById(requested, custom);
67
+ if (preset === null) {
68
+ warnings.push("unknown permission preset " + JSON.stringify(requested) + " - using " + fallback.id);
69
+ preset = fallback;
70
+ }
71
+ const clamped = clampByMax(preset, max);
72
+ const writeRoots = [];
73
+ if (clamped.toolRootsWritable) {
74
+ for (const root of parseToolRoots(env["CELESTEA_TOOL_ROOTS"]))
75
+ if (!writeRoots.includes(root))
76
+ writeRoots.push(root);
77
+ }
78
+ for (const root of clamped.writeRoots)
79
+ if (!writeRoots.includes(root))
80
+ writeRoots.push(root);
81
+ return {
82
+ preset: clamped.id,
83
+ network: clamped.network,
84
+ workspaceWritable: clamped.workspaceWritable,
85
+ toolRootsWritable: clamped.toolRootsWritable,
86
+ writeRoots,
87
+ allPaths: clamped.allPaths,
88
+ unsandboxed: clamped.unsandboxed,
89
+ toolDeny: clamped.toolDeny,
90
+ warnings,
91
+ };
92
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The engine's compose plugins: the `Llm` seam, the tool registry (builtins +
3
+ * the three worker-orchestration tools) and the agent loop.
4
+ *
5
+ * This module exists so `real-runtime-adapter.ts` stays about the HTTP contract:
6
+ * every `Context` service the runtime resolves at turn start is provided here,
7
+ * in one place, with the tool set assembled explicitly (a later `provide` wins,
8
+ * so the host can override any of them by mounting its own plugin).
9
+ *
10
+ * W741 (fixes the W738 §4 finding): the sandbox is **always** chosen by the
11
+ * provider policy — `selectSandboxDetailed`, i.e. bwrap whenever the host can
12
+ * give it, with or without session grants — and the resulting decision travels
13
+ * with every run (`SandboxDecision`). `CELESTEA_SANDBOX_FALLBACK=fail` is read on
14
+ * the default path too and it REFUSES to execute (structured `SandboxError`)
15
+ * instead of degrading to the userspace provider behind the operator's back.
16
+ */
17
+ import { type AskUserQuestionAnswerItem, type EventBus, type Llm, type Plugin, type Sandbox, type SandboxMeta, type Tool, type ToolGuard, type ToolRegistry } from "@celestea/core";
18
+ import { PendingQuestion, type QuestionRegistry } from "../question-registry.js";
19
+ import { type Profile } from "@celestea/runtime";
20
+ import { DisclosurePolicy, ProcessRegistry, type AttachmentStore, type SessionFsScope, type HostProbe, type SandboxFallbackMode } from "@celestea/tools";
21
+ import { type WorkerRegistry } from "@celestea/workers";
22
+ import { type EffectiveGrants, type EngineGrantAudit } from "./engine-grants.js";
23
+ import { type SessionMode } from "../store/mode.js";
24
+ /** Everything the engine context needs from the host. */
25
+ export interface EnginePluginInput {
26
+ profile: Profile;
27
+ llm: Llm;
28
+ /**
29
+ * W804: the session's content-addressed attachment store. Present = the tool
30
+ * face also offers `read_image` (and the wire can resolve bytes); absent = no
31
+ * attachment capability at all.
32
+ */
33
+ attachments?: AttachmentStore | null;
34
+ /**
35
+ * W804: false ONLY when the target model's input_modalities was EXPLICITLY
36
+ * configured without "image". Absent/true = the optimistic default.
37
+ */
38
+ imageInputAllowed?: boolean;
39
+ /**
40
+ * W791 (P1, §5.2 #2): the session's working mode. `execution` folds the SDK
41
+ * tools out of the DIRECT face (the inner registry keeps them, so `run_code`
42
+ * sub-calls still run); absent/`standard` = the whole registry, byte-for-byte
43
+ * what every pre-P1 generation exposed.
44
+ */
45
+ mode?: SessionMode;
46
+ /**
47
+ * W806 (P0): dynamic tool disclosure. Absent = the static mode baseline, i.e.
48
+ * today's byte-identical face. Present = `initial` is offered from the start
49
+ * and the rest of the disclosable universe is revealed ONE TURN AT A TIME,
50
+ * appended at the tail, when a direct call is refused (monotonic; never
51
+ * reordered — see `disclosure.ts`).
52
+ */
53
+ disclosure?: DisclosureOptions;
54
+ /** Worker registry to expose the three orchestration tools over (null = none). */
55
+ workers: WorkerRegistry | null;
56
+ /** Extra tools appended after the builtins. */
57
+ tools?: readonly Tool[];
58
+ /**
59
+ * Sandbox override (tests inject a fake). This is the ONE explicit policy
60
+ * bypass: it is reported as `fallback_source: "injected"` in every result.
61
+ */
62
+ sandbox?: Sandbox;
63
+ /** Guard override: `undefined` = mount the production guard, `null` = none. */
64
+ guard?: ToolGuard | null;
65
+ env?: NodeJS.ProcessEnv;
66
+ /** The session's effective grants (W516); default: none (least privilege). */
67
+ grants?: EffectiveGrants;
68
+ /** Bound audit sink for grant use / degradation events (W516 §4.4). */
69
+ audit?: EngineGrantAudit;
70
+ /** Injected host probe (tests / diagnostics); default: the memoized host probe. */
71
+ probe?: HostProbe;
72
+ /**
73
+ * W768: the composing SESSION's workspace (see `SessionFsScope`). The host
74
+ * resolves it from the session's own record — the very same value its system
75
+ * prompt renders — so the sandbox cwd/root and the guard's writable workspace
76
+ * follow the session, not the process. `null`/absent = the env posture.
77
+ */
78
+ workspace?: SessionFsScope | null;
79
+ /**
80
+ * W783: the pending-question table of this process. Supplied = the session
81
+ * mounts `ask_user_question` and answers through the waterfall; absent = the
82
+ * feature is not mounted at all (the tool is then not offered to the model).
83
+ */
84
+ questions?: QuestionWiring | null;
85
+ }
86
+ /**
87
+ * W783: a one-slot holder for the session's event bus. `engineTools()` runs
88
+ * before `compose()` provides the bus and the plugin body runs after, so the
89
+ * value travels through this object instead of a compose-time argument.
90
+ */
91
+ export interface BusHolder {
92
+ current: EventBus | null;
93
+ }
94
+ /**
95
+ * W806: activation of the dynamic-disclosure layer. The mechanism is always
96
+ * assembled; this is what asks it to withhold anything.
97
+ */
98
+ export interface DisclosureOptions {
99
+ /**
100
+ * Names offered from the start. Default = the whole disclosable universe for
101
+ * the session's mode (the mode baseline = today's face, a no-op). Names
102
+ * outside the registry, or blocked by the mode, are dropped.
103
+ */
104
+ initial?: readonly string[];
105
+ }
106
+ /** W783: everything the engine needs to mount the user-question capability. */
107
+ export interface QuestionWiring {
108
+ /** Process-wide pending table (shared by every session generation). */
109
+ registry: QuestionRegistry;
110
+ /**
111
+ * The session's async answerer chain (the runtime's own event bus). The plugin
112
+ * body fills this in, so the wiring can be built BEFORE `compose()` runs while
113
+ * the bus only exists DURING it.
114
+ */
115
+ bus: BusHolder;
116
+ /** The session id this generation is composed for (`null` = detached). */
117
+ sessionId: string | null;
118
+ /** Is this generation still live? (§5.3 `CALLER_NOT_LIVE` when it is not.) */
119
+ isLive?: () => boolean;
120
+ /** Publish one request to the UI (`question` SSE frame). */
121
+ publish: (question: PendingQuestion) => void;
122
+ /** Record the request in the session log (`user_question` row, §7). */
123
+ record: (question: PendingQuestion) => void;
124
+ /** Record how it ended (`user_answer` row, §7) — answer and timeout alike. */
125
+ recordAnswer: (requestId: string, answers: AskUserQuestionAnswerItem[], timedOut: boolean) => void;
126
+ }
127
+ export interface EngineTools {
128
+ /** Plugin providing TOOL_REGISTRY_SERVICE / SANDBOX_SERVICE / PROCESS_REGISTRY_SERVICE. */
129
+ plugin: Plugin;
130
+ /**
131
+ * The registry the session's Context provides (and therefore the face `GET
132
+ * /api/tools?session=` and `{{tools}}` read). W791: in `execution` mode this is
133
+ * the EXPOSED view — the inner registry stays reachable to `run_code` only.
134
+ */
135
+ registry: ToolRegistry;
136
+ /** The sandbox actually mounted (W741: annotated with the policy decision). */
137
+ sandbox: Sandbox;
138
+ /**
139
+ * W855: the session's background-process registry. The HOST keeps this handle
140
+ * so `Runtime.shutdown` can reap detached children via a shutdown hook —
141
+ * `ProcessRegistry.dispose()` had no caller before this.
142
+ */
143
+ processes: ProcessRegistry;
144
+ /** Why that sandbox was chosen — auditable, never inferred by a caller. */
145
+ decision: SandboxDecision;
146
+ /**
147
+ * W806: the dynamic-disclosure policy of this generation. Always present; inert
148
+ * (nothing withheld) unless `EnginePluginInput.disclosure` asked for a reduced
149
+ * initial set. The runtime calls `beginTurn()` at every turn start.
150
+ */
151
+ disclosure: DisclosurePolicy;
152
+ }
153
+ /** The tool set: six builtins + the three worker tools (when a registry exists). */
154
+ export declare function engineTools(opts: EnginePluginInput): EngineTools;
155
+ /** Where one session's sandbox came from (W741 §3 — never inferred by a caller). */
156
+ export type SandboxDecisionSource = "policy" | "grant" | "refused" | "injected";
157
+ /**
158
+ * The provider-policy decision behind one composed session.
159
+ *
160
+ * `degraded` keeps one invariant: `true` ⟺ commands really run on the userspace
161
+ * provider (no namespaces, no seccomp). A refusal is therefore NOT "degraded" —
162
+ * it is `source: "refused"` plus a structured `SandboxError` on every execution
163
+ * attempt — and an injected sandbox reports `null` (the policy never decided it).
164
+ */
165
+ export interface SandboxDecision {
166
+ /** Provider that will execute (`"none"` when every run is refused). */
167
+ provider: string;
168
+ degraded: boolean | null;
169
+ reason: string | null;
170
+ /** `CELESTEA_SANDBOX_FALLBACK` in force at compose time (`null` = unreadable). */
171
+ mode: SandboxFallbackMode | null;
172
+ source: SandboxDecisionSource;
173
+ }
174
+ /** `SandboxMeta` plus the fallback decision the 4-field contract cannot carry. */
175
+ export interface DecidedSandboxMeta extends SandboxMeta {
176
+ degraded: boolean | null;
177
+ fallback_reason: string | null;
178
+ fallback_mode: SandboxFallbackMode | null;
179
+ fallback_source: SandboxDecisionSource;
180
+ }
181
+ /** Provide the `Llm` seam (the offline engine by default). */
182
+ export declare function engineLlmPlugin(llm: Llm, name?: string): Plugin;
183
+ /** Provide the agent loop driver (needed for worker driving). */
184
+ export declare function engineLoopPlugin(profile: Profile, name?: string): Plugin;
185
+ /** Convenience: the three plugins in mount order (llm, loop, tools). */
186
+ export declare function enginePlugins(input: EnginePluginInput): {
187
+ plugins: Plugin[];
188
+ tools: EngineTools;
189
+ };
@@ -0,0 +1,292 @@
1
+ /**
2
+ * The engine's compose plugins: the `Llm` seam, the tool registry (builtins +
3
+ * the three worker-orchestration tools) and the agent loop.
4
+ *
5
+ * This module exists so `real-runtime-adapter.ts` stays about the HTTP contract:
6
+ * every `Context` service the runtime resolves at turn start is provided here,
7
+ * in one place, with the tool set assembled explicitly (a later `provide` wins,
8
+ * so the host can override any of them by mounting its own plugin).
9
+ *
10
+ * W741 (fixes the W738 §4 finding): the sandbox is **always** chosen by the
11
+ * provider policy — `selectSandboxDetailed`, i.e. bwrap whenever the host can
12
+ * give it, with or without session grants — and the resulting decision travels
13
+ * with every run (`SandboxDecision`). `CELESTEA_SANDBOX_FALLBACK=fail` is read on
14
+ * the default path too and it REFUSES to execute (structured `SandboxError`)
15
+ * instead of degrading to the userspace provider behind the operator's back.
16
+ */
17
+ import { definePlugin, LLM_SERVICE, SANDBOX_SERVICE, TOOL_REGISTRY_SERVICE, USER_QUESTION_SERVICE, EVENT_BUS_SERVICE, SandboxError, } from "@celestea/core";
18
+ import { agentLoopPlugin } from "@celestea/agent-loop";
19
+ import { createUserQuestionService } from "../user-questions.js";
20
+ import { PendingQuestion } from "../question-registry.js";
21
+ import { agentConfigFromProfile } from "@celestea/runtime";
22
+ import { assembleTools, builtinTools, DisclosurePolicy, disclosureExposure, ENV_SANDBOX_FALLBACK, EXECUTION_TOOL_NAMES, exposedRegistry, fallbackMode, httpOptions, PROCESS_REGISTRY_SERVICE, ProcessRegistry, sandboxConfigFromEnv, sessionSandboxConfig, selectSandboxDetailed, } from "@celestea/tools";
23
+ import { workerTools } from "@celestea/workers";
24
+ import { EMPTY_GRANTS } from "./engine-grants.js";
25
+ import { DEFAULT_SESSION_MODE } from "../store/mode.js";
26
+ /** The tool set: six builtins + the three worker tools (when a registry exists). */
27
+ export function engineTools(opts) {
28
+ const env = opts.env ?? process.env;
29
+ const grants = opts.grants ?? EMPTY_GRANTS;
30
+ const scope = opts.workspace ?? null;
31
+ const processes = new ProcessRegistry();
32
+ const choice = opts.sandbox === undefined ? chooseSandbox(env, grants, opts.audit, opts.probe, scope) : injectedChoice(opts.sandbox, env);
33
+ const sandbox = choice.sandbox;
34
+ const http = httpOptions(env, { netHosts: grants.netHosts });
35
+ if (http.policy?.netHostsIneffective) {
36
+ opts.audit?.({ event: "net_hosts_ineffective", cap: "net_hosts", reason: "neither CELESTEA_HTTP_ALLOW nor CELESTEA_HTTP_DENY is set: the policy stays inactive" });
37
+ }
38
+ // W783 §5.3: the service refuses a DELEGATED caller itself (a worker turn has
39
+ // no human), so the guard is mounted unconditionally here.
40
+ // W783: the bus holder is filled in by the plugin body below, which runs
41
+ // inside `compose()` AFTER the runtime provided EVENT_BUS_SERVICE.
42
+ const busHolder = { current: null };
43
+ const questions = opts.questions === undefined || opts.questions === null ? null : userQuestionsOf(opts.questions, busHolder);
44
+ const tools = [
45
+ ...builtinTools({
46
+ sandbox,
47
+ processes,
48
+ http,
49
+ // W884: `load_skill` resolves its two source layers from the session's own
50
+ // workspace — the value the composer resolved via `sessionWorkspaceOf`.
51
+ workspace: scope?.workspace ?? null,
52
+ env,
53
+ ...(questions === null ? {} : { questions }),
54
+ ...(opts.attachments === undefined ? {} : { attachments: opts.attachments }),
55
+ ...(opts.imageInputAllowed === undefined ? {} : { imageInputAllowed: opts.imageInputAllowed }),
56
+ model: opts.profile.model,
57
+ }),
58
+ ...(opts.tools ?? []),
59
+ ];
60
+ if (opts.workers !== null)
61
+ tools.push(...workerTools(opts.workers));
62
+ const assembly = assembleTools({
63
+ tools,
64
+ sandbox,
65
+ processes,
66
+ env,
67
+ scope,
68
+ grants: { readRoots: grants.readRoots, writeRoots: grants.writeRoots, workspaceWritable: grants.workspaceWritable },
69
+ ...(opts.guard === undefined ? {} : { guard: opts.guard }),
70
+ });
71
+ // W791 (P1, §5.2 #2 — the "关键机关"): the CONTEXT sees the mode's model-visible
72
+ // face while `run_code`'s RegistryHandle stays bound to the INNER registry
73
+ // (`assembleTools` bound it above), so a program's `tools.read_file(...)` is
74
+ // dispatched exactly like a direct call was before the fold.
75
+ // W806 (P0): the SAME face is projected through the dynamic-disclosure policy.
76
+ // Absent `opts.disclosure` the policy's initial set IS the mode baseline, so
77
+ // `hidden()` is exactly the old fold and the wire array is byte-identical.
78
+ // Present, it withholds part of the disclosable universe and reveals it one
79
+ // turn at a time, appended at the tail (see `disclosure.ts`).
80
+ const mode = opts.mode ?? DEFAULT_SESSION_MODE;
81
+ // W9: the permission baseline's denied tools join the BLOCKED set (never
82
+ // disclosed), so the mode fold and the dynamic-disclosure layer both keep
83
+ // them out — the universe still lists them, which is what makes `hidden()`
84
+ // and therefore `exposedRegistry` drop them from the face.
85
+ const universe = assembly.registry.schemas().map((spec) => spec.name);
86
+ const blocked = [
87
+ ...(mode === "execution" ? universe.filter((name) => !EXECUTION_TOOL_NAMES.includes(name)) : []),
88
+ ...grants.toolDeny,
89
+ ];
90
+ const disclosure = new DisclosurePolicy({ universe, initial: opts.disclosure?.initial ?? universe, blocked });
91
+ const wrapped = mode === "execution" || opts.disclosure !== undefined || grants.toolDeny.length > 0;
92
+ const exposed = wrapped ? exposedRegistry(assembly.registry, disclosureExposure(disclosure)) : assembly.registry;
93
+ const plugin = definePlugin("studio.engine.tools", (ctx) => {
94
+ ctx.provide(TOOL_REGISTRY_SERVICE, exposed);
95
+ ctx.provide(SANDBOX_SERVICE, assembly.sandbox);
96
+ ctx.provide(PROCESS_REGISTRY_SERVICE, assembly.processes);
97
+ // W783: the same service instance the tool was constructed with, published
98
+ // as a seam too so an answerer layer (or a test) can reach it.
99
+ if (questions !== null) {
100
+ busHolder.current = ctx.require(EVENT_BUS_SERVICE);
101
+ ctx.provide(USER_QUESTION_SERVICE, questions);
102
+ }
103
+ });
104
+ return { plugin, registry: exposed, sandbox, processes, decision: choice.decision, disclosure };
105
+ }
106
+ /** Annotate one provider meta with the decision (`run_shell` reports it verbatim). */
107
+ function decidedMeta(meta, decision) {
108
+ return {
109
+ ...meta,
110
+ degraded: decision.degraded,
111
+ fallback_reason: decision.reason,
112
+ fallback_mode: decision.mode,
113
+ fallback_source: decision.source,
114
+ };
115
+ }
116
+ /** Wraps a policy-chosen sandbox so every result carries WHY it was chosen. */
117
+ class DecidedSandbox {
118
+ config;
119
+ decision;
120
+ inner;
121
+ constructor(inner, decision) {
122
+ this.inner = inner;
123
+ this.decision = decision;
124
+ this.config = inner.config;
125
+ }
126
+ async run(request) {
127
+ const result = await this.inner.run(request);
128
+ return { ...result, sandbox: decidedMeta(result.sandbox, this.decision) };
129
+ }
130
+ async spawn(request) {
131
+ const spawned = await this.inner.spawn(request);
132
+ return { ...spawned, sandbox: decidedMeta(spawned.sandbox, this.decision) };
133
+ }
134
+ }
135
+ /** Fail-closed provider: the `fail` policy's answer is a refusal, not a degrade. */
136
+ class RefusingSandbox {
137
+ config;
138
+ decision;
139
+ constructor(config, decision) {
140
+ this.config = config;
141
+ this.decision = decision;
142
+ }
143
+ async run() {
144
+ throw this.refusal();
145
+ }
146
+ async spawn() {
147
+ throw this.refusal();
148
+ }
149
+ /** The structured error every caller sees: `run_shell-sandbox: code=config …`. */
150
+ refusal() {
151
+ const reason = this.decision.reason ?? "bubblewrap is unusable on this host";
152
+ return new SandboxError("config", `${reason}; CELESTEA_SANDBOX_FALLBACK refuses to execute without OS isolation (only an 'unsandboxed' session grant overrides it)`, { provider: this.decision.provider, reason, mode: this.decision.mode, executed: false });
153
+ }
154
+ }
155
+ /**
156
+ * W516/W741: the session's sandbox comes from the provider POLICY — bwrap
157
+ * whenever the host can give it, with or without grants. Grants only ever widen
158
+ * what bwrap may keep (`network` → `--share-net`; `unsandboxed` → accept the
159
+ * userspace provider under `fail`), and nothing here degrades silently: a policy
160
+ * refusal (or an unreadable policy) becomes a [RefusingSandbox].
161
+ */
162
+ function chooseSandbox(env, grants, audit, probe, scope = null) {
163
+ let selection;
164
+ try {
165
+ const view = { network: grants.network, unsandboxed: grants.unsandboxed, workspaceWritable: grants.workspaceWritable, writeRoots: grants.writeRoots };
166
+ selection = selectSandboxDetailed({
167
+ env,
168
+ grants: view,
169
+ config: sessionSandboxConfig(scope, env),
170
+ ...(probe === undefined ? {} : { probe }),
171
+ });
172
+ }
173
+ catch (error) {
174
+ return refusedChoice(env, error, audit, scope);
175
+ }
176
+ const decision = {
177
+ provider: selection.provider,
178
+ degraded: selection.degraded,
179
+ reason: selection.reason,
180
+ mode: selection.mode,
181
+ source: selection.degradedByGrant ? "grant" : "policy",
182
+ };
183
+ if (selection.degradedByGrant) {
184
+ audit?.({ event: "degraded_by_grant", cap: "unsandboxed", provider: selection.provider, reason: selection.reason ?? undefined });
185
+ }
186
+ return { sandbox: new DecidedSandbox(selection.sandbox, decision), decision };
187
+ }
188
+ /** Policy refused (or could not be read): refuse to execute, never degrade. */
189
+ function refusedChoice(env, error, audit, scope = null) {
190
+ const reason = refusalReason(error);
191
+ const decision = { provider: "none", degraded: false, reason, mode: modeOrNull(env), source: "refused" };
192
+ audit?.({
193
+ event: "deny",
194
+ cap: "sandbox",
195
+ provider: "none",
196
+ reason,
197
+ detail: "the sandbox provider policy refuses to execute: no OS isolation and no degradation allowed (an 'unsandboxed' session grant is the only override)",
198
+ });
199
+ return { sandbox: new RefusingSandbox(sessionSandboxConfig(scope, env), decision), decision };
200
+ }
201
+ /** Explicit host injection: the policy is bypassed ON PURPOSE, and says so. */
202
+ function injectedChoice(sandbox, env) {
203
+ const decision = {
204
+ provider: sandbox.constructor.name === "" ? "injected" : sandbox.constructor.name,
205
+ degraded: null,
206
+ reason: "sandbox injected through EnginePluginInput.sandbox: the provider policy was bypassed explicitly",
207
+ mode: modeOrNull(env),
208
+ source: "injected",
209
+ };
210
+ return { sandbox: new DecidedSandbox(sandbox, decision), decision };
211
+ }
212
+ /** Cleanest available reason for a refusal (never a nested error envelope). */
213
+ function refusalReason(error) {
214
+ if (error instanceof SandboxError) {
215
+ const probeReason = error.detail["reason"];
216
+ if (typeof probeReason === "string" && probeReason !== "")
217
+ return `sandbox_unavailable: ${probeReason}`;
218
+ const badValue = error.detail["value"];
219
+ if (typeof badValue === "string")
220
+ return `invalid ${ENV_SANDBOX_FALLBACK}='${badValue}' (expected 'userspace' or 'fail')`;
221
+ }
222
+ return error instanceof Error ? error.message : String(error);
223
+ }
224
+ /** The operator's fallback mode as text, or null when it is unreadable (a typo). */
225
+ function modeOrNull(env) {
226
+ try {
227
+ return fallbackMode(env);
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
233
+ /** Provide the `Llm` seam (the offline engine by default). */
234
+ export function engineLlmPlugin(llm, name = "studio.engine.llm") {
235
+ return definePlugin(name, (ctx) => ctx.provide(LLM_SERVICE, llm));
236
+ }
237
+ /** Provide the agent loop driver (needed for worker driving). */
238
+ export function engineLoopPlugin(profile, name = "studio.engine.agent-loop") {
239
+ return agentLoopPlugin(agentConfigFromProfile(profile), {}, name);
240
+ }
241
+ /** Convenience: the three plugins in mount order (llm, loop, tools). */
242
+ export function enginePlugins(input) {
243
+ const tools = engineTools(input);
244
+ return {
245
+ plugins: [engineLlmPlugin(input.llm), engineLoopPlugin(input.profile), tools.plugin],
246
+ tools,
247
+ };
248
+ }
249
+ /**
250
+ * W783: the session's user-question service. `isLive` answers "is this
251
+ * generation still the live one?" — a settled/released instance must refuse
252
+ * instead of parking a question nobody can ever answer (§5.3 `CALLER_NOT_LIVE`).
253
+ *
254
+ * The bus is the runtime's own `EVENT_BUS_SERVICE` instance, handed in by the
255
+ * composer (`session-compose.ts` reads it from the Context at compose time, by
256
+ * which point `compose()` has already provided it).
257
+ */
258
+ function userQuestionsOf(wiring, bus) {
259
+ return createUserQuestionService({
260
+ registry: wiring.registry,
261
+ bus: lazyBus(bus),
262
+ sessionId: wiring.sessionId,
263
+ ...(wiring.isLive === undefined ? {} : { isLive: wiring.isLive }),
264
+ publish: wiring.publish,
265
+ record: wiring.record,
266
+ recordAnswer: wiring.recordAnswer,
267
+ });
268
+ }
269
+ /**
270
+ * A deferred view of the session's event bus. The service holds this for its
271
+ * whole life while the real bus only exists from the moment `compose()` provides
272
+ * it, so the first question resolves it and every later one reuses that answer.
273
+ */
274
+ function lazyBus(holder) {
275
+ const get = () => {
276
+ const bus = holder.current;
277
+ if (bus === null)
278
+ throw new Error("the session EventBus is not mounted yet: ask_user_question cannot reach the answerer waterfall");
279
+ return bus;
280
+ };
281
+ return {
282
+ on: (key, listener) => get().on(key, listener),
283
+ emit: (key, event) => get().emit(key, event),
284
+ bail: (key, listener) => get().bail(key, listener),
285
+ runBail: (key, event) => get().runBail(key, event),
286
+ waterfall: (key, listener) => get().waterfall(key, listener),
287
+ runWaterfall: (key, event, init) => get().runWaterfall(key, event, init),
288
+ waterfallAsync: (key, listener) => get().waterfallAsync(key, listener),
289
+ runWaterfallAsync: (key, event, init) => get().runWaterfallAsync(key, event, init),
290
+ counts: (key) => get().counts(key),
291
+ };
292
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `EngineProfile` (the HTTP-facing host view) <-> `Profile` (the runtime's
3
+ * frozen 12-key compose config).
4
+ *
5
+ * The two shapes differ on purpose: the host view uses the contract names of
6
+ * `GET /api/config` (`context_window`, no key source), while the engine profile
7
+ * carries the key RESOLUTION (env var name + optional file) and the request
8
+ * format. This module is the only translation point, so a new profile key can
9
+ * never be added on one side only.
10
+ */
11
+ import type { Profile } from "@celestea/runtime";
12
+ import type { EngineProfile, ProfilePatch } from "../runtime-adapter.js";
13
+ /** The only request format the TS engine composes today (OpenAI-compatible). */
14
+ export declare const ENGINE_REQUEST_FORMAT: Profile["request_format"];
15
+ /** Host view of a composed profile. */
16
+ export declare function engineProfileOf(profile: Profile): EngineProfile;
17
+ /** Compose profile from the host view (extra keys inherit from `base`). */
18
+ export declare function profileFromEngine(engine: EngineProfile, base?: Partial<Profile>): Profile;
19
+ /** Apply an accepted `POST /api/config` patch (the host already validated it). */
20
+ export declare function applyProfilePatch(profile: Profile, patch: ProfilePatch): Profile;
21
+ /**
22
+ * The startup profile: the host's frozen constants (`MIN_STEPS`,
23
+ * `CONTEXT_WINDOW`, both read off the frozen `/api/config` snapshot) plus the env
24
+ * overrides the host honors. The loop budget derives from this profile
25
+ * (`agentConfigFromProfile`), so the statusline window and the trim budget can
26
+ * never disagree.
27
+ */
28
+ export declare function defaultEngineProfile(env: NodeJS.ProcessEnv, apiKeyEnv: string): EngineProfile;