@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.
- package/LICENSE +21 -0
- package/dist/app.d.ts +111 -0
- package/dist/app.js +219 -0
- package/dist/auth/api-token.d.ts +76 -0
- package/dist/auth/api-token.js +172 -0
- package/dist/auth/htpasswd.d.ts +22 -0
- package/dist/auth/htpasswd.js +61 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +14 -0
- package/dist/auth/page.d.ts +19 -0
- package/dist/auth/page.js +73 -0
- package/dist/auth/rate-limit.d.ts +27 -0
- package/dist/auth/rate-limit.js +47 -0
- package/dist/auth/token.d.ts +49 -0
- package/dist/auth/token.js +112 -0
- package/dist/config.d.ts +78 -0
- package/dist/config.js +72 -0
- package/dist/deployment.d.ts +92 -0
- package/dist/deployment.js +154 -0
- package/dist/fake-runtime-adapter.d.ts +37 -0
- package/dist/fake-runtime-adapter.js +276 -0
- package/dist/handlers/auth.d.ts +24 -0
- package/dist/handlers/auth.js +155 -0
- package/dist/handlers/common.d.ts +76 -0
- package/dist/handlers/common.js +127 -0
- package/dist/handlers/config-shape.d.ts +76 -0
- package/dist/handlers/config-shape.js +227 -0
- package/dist/handlers/config.d.ts +26 -0
- package/dist/handlers/config.js +161 -0
- package/dist/handlers/context-shape.d.ts +33 -0
- package/dist/handlers/context-shape.js +59 -0
- package/dist/handlers/dialog.d.ts +22 -0
- package/dist/handlers/dialog.js +292 -0
- package/dist/handlers/exec.d.ts +22 -0
- package/dist/handlers/exec.js +131 -0
- package/dist/handlers/fs-read.d.ts +63 -0
- package/dist/handlers/fs-read.js +159 -0
- package/dist/handlers/fs.d.ts +57 -0
- package/dist/handlers/fs.js +153 -0
- package/dist/handlers/grants-shape.d.ts +45 -0
- package/dist/handlers/grants-shape.js +130 -0
- package/dist/handlers/grants.d.ts +21 -0
- package/dist/handlers/grants.js +267 -0
- package/dist/handlers/health.d.ts +37 -0
- package/dist/handlers/health.js +139 -0
- package/dist/handlers/index.d.ts +40 -0
- package/dist/handlers/index.js +85 -0
- package/dist/handlers/permissions.d.ts +9 -0
- package/dist/handlers/permissions.js +183 -0
- package/dist/handlers/plugins.d.ts +18 -0
- package/dist/handlers/plugins.js +23 -0
- package/dist/handlers/prompts.d.ts +12 -0
- package/dist/handlers/prompts.js +120 -0
- package/dist/handlers/providers.d.ts +12 -0
- package/dist/handlers/providers.js +178 -0
- package/dist/handlers/questions.d.ts +23 -0
- package/dist/handlers/questions.js +115 -0
- package/dist/handlers/session-model.d.ts +38 -0
- package/dist/handlers/session-model.js +105 -0
- package/dist/handlers/session-move.d.ts +22 -0
- package/dist/handlers/session-move.js +188 -0
- package/dist/handlers/session-tools.d.ts +19 -0
- package/dist/handlers/session-tools.js +75 -0
- package/dist/handlers/sessions.d.ts +32 -0
- package/dist/handlers/sessions.js +242 -0
- package/dist/handlers/usage.d.ts +26 -0
- package/dist/handlers/usage.js +77 -0
- package/dist/handlers/worker.d.ts +12 -0
- package/dist/handlers/worker.js +82 -0
- package/dist/handlers/workspaces.d.ts +12 -0
- package/dist/handlers/workspaces.js +88 -0
- package/dist/harness.test-util.d.ts +101 -0
- package/dist/harness.test-util.js +176 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/main.d.ts +13 -0
- package/dist/main.js +35 -0
- package/dist/plugins.d.ts +105 -0
- package/dist/plugins.js +97 -0
- package/dist/question-registry.d.ts +124 -0
- package/dist/question-registry.js +212 -0
- package/dist/question-rows.d.ts +32 -0
- package/dist/question-rows.js +43 -0
- package/dist/replay/compare.d.ts +47 -0
- package/dist/replay/compare.js +89 -0
- package/dist/replay/e2e-replay.d.ts +46 -0
- package/dist/replay/e2e-replay.js +91 -0
- package/dist/replay/expect-compact.d.ts +31 -0
- package/dist/replay/expect-compact.js +92 -0
- package/dist/replay/fixtures.d.ts +52 -0
- package/dist/replay/fixtures.js +53 -0
- package/dist/replay/host.d.ts +32 -0
- package/dist/replay/host.js +62 -0
- package/dist/replay/index.d.ts +16 -0
- package/dist/replay/index.js +16 -0
- package/dist/replay/probes.d.ts +55 -0
- package/dist/replay/probes.js +225 -0
- package/dist/replay/report.d.ts +13 -0
- package/dist/replay/report.js +103 -0
- package/dist/replay/session-e2e.d.ts +44 -0
- package/dist/replay/session-e2e.js +91 -0
- package/dist/routes.d.ts +47 -0
- package/dist/routes.js +59 -0
- package/dist/runtime/attachments-llm.d.ts +13 -0
- package/dist/runtime/attachments-llm.js +37 -0
- package/dist/runtime/boot-recovery.d.ts +32 -0
- package/dist/runtime/boot-recovery.js +71 -0
- package/dist/runtime/context-snapshot.d.ts +59 -0
- package/dist/runtime/context-snapshot.js +113 -0
- package/dist/runtime/engine-grants.d.ts +121 -0
- package/dist/runtime/engine-grants.js +338 -0
- package/dist/runtime/engine-permissions.d.ts +29 -0
- package/dist/runtime/engine-permissions.js +92 -0
- package/dist/runtime/engine-plugins.d.ts +189 -0
- package/dist/runtime/engine-plugins.js +292 -0
- package/dist/runtime/engine-profile.d.ts +28 -0
- package/dist/runtime/engine-profile.js +88 -0
- package/dist/runtime/engine-session.d.ts +9 -0
- package/dist/runtime/engine-session.js +9 -0
- package/dist/runtime/fallback-contract.d.ts +32 -0
- package/dist/runtime/fallback-contract.js +10 -0
- package/dist/runtime/fallback-host.d.ts +134 -0
- package/dist/runtime/fallback-host.js +299 -0
- package/dist/runtime/host-autowake.d.ts +79 -0
- package/dist/runtime/host-autowake.js +86 -0
- package/dist/runtime/image-downgrade.d.ts +49 -0
- package/dist/runtime/image-downgrade.js +90 -0
- package/dist/runtime/inbox-message.d.ts +11 -0
- package/dist/runtime/inbox-message.js +20 -0
- package/dist/runtime/index.d.ts +17 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/ledger-view.d.ts +40 -0
- package/dist/runtime/ledger-view.js +56 -0
- package/dist/runtime/llm-assembly.d.ts +66 -0
- package/dist/runtime/llm-assembly.js +114 -0
- package/dist/runtime/offline-llm.d.ts +71 -0
- package/dist/runtime/offline-llm.js +132 -0
- package/dist/runtime/provider-target.d.ts +10 -0
- package/dist/runtime/provider-target.js +10 -0
- package/dist/runtime/question-host.d.ts +33 -0
- package/dist/runtime/question-host.js +41 -0
- package/dist/runtime/question-view.d.ts +48 -0
- package/dist/runtime/question-view.js +76 -0
- package/dist/runtime/real-runtime-adapter.d.ts +95 -0
- package/dist/runtime/real-runtime-adapter.js +553 -0
- package/dist/runtime/recovery-audit.d.ts +81 -0
- package/dist/runtime/recovery-audit.js +95 -0
- package/dist/runtime/recovery-view.d.ts +36 -0
- package/dist/runtime/recovery-view.js +63 -0
- package/dist/runtime/session-compose.d.ts +235 -0
- package/dist/runtime/session-compose.js +350 -0
- package/dist/runtime/session-grants.d.ts +36 -0
- package/dist/runtime/session-grants.js +69 -0
- package/dist/runtime/session-lifecycle.d.ts +40 -0
- package/dist/runtime/session-lifecycle.js +86 -0
- package/dist/runtime/session-publisher.d.ts +40 -0
- package/dist/runtime/session-publisher.js +48 -0
- package/dist/runtime/session-release.d.ts +53 -0
- package/dist/runtime/session-release.js +80 -0
- package/dist/runtime/test-util.d.ts +78 -0
- package/dist/runtime/test-util.js +162 -0
- package/dist/runtime/watchdog-view.d.ts +33 -0
- package/dist/runtime/watchdog-view.js +45 -0
- package/dist/runtime/worker-bridge.d.ts +76 -0
- package/dist/runtime/worker-bridge.js +156 -0
- package/dist/runtime/worker-live.d.ts +22 -0
- package/dist/runtime/worker-live.js +25 -0
- package/dist/runtime/worker-recovery.d.ts +35 -0
- package/dist/runtime/worker-recovery.js +64 -0
- package/dist/runtime/worker-table.d.ts +58 -0
- package/dist/runtime/worker-table.js +70 -0
- package/dist/runtime-adapter.d.ts +421 -0
- package/dist/runtime-adapter.js +76 -0
- package/dist/serial-queue.d.ts +16 -0
- package/dist/serial-queue.js +22 -0
- package/dist/server.d.ts +44 -0
- package/dist/server.js +119 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +31 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.js +186 -0
- package/dist/static.d.ts +20 -0
- package/dist/static.js +120 -0
- package/dist/store/builtin-sections.d.ts +52 -0
- package/dist/store/builtin-sections.js +118 -0
- package/dist/store/celestea-home.d.ts +12 -0
- package/dist/store/celestea-home.js +11 -0
- package/dist/store/fs-json.d.ts +51 -0
- package/dist/store/fs-json.js +119 -0
- package/dist/store/grants-audit.d.ts +84 -0
- package/dist/store/grants-audit.js +109 -0
- package/dist/store/grants-service.d.ts +44 -0
- package/dist/store/grants-service.js +41 -0
- package/dist/store/grants-tokens.d.ts +62 -0
- package/dist/store/grants-tokens.js +105 -0
- package/dist/store/grants.d.ts +113 -0
- package/dist/store/grants.js +238 -0
- package/dist/store/index.d.ts +36 -0
- package/dist/store/index.js +36 -0
- package/dist/store/mode.d.ts +35 -0
- package/dist/store/mode.js +41 -0
- package/dist/store/permissions.d.ts +61 -0
- package/dist/store/permissions.js +139 -0
- package/dist/store/prompts-compose.d.ts +56 -0
- package/dist/store/prompts-compose.js +62 -0
- package/dist/store/prompts-template.d.ts +26 -0
- package/dist/store/prompts-template.js +99 -0
- package/dist/store/prompts.d.ts +142 -0
- package/dist/store/prompts.js +273 -0
- package/dist/store/provider-probe.d.ts +81 -0
- package/dist/store/provider-probe.js +154 -0
- package/dist/store/providers.d.ts +97 -0
- package/dist/store/providers.js +219 -0
- package/dist/store/result.d.ts +29 -0
- package/dist/store/result.js +30 -0
- package/dist/store/session-id.d.ts +119 -0
- package/dist/store/session-id.js +220 -0
- package/dist/store/session-meta.d.ts +41 -0
- package/dist/store/session-meta.js +66 -0
- package/dist/store/session-ops.d.ts +109 -0
- package/dist/store/session-ops.js +264 -0
- package/dist/store/session-tools.d.ts +42 -0
- package/dist/store/session-tools.js +69 -0
- package/dist/store/sessions.d.ts +162 -0
- package/dist/store/sessions.js +307 -0
- package/dist/store/validate.d.ts +13 -0
- package/dist/store/validate.js +38 -0
- package/dist/store/workspaces.d.ts +67 -0
- package/dist/store/workspaces.js +228 -0
- package/dist/user-questions.d.ts +58 -0
- package/dist/user-questions.js +157 -0
- package/dist/version.d.ts +14 -0
- package/dist/version.js +52 -0
- package/package.json +38 -0
- package/webdist/assets/index-Bngs7gUm.js +98 -0
- package/webdist/assets/index-Bu6ci_rN.css +1 -0
- package/webdist/assets/katex-CCbS2qAY.js +257 -0
- package/webdist/assets/mhchem-CnUN8HwY.js +1 -0
- package/webdist/build-meta.json +7 -0
- 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;
|