@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,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W767 — the three endpoints of Studio's OWN login-cookie gate.
|
|
3
|
+
*
|
|
4
|
+
* GET /login the self-contained login page (always reachable)
|
|
5
|
+
* POST /auth/login verify against Studio's password file, then Set-Cookie
|
|
6
|
+
* GET /auth/check 200/401 for nginx `auth_request` (the actual gate)
|
|
7
|
+
*
|
|
8
|
+
* nginx asks `/auth/check` for every other path and rewrites a 401 into a
|
|
9
|
+
* redirect to `/login`; these three are the only paths exempted there. The
|
|
10
|
+
* backend therefore stays usable from localhost with no cookie (the engine's own
|
|
11
|
+
* API is unchanged) while the public host is gated by the cookie.
|
|
12
|
+
*
|
|
13
|
+
* Hard rules kept here:
|
|
14
|
+
* - the password is verified through `htpasswd -vbi` (stdin, never argv);
|
|
15
|
+
* - a SUCCESSFUL login answers 200 with `Set-Cookie` AND the navigation in the
|
|
16
|
+
* same response (never a 302 — some mobile clients drop the cookie on a
|
|
17
|
+
* redirect);
|
|
18
|
+
* - the cookie value never reaches a log line, and a wrong password and an
|
|
19
|
+
* unknown user are indistinguishable (401 both ways).
|
|
20
|
+
*/
|
|
21
|
+
import { failJson } from "./common.js";
|
|
22
|
+
import { AUTH_COOKIE, AUTH_MAX_FAILURES, AUTH_WINDOW_MS, authCookie, cookieValue, createFailureLimiter, loadAuthSecret, loginPage, LOGIN_OK_PAGE, mintToken, verifyPassword, verifyToken, } from "../auth/index.js";
|
|
23
|
+
const HTML = "text/html; charset=utf-8";
|
|
24
|
+
const NO_STORE = "no-store";
|
|
25
|
+
export function registerAuth(app, deps, table) {
|
|
26
|
+
const gate = createGate(deps);
|
|
27
|
+
const page = table.get("get_login");
|
|
28
|
+
const login = table.get("post_auth_login");
|
|
29
|
+
const check = table.get("get_auth_check");
|
|
30
|
+
app.on(page.method, page.honoPath, () => pageResponse(loginPage(), 200));
|
|
31
|
+
app.on(check.method, check.honoPath, (c) => {
|
|
32
|
+
const user = cookieUser(c, gate);
|
|
33
|
+
return user === null
|
|
34
|
+
? jsonResponse({ ok: false, error: "unauthorized" }, 401)
|
|
35
|
+
: jsonResponse({ ok: true, user }, 200);
|
|
36
|
+
});
|
|
37
|
+
app.on(login.method, login.honoPath, (c) => loginResponse(c, gate));
|
|
38
|
+
return [page.id, login.id, check.id];
|
|
39
|
+
}
|
|
40
|
+
function createGate(deps) {
|
|
41
|
+
const limiter = createFailureLimiter({ now: Date.now, windowMs: AUTH_WINDOW_MS, maxFailures: AUTH_MAX_FAILURES });
|
|
42
|
+
const htpasswdFile = deps.config.paths.authHtpasswdFile;
|
|
43
|
+
let secret = null;
|
|
44
|
+
return {
|
|
45
|
+
limiter,
|
|
46
|
+
htpasswdFile,
|
|
47
|
+
secret: () => (secret ??= loadAuthSecret(deps.config.paths.authSecretFile)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The user of a valid cookie, or null (missing / tampered / expired / unknown). */
|
|
51
|
+
function cookieUser(c, gate) {
|
|
52
|
+
const raw = cookieValue(c.req.header("cookie"), AUTH_COOKIE);
|
|
53
|
+
if (raw === null)
|
|
54
|
+
return null;
|
|
55
|
+
const verdict = verifyToken(raw, gate.secret(), Math.floor(Date.now() / 1000));
|
|
56
|
+
return verdict.ok ? verdict.user : null;
|
|
57
|
+
}
|
|
58
|
+
/** One failed attempt is counted against BOTH the username and the client IP. */
|
|
59
|
+
function attemptKeys(c, user) {
|
|
60
|
+
return [`u:${user.toLowerCase()}`, `ip:${clientIp(c)}`];
|
|
61
|
+
}
|
|
62
|
+
async function loginResponse(c, gate) {
|
|
63
|
+
const wantsJson = prefersJson(c);
|
|
64
|
+
const creds = await credentialsOf(c);
|
|
65
|
+
if (creds === null)
|
|
66
|
+
return denied(c, wantsJson, 401, "invalid username or password", "用户名或密码不正确");
|
|
67
|
+
const keys = attemptKeys(c, creds.user);
|
|
68
|
+
if (keys.some((key) => gate.limiter.blocked(key))) {
|
|
69
|
+
return wantsJson
|
|
70
|
+
? failJson(c, 429, "too many failed login attempts", { retry_after: AUTH_WINDOW_MS / 1000 })
|
|
71
|
+
: pageResponse(loginPage("尝试次数过多,请稍后再试"), 429);
|
|
72
|
+
}
|
|
73
|
+
const verdict = verifyPassword(gate.htpasswdFile, creds.user, creds.pass);
|
|
74
|
+
if (verdict !== "ok") {
|
|
75
|
+
for (const key of keys)
|
|
76
|
+
gate.limiter.fail(key);
|
|
77
|
+
if (verdict === "error") {
|
|
78
|
+
warn(`password check failed to run (file=${gate.htpasswdFile}) — login denied`);
|
|
79
|
+
return denied(c, wantsJson, 500, "credential store unavailable", "凭据校验服务不可用,请联系管理员");
|
|
80
|
+
}
|
|
81
|
+
return denied(c, wantsJson, 401, "invalid username or password", "用户名或密码不正确");
|
|
82
|
+
}
|
|
83
|
+
for (const key of keys)
|
|
84
|
+
gate.limiter.clear(key);
|
|
85
|
+
const token = mintToken(creds.user, gate.secret(), Math.floor(Date.now() / 1000));
|
|
86
|
+
return wantsJson
|
|
87
|
+
? jsonResponse({ ok: true, user: creds.user }, 200, { "set-cookie": authCookie(token) })
|
|
88
|
+
: pageResponse(LOGIN_OK_PAGE, 200, { "set-cookie": authCookie(token) });
|
|
89
|
+
}
|
|
90
|
+
function denied(c, wantsJson, status, error, pageError) {
|
|
91
|
+
return wantsJson ? failJson(c, status, error) : pageResponse(loginPage(pageError), status);
|
|
92
|
+
}
|
|
93
|
+
/** `{username, password}` out of a form post OR a JSON body; null when absent. */
|
|
94
|
+
async function credentialsOf(c) {
|
|
95
|
+
let raw;
|
|
96
|
+
try {
|
|
97
|
+
raw = await c.req.text();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (raw.trim() === "")
|
|
103
|
+
return null;
|
|
104
|
+
const type = (c.req.header("content-type") ?? "").toLowerCase();
|
|
105
|
+
if (type.includes("json")) {
|
|
106
|
+
try {
|
|
107
|
+
return pickCredentials(JSON.parse(raw));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return pickCredentials(Object.fromEntries(new URLSearchParams(raw)));
|
|
114
|
+
}
|
|
115
|
+
function pickCredentials(body) {
|
|
116
|
+
const user = body["username"];
|
|
117
|
+
const pass = body["password"];
|
|
118
|
+
if (typeof user !== "string" || typeof pass !== "string")
|
|
119
|
+
return null;
|
|
120
|
+
return user.trim() === "" || pass === "" ? null : { user: user.trim(), pass };
|
|
121
|
+
}
|
|
122
|
+
/** JSON clients (curl/API) get JSON; a browser form post gets a page back. */
|
|
123
|
+
function prefersJson(c) {
|
|
124
|
+
const type = (c.req.header("content-type") ?? "").toLowerCase();
|
|
125
|
+
const accept = (c.req.header("accept") ?? "").toLowerCase();
|
|
126
|
+
return type.includes("json") || accept.includes("application/json");
|
|
127
|
+
}
|
|
128
|
+
/** The client IP nginx reports, else the (local) caller is one bucket. */
|
|
129
|
+
function clientIp(c) {
|
|
130
|
+
const real = c.req.header("x-real-ip");
|
|
131
|
+
if (real !== undefined && real.trim() !== "")
|
|
132
|
+
return real.trim();
|
|
133
|
+
const forwarded = c.req.header("x-forwarded-for");
|
|
134
|
+
if (forwarded !== undefined) {
|
|
135
|
+
const first = forwarded.split(",")[0]?.trim();
|
|
136
|
+
if (first !== undefined && first !== "")
|
|
137
|
+
return first;
|
|
138
|
+
}
|
|
139
|
+
return "local";
|
|
140
|
+
}
|
|
141
|
+
function pageResponse(body, status, extra = {}) {
|
|
142
|
+
return new Response(body, {
|
|
143
|
+
status,
|
|
144
|
+
headers: { "content-type": HTML, "cache-control": NO_STORE, ...extra },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function jsonResponse(body, status, extra = {}) {
|
|
148
|
+
return new Response(JSON.stringify(body), {
|
|
149
|
+
status,
|
|
150
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": NO_STORE, ...extra },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function warn(message) {
|
|
154
|
+
process.stderr.write(`studio auth: ${message}\n`);
|
|
155
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP helpers for the Studio handlers.
|
|
3
|
+
*
|
|
4
|
+
* Error bodies follow the frozen convention: `{"ok":false,"error":"<verbatim
|
|
5
|
+
* source string>"}` (`contracts/endpoints.json` §conventions). Two handlers in
|
|
6
|
+
* the retired backend return only `{"error":…}` (POST /api/turn's empty-input case
|
|
7
|
+
* and the static/API 404); those keep their special shape and say so locally.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from "hono";
|
|
10
|
+
import { CapacityError } from "../runtime-adapter.js";
|
|
11
|
+
import type { StudioServices } from "../plugins.js";
|
|
12
|
+
import type { StoreResult } from "../store/result.js";
|
|
13
|
+
/** Handler dependencies: the composed studio services. */
|
|
14
|
+
export type Deps = StudioServices;
|
|
15
|
+
export type JsonObject = Record<string, unknown>;
|
|
16
|
+
/** `{ok:false,error}` with the contract status (plus optional extra fields). */
|
|
17
|
+
export declare function failJson(c: Context, status: number, error: string, extra?: JsonObject): Response;
|
|
18
|
+
/**
|
|
19
|
+
* W513 capacity response: 503 + `Retry-After` (the engine refused to create
|
|
20
|
+
* another live session or another concurrent turn, and waiting helps).
|
|
21
|
+
*/
|
|
22
|
+
export declare function capacityJson(c: Context, error: CapacityError): Response;
|
|
23
|
+
/** Turn a store failure straight into its contract response. */
|
|
24
|
+
export declare function storeFail(c: Context, failure: Extract<StoreResult<never>, {
|
|
25
|
+
ok: false;
|
|
26
|
+
}>): Response;
|
|
27
|
+
/** `{error}`-only body (POST /api/turn empty input, static/API 404). */
|
|
28
|
+
export declare function errorOnly(c: Context, status: number, error: string): Response;
|
|
29
|
+
export type BodyRead = {
|
|
30
|
+
ok: true;
|
|
31
|
+
body: JsonObject;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
response: Response;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Read a JSON object body. The retired backend's rejections are mirrored:
|
|
38
|
+
* missing body -> 415, unparsable -> 400, non-object -> 422.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readJsonBody(c: Context, required?: boolean): Promise<BodyRead>;
|
|
41
|
+
export type FieldRead<T> = {
|
|
42
|
+
ok: true;
|
|
43
|
+
value: T | undefined;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
response: Response;
|
|
47
|
+
};
|
|
48
|
+
/** Optional string field: absent/null -> undefined, wrong type -> 422. */
|
|
49
|
+
export declare function strField(c: Context, body: JsonObject, name: string): FieldRead<string>;
|
|
50
|
+
/** Optional number field (JSON numbers only; no numeric strings). */
|
|
51
|
+
export declare function numField(c: Context, body: JsonObject, name: string): FieldRead<number>;
|
|
52
|
+
/** Optional string[] field. */
|
|
53
|
+
export declare function strArrayField(c: Context, body: JsonObject, name: string): FieldRead<string[]>;
|
|
54
|
+
/** Optional free-form JSON object field. */
|
|
55
|
+
export declare function objectField(c: Context, body: JsonObject, name: string): FieldRead<JsonObject>;
|
|
56
|
+
/** The active session id, or null (used by /api/status, /api/clear, prompts). */
|
|
57
|
+
export declare function activeSession(deps: Deps): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* W729: the mode of one session (`session.json.mode`; absent/unknown = the
|
|
60
|
+
* default `standard`, so a session created before this feature reads exactly as
|
|
61
|
+
* it always behaved). An unresolvable id also reads as the default — the
|
|
62
|
+
* endpoint that asked owns the 404.
|
|
63
|
+
*/
|
|
64
|
+
export declare function modeOfSession(deps: Deps, session: string | null): string;
|
|
65
|
+
/**
|
|
66
|
+
* W870: does ONE session carry its OWN `session.json.model` override?
|
|
67
|
+
*
|
|
68
|
+
* `/api/status.model` is the session instance's profile model (global base +
|
|
69
|
+
* this override, see `runtime/session-compose.ts` `profileFor`), so a client
|
|
70
|
+
* cannot tell "the global default happens to equal this session's model" from
|
|
71
|
+
* "this session is pinned" by looking at `model` alone. The statusline's model
|
|
72
|
+
* picker needs exactly that distinction to say 「本会话已固定模型」 instead of
|
|
73
|
+
* silently switching something the user cannot see. An unresolvable id (and the
|
|
74
|
+
* detached/absent session) answers false — the endpoint that asked owns the 404.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sessionModelCovered(deps: Deps, session: string | null): boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP helpers for the Studio handlers.
|
|
3
|
+
*
|
|
4
|
+
* Error bodies follow the frozen convention: `{"ok":false,"error":"<verbatim
|
|
5
|
+
* source string>"}` (`contracts/endpoints.json` §conventions). Two handlers in
|
|
6
|
+
* the retired backend return only `{"error":…}` (POST /api/turn's empty-input case
|
|
7
|
+
* and the static/API 404); those keep their special shape and say so locally.
|
|
8
|
+
*/
|
|
9
|
+
import { CapacityError } from "../runtime-adapter.js";
|
|
10
|
+
import { effectiveMode } from "../store/mode.js";
|
|
11
|
+
import { readSessionMeta } from "../store/session-meta.js";
|
|
12
|
+
/** `{ok:false,error}` with the contract status (plus optional extra fields). */
|
|
13
|
+
export function failJson(c, status, error, extra) {
|
|
14
|
+
return c.json(extra === undefined ? { ok: false, error } : { ok: false, error, ...extra }, status);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* W513 capacity response: 503 + `Retry-After` (the engine refused to create
|
|
18
|
+
* another live session or another concurrent turn, and waiting helps).
|
|
19
|
+
*/
|
|
20
|
+
export function capacityJson(c, error) {
|
|
21
|
+
const response = failJson(c, 503, error.message);
|
|
22
|
+
response.headers.set("retry-after", String(error.retryAfterSeconds));
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
/** Turn a store failure straight into its contract response. */
|
|
26
|
+
export function storeFail(c, failure) {
|
|
27
|
+
return failJson(c, failure.status, failure.error, failure.extra);
|
|
28
|
+
}
|
|
29
|
+
/** `{error}`-only body (POST /api/turn empty input, static/API 404). */
|
|
30
|
+
export function errorOnly(c, status, error) {
|
|
31
|
+
return c.json({ error }, status);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read a JSON object body. The retired backend's rejections are mirrored:
|
|
35
|
+
* missing body -> 415, unparsable -> 400, non-object -> 422.
|
|
36
|
+
*/
|
|
37
|
+
export async function readJsonBody(c, required = true) {
|
|
38
|
+
const raw = await c.req.text();
|
|
39
|
+
if (raw.trim() === "") {
|
|
40
|
+
if (required)
|
|
41
|
+
return { ok: false, response: failJson(c, 415, "request body required") };
|
|
42
|
+
return { ok: true, body: {} };
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { ok: false, response: failJson(c, 400, "invalid JSON body") };
|
|
50
|
+
}
|
|
51
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
52
|
+
return { ok: false, response: failJson(c, 422, "expected a JSON object body") };
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, body: parsed };
|
|
55
|
+
}
|
|
56
|
+
/** Optional string field: absent/null -> undefined, wrong type -> 422. */
|
|
57
|
+
export function strField(c, body, name) {
|
|
58
|
+
const v = body[name];
|
|
59
|
+
if (v === undefined || v === null)
|
|
60
|
+
return { ok: true, value: undefined };
|
|
61
|
+
if (typeof v !== "string")
|
|
62
|
+
return { ok: false, response: failJson(c, 422, `field '${name}' must be a string`) };
|
|
63
|
+
return { ok: true, value: v };
|
|
64
|
+
}
|
|
65
|
+
/** Optional number field (JSON numbers only; no numeric strings). */
|
|
66
|
+
export function numField(c, body, name) {
|
|
67
|
+
const v = body[name];
|
|
68
|
+
if (v === undefined || v === null)
|
|
69
|
+
return { ok: true, value: undefined };
|
|
70
|
+
if (typeof v !== "number" || !Number.isFinite(v))
|
|
71
|
+
return { ok: false, response: failJson(c, 422, `field '${name}' must be a number`) };
|
|
72
|
+
return { ok: true, value: v };
|
|
73
|
+
}
|
|
74
|
+
/** Optional string[] field. */
|
|
75
|
+
export function strArrayField(c, body, name) {
|
|
76
|
+
const v = body[name];
|
|
77
|
+
if (v === undefined || v === null)
|
|
78
|
+
return { ok: true, value: undefined };
|
|
79
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
80
|
+
return { ok: false, response: failJson(c, 422, `field '${name}' must be an array of strings`) };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, value: v };
|
|
83
|
+
}
|
|
84
|
+
/** Optional free-form JSON object field. */
|
|
85
|
+
export function objectField(c, body, name) {
|
|
86
|
+
const v = body[name];
|
|
87
|
+
if (v === undefined || v === null)
|
|
88
|
+
return { ok: true, value: undefined };
|
|
89
|
+
if (typeof v !== "object" || Array.isArray(v))
|
|
90
|
+
return { ok: false, response: failJson(c, 422, `field '${name}' must be an object`) };
|
|
91
|
+
return { ok: true, value: v };
|
|
92
|
+
}
|
|
93
|
+
/** The active session id, or null (used by /api/status, /api/clear, prompts). */
|
|
94
|
+
export function activeSession(deps) {
|
|
95
|
+
return deps.workspaces.activeSession();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* W729: the mode of one session (`session.json.mode`; absent/unknown = the
|
|
99
|
+
* default `standard`, so a session created before this feature reads exactly as
|
|
100
|
+
* it always behaved). An unresolvable id also reads as the default — the
|
|
101
|
+
* endpoint that asked owns the 404.
|
|
102
|
+
*/
|
|
103
|
+
export function modeOfSession(deps, session) {
|
|
104
|
+
if (session === null || session === "")
|
|
105
|
+
return effectiveMode(null);
|
|
106
|
+
const resolved = deps.sessions.resolve(session);
|
|
107
|
+
return effectiveMode(resolved.ok ? readSessionMeta(resolved.value.dir)?.mode : null);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* W870: does ONE session carry its OWN `session.json.model` override?
|
|
111
|
+
*
|
|
112
|
+
* `/api/status.model` is the session instance's profile model (global base +
|
|
113
|
+
* this override, see `runtime/session-compose.ts` `profileFor`), so a client
|
|
114
|
+
* cannot tell "the global default happens to equal this session's model" from
|
|
115
|
+
* "this session is pinned" by looking at `model` alone. The statusline's model
|
|
116
|
+
* picker needs exactly that distinction to say 「本会话已固定模型」 instead of
|
|
117
|
+
* silently switching something the user cannot see. An unresolvable id (and the
|
|
118
|
+
* detached/absent session) answers false — the endpoint that asked owns the 404.
|
|
119
|
+
*/
|
|
120
|
+
export function sessionModelCovered(deps, session) {
|
|
121
|
+
if (session === null || session === "")
|
|
122
|
+
return false;
|
|
123
|
+
const resolved = deps.sessions.resolve(session);
|
|
124
|
+
if (!resolved.ok)
|
|
125
|
+
return false;
|
|
126
|
+
return (readSessionMeta(resolved.value.dir)?.model ?? "") !== "";
|
|
127
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/api/config` response shape, assembled from live stores.
|
|
3
|
+
*
|
|
4
|
+
* `available.models` is rebuilt on EVERY read from the providers store
|
|
5
|
+
* (`src/api.rs:61-68`): one row per (provider, model) pair — de-dup is per
|
|
6
|
+
* provider since W750 — carrying the display name (`provider`), the stable id
|
|
7
|
+
* (`provider_id`), whether that exact pair is the composed one (`active`) and
|
|
8
|
+
* `reasoning` = the model row declares at least one effort. `system_prompt` is
|
|
9
|
+
* either the host override (POST /api/config) or the registry assembly
|
|
10
|
+
* (`build_gen`).
|
|
11
|
+
*
|
|
12
|
+
* S1 (W729): `system_prompt` is the FOCUSED (active) session's assembly. The
|
|
13
|
+
* engine's own per-session prompt comes from the same function with an explicit
|
|
14
|
+
* session id, injected into the composer by `app.ts` — one assembly path, two
|
|
15
|
+
* callers, so the UI and the engine can never disagree about a session's mode.
|
|
16
|
+
*/
|
|
17
|
+
import { type SessionMode } from "../store/mode.js";
|
|
18
|
+
import type { PromptScope } from "../store/prompts.js";
|
|
19
|
+
import { type ResolvedSession } from "../store/sessions.js";
|
|
20
|
+
import type { Deps, JsonObject } from "./common.js";
|
|
21
|
+
export declare const EFFORTS: readonly string[];
|
|
22
|
+
export interface AvailableModel {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
/** Provider DISPLAY name (grouping header); `provider_id` is the stable id. */
|
|
26
|
+
provider: string;
|
|
27
|
+
/** W750: the provider's stable id — what a switch must send back. */
|
|
28
|
+
provider_id: string;
|
|
29
|
+
/** W750: this exact (provider, model) pair is the one the engine routes to. */
|
|
30
|
+
active: boolean;
|
|
31
|
+
reasoning: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Resolve the active session's directory (null when nothing is active). */
|
|
34
|
+
export declare function activeResolved(deps: Deps): ResolvedSession | null;
|
|
35
|
+
/** Prompt scope of the active session's workspace (global when unknown). */
|
|
36
|
+
export declare function activeScope(deps: Deps): PromptScope;
|
|
37
|
+
/** Prompt scope of one session (global when the session cannot be resolved). */
|
|
38
|
+
export declare function scopeOf(deps: Deps, resolved: ResolvedSession | null): PromptScope;
|
|
39
|
+
/** The session's directory, or null when the id does not resolve here. */
|
|
40
|
+
export declare function resolveIfKnown(deps: Deps, sessionId: string | null): ResolvedSession | null;
|
|
41
|
+
/** `prompt` binding of the active session, if it has one. */
|
|
42
|
+
export declare function activePromptBinding(deps: Deps): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The model picker's catalogue, rebuilt from the live providers store.
|
|
45
|
+
*
|
|
46
|
+
* W750 (bug fix): de-duplication is PER PROVIDER, never global. The same model
|
|
47
|
+
* id under two providers is two different choices — the provider is what decides
|
|
48
|
+
* the endpoint the request goes to — so a global `seen` set silently swallowed
|
|
49
|
+
* every provider after the first one that shared an id (production: provider
|
|
50
|
+
* 「基元」 vanished because it also lists `deepseek-flash`). Identical ids
|
|
51
|
+
* repeated INSIDE one provider are still dropped (a store typo, not a choice).
|
|
52
|
+
*
|
|
53
|
+
* `active` marks the single (provider, model) pair the engine would use right
|
|
54
|
+
* now: same model id AND same endpoint as the composed profile. When nothing
|
|
55
|
+
* matches the endpoint — a custom base_url override, or a non
|
|
56
|
+
* `chat_completions` provider whose switch never rewrites the endpoint — the
|
|
57
|
+
* model id alone is enough ONLY if it is unambiguous; an ambiguous id is left
|
|
58
|
+
* unmarked rather than marked wrong.
|
|
59
|
+
*/
|
|
60
|
+
export declare function availableModels(deps: Deps): AvailableModel[];
|
|
61
|
+
/**
|
|
62
|
+
* Registry-assembled (or overridden) system prompt.
|
|
63
|
+
*
|
|
64
|
+
* W729 (§5.1 #4/#5, S1/S2): with an explicit `sessionId` the WHOLE assembly is
|
|
65
|
+
* resolved against THAT session — its workspace scope, its bound prompt, its
|
|
66
|
+
* `session.json` model, its own tool face and its own mode. That is what makes
|
|
67
|
+
* "one mode per session" true for a BACKGROUND session and not only for the focused one
|
|
68
|
+
* (R3); `null` keeps the historical reading (the ACTIVE session's scope, the
|
|
69
|
+
* process model, the default generation's tools), which is what the startup
|
|
70
|
+
* priming and `GET /api/config` use.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assembleSystemPromptFor(deps: Deps, sessionId?: string | null, mode?: SessionMode, env?: NodeJS.ProcessEnv): string;
|
|
73
|
+
/** Effective base_url: host override wins over the engine profile. */
|
|
74
|
+
export declare function baseUrlOf(deps: Deps): string;
|
|
75
|
+
/** GET /api/config body (also the POST /api/config response). */
|
|
76
|
+
export declare function configView(deps: Deps): JsonObject;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/api/config` response shape, assembled from live stores.
|
|
3
|
+
*
|
|
4
|
+
* `available.models` is rebuilt on EVERY read from the providers store
|
|
5
|
+
* (`src/api.rs:61-68`): one row per (provider, model) pair — de-dup is per
|
|
6
|
+
* provider since W750 — carrying the display name (`provider`), the stable id
|
|
7
|
+
* (`provider_id`), whether that exact pair is the composed one (`active`) and
|
|
8
|
+
* `reasoning` = the model row declares at least one effort. `system_prompt` is
|
|
9
|
+
* either the host override (POST /api/config) or the registry assembly
|
|
10
|
+
* (`build_gen`).
|
|
11
|
+
*
|
|
12
|
+
* S1 (W729): `system_prompt` is the FOCUSED (active) session's assembly. The
|
|
13
|
+
* engine's own per-session prompt comes from the same function with an explicit
|
|
14
|
+
* session id, injected into the composer by `app.ts` — one assembly path, two
|
|
15
|
+
* callers, so the UI and the engine can never disagree about a session's mode.
|
|
16
|
+
*/
|
|
17
|
+
import { deploymentFacts } from "../deployment.js";
|
|
18
|
+
import { DEFAULT_SESSION_MODE } from "../store/mode.js";
|
|
19
|
+
import { assembleSystemPrompt, resolveActivePrompt, toPromptVars } from "../store/prompts-compose.js";
|
|
20
|
+
import { readSessionMeta } from "../store/session-meta.js";
|
|
21
|
+
import { sessionWorkspaceOf } from "../store/sessions.js";
|
|
22
|
+
import { activeSession } from "./common.js";
|
|
23
|
+
export const EFFORTS = ["low", "high", "max"];
|
|
24
|
+
/** Resolve the active session's directory (null when nothing is active). */
|
|
25
|
+
export function activeResolved(deps) {
|
|
26
|
+
const id = activeSession(deps);
|
|
27
|
+
if (id === null)
|
|
28
|
+
return null;
|
|
29
|
+
const res = deps.sessions.resolve(id);
|
|
30
|
+
return res.ok ? res.value : null;
|
|
31
|
+
}
|
|
32
|
+
/** Prompt scope of the active session's workspace (global when unknown). */
|
|
33
|
+
export function activeScope(deps) {
|
|
34
|
+
return scopeOf(deps, activeResolved(deps));
|
|
35
|
+
}
|
|
36
|
+
/** Prompt scope of one session (global when the session cannot be resolved). */
|
|
37
|
+
export function scopeOf(deps, resolved) {
|
|
38
|
+
if (resolved === null)
|
|
39
|
+
return deps.prompts.scopeGlobal();
|
|
40
|
+
return deps.prompts.scopeWorkspace(resolved.workspace, resolved.wsPath);
|
|
41
|
+
}
|
|
42
|
+
/** The session's directory, or null when the id does not resolve here. */
|
|
43
|
+
export function resolveIfKnown(deps, sessionId) {
|
|
44
|
+
if (sessionId === null || sessionId === "")
|
|
45
|
+
return null;
|
|
46
|
+
const res = deps.sessions.resolve(sessionId);
|
|
47
|
+
return res.ok ? res.value : null;
|
|
48
|
+
}
|
|
49
|
+
/** `prompt` binding of the active session, if it has one. */
|
|
50
|
+
export function activePromptBinding(deps) {
|
|
51
|
+
const resolved = activeResolved(deps);
|
|
52
|
+
if (resolved === null)
|
|
53
|
+
return null;
|
|
54
|
+
return readSessionMeta(resolved.dir)?.prompt ?? null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Provider display name that lists `model`, else "" (static rows).
|
|
58
|
+
*
|
|
59
|
+
* W750: the same model id can live under several providers, so the provider the
|
|
60
|
+
* engine actually routes to — the one whose `base_url` is the active one — wins;
|
|
61
|
+
* only when no provider matches the active endpoint does the first lister win
|
|
62
|
+
* (the historical reading, kept for custom endpoints).
|
|
63
|
+
*/
|
|
64
|
+
function providerOf(deps, model) {
|
|
65
|
+
const rows = deps.providers.rows();
|
|
66
|
+
const listed = rows.filter((p) => p.models.some((m) => m.id === model));
|
|
67
|
+
if (listed.length === 0)
|
|
68
|
+
return "";
|
|
69
|
+
const activeBase = trimSlash(baseUrlOf(deps));
|
|
70
|
+
const exact = listed.find((p) => trimSlash(p.base_url) === activeBase);
|
|
71
|
+
return (exact ?? listed[0]).name;
|
|
72
|
+
}
|
|
73
|
+
/** Trailing-slash-insensitive compare (providers.json and the profile differ). */
|
|
74
|
+
function trimSlash(url) {
|
|
75
|
+
return url.replace(/\/+$/, "");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The model picker's catalogue, rebuilt from the live providers store.
|
|
79
|
+
*
|
|
80
|
+
* W750 (bug fix): de-duplication is PER PROVIDER, never global. The same model
|
|
81
|
+
* id under two providers is two different choices — the provider is what decides
|
|
82
|
+
* the endpoint the request goes to — so a global `seen` set silently swallowed
|
|
83
|
+
* every provider after the first one that shared an id (production: provider
|
|
84
|
+
* 「基元」 vanished because it also lists `deepseek-flash`). Identical ids
|
|
85
|
+
* repeated INSIDE one provider are still dropped (a store typo, not a choice).
|
|
86
|
+
*
|
|
87
|
+
* `active` marks the single (provider, model) pair the engine would use right
|
|
88
|
+
* now: same model id AND same endpoint as the composed profile. When nothing
|
|
89
|
+
* matches the endpoint — a custom base_url override, or a non
|
|
90
|
+
* `chat_completions` provider whose switch never rewrites the endpoint — the
|
|
91
|
+
* model id alone is enough ONLY if it is unambiguous; an ambiguous id is left
|
|
92
|
+
* unmarked rather than marked wrong.
|
|
93
|
+
*/
|
|
94
|
+
export function availableModels(deps) {
|
|
95
|
+
const activeModel = deps.runtime.profile().model;
|
|
96
|
+
const activeBase = trimSlash(baseUrlOf(deps));
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const p of deps.providers.rows()) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
const base = trimSlash(p.base_url);
|
|
101
|
+
for (const m of p.models) {
|
|
102
|
+
if (seen.has(m.id))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(m.id);
|
|
105
|
+
out.push({
|
|
106
|
+
id: m.id,
|
|
107
|
+
name: m.name === "" ? m.id : m.name,
|
|
108
|
+
provider: p.name,
|
|
109
|
+
provider_id: p.id,
|
|
110
|
+
active: m.id === activeModel && base === activeBase,
|
|
111
|
+
reasoning: m.reasoning_efforts.length > 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!out.some((e) => e.active)) {
|
|
116
|
+
const same = out.filter((e) => e.id === activeModel);
|
|
117
|
+
if (same.length === 1)
|
|
118
|
+
same[0].active = true;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* W782: the `{{studio_*}}` values, resolved from the live deployment. Kept as a
|
|
124
|
+
* named helper so the derivation lives in ONE place and a test can pin it.
|
|
125
|
+
*/
|
|
126
|
+
function deploymentVars(deps, env) {
|
|
127
|
+
const facts = deploymentFacts(deps.config, env);
|
|
128
|
+
return {
|
|
129
|
+
studio_repo: facts.repo,
|
|
130
|
+
studio_frontend_dir: facts.frontendDir,
|
|
131
|
+
studio_static_root: facts.staticRoot,
|
|
132
|
+
studio_service: facts.service,
|
|
133
|
+
studio_bind: facts.bind,
|
|
134
|
+
studio_site: facts.publicSite,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Registry-assembled (or overridden) system prompt.
|
|
139
|
+
*
|
|
140
|
+
* W729 (§5.1 #4/#5, S1/S2): with an explicit `sessionId` the WHOLE assembly is
|
|
141
|
+
* resolved against THAT session — its workspace scope, its bound prompt, its
|
|
142
|
+
* `session.json` model, its own tool face and its own mode. That is what makes
|
|
143
|
+
* "one mode per session" true for a BACKGROUND session and not only for the focused one
|
|
144
|
+
* (R3); `null` keeps the historical reading (the ACTIVE session's scope, the
|
|
145
|
+
* process model, the default generation's tools), which is what the startup
|
|
146
|
+
* priming and `GET /api/config` use.
|
|
147
|
+
*/
|
|
148
|
+
export function assembleSystemPromptFor(deps, sessionId = null, mode, env = process.env) {
|
|
149
|
+
const override = deps.settings.systemPromptOverride();
|
|
150
|
+
if (override !== null)
|
|
151
|
+
return override;
|
|
152
|
+
const profile = deps.runtime.profile();
|
|
153
|
+
const scoped = sessionId === null ? null : resolveIfKnown(deps, sessionId);
|
|
154
|
+
const resolved = sessionId === null ? activeResolved(deps) : scoped;
|
|
155
|
+
const meta = resolved === null ? null : readSessionMeta(resolved.dir);
|
|
156
|
+
// The session's own model wins for a scoped assembly; the process model stays
|
|
157
|
+
// the source for the legacy (null) reading, so a no-mode session's prompt is
|
|
158
|
+
// byte-for-byte what it was before W729 (K8).
|
|
159
|
+
// TS 6.0 (TS2871, new syntactic nullish check): the inner `?? null` was
|
|
160
|
+
// redundant anyway — `meta?.model` is `string | undefined` and the outer `??`
|
|
161
|
+
// already covers both nullish cases, so the result type and behaviour are
|
|
162
|
+
// unchanged while the `?? null ?? x` pattern the new check rejects is gone.
|
|
163
|
+
const model = (scoped === null ? null : meta?.model) ?? profile.model;
|
|
164
|
+
const scope = scopeOf(deps, resolved);
|
|
165
|
+
// W768: the prompt's workspace NAME and ROOT PATH come from the ONE projector
|
|
166
|
+
// the composer also uses for the sandbox cwd (`sessionWorkspaceOf`) — a prompt
|
|
167
|
+
// naming one workspace while the shell starts in another is impossible now.
|
|
168
|
+
const workspace = sessionWorkspaceOf(resolved);
|
|
169
|
+
const vars = toPromptVars({
|
|
170
|
+
model,
|
|
171
|
+
provider: providerOf(deps, model),
|
|
172
|
+
base_url: baseUrlOf(deps),
|
|
173
|
+
workspace: workspace?.name ?? "",
|
|
174
|
+
workspace_dir: workspace?.path ?? "",
|
|
175
|
+
session: resolved?.id ?? "",
|
|
176
|
+
// W791 (P1, S2/M9): the tool face follows the RESOLVED session — for the
|
|
177
|
+
// legacy (null) reading that is the FOCUSED session, for a scoped one the
|
|
178
|
+
// session itself. A no-mode session's face is the full registry, so the
|
|
179
|
+
// rendered value of every pre-P1 session is unchanged.
|
|
180
|
+
tools: toolsOf(deps, resolved?.id ?? null).map((t) => t.name).join(", "),
|
|
181
|
+
context_window: profile.context_window,
|
|
182
|
+
max_output_tokens: profile.max_output_tokens,
|
|
183
|
+
date: new Date().toISOString().slice(0, 10),
|
|
184
|
+
// W782: the deployment facts the `environment` section renders. Every one is
|
|
185
|
+
// derived here (this process's own checkout + the operator's config), so the
|
|
186
|
+
// template states where the RUNNING service actually is — not where some
|
|
187
|
+
// hand-edited string once said it was.
|
|
188
|
+
...deploymentVars(deps, env),
|
|
189
|
+
});
|
|
190
|
+
const binding = scoped === null ? activePromptBinding(deps) : (meta?.prompt ?? null);
|
|
191
|
+
// `mode` (explicit) wins over the session's own: the BASE generation is primed
|
|
192
|
+
// with the DEFAULT mode on purpose, so one execution session can never leak
|
|
193
|
+
// its variant into the prompt every session without a mode inherits (R3).
|
|
194
|
+
return assembleSystemPrompt(deps.prompts, scope, resolveActivePrompt(deps.prompts, scope, binding), vars, mode ?? meta?.mode ?? DEFAULT_SESSION_MODE);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The tool face the `{{tools}}` variable renders: the session's own generation
|
|
198
|
+
* when it has one, else the default generation. W791 (P1): in `execution` mode
|
|
199
|
+
* the session's generation PROVIDES the folded face, so the rendered list is the
|
|
200
|
+
* same set `GET /api/tools?session=X` answers (M9) — one source, two readers.
|
|
201
|
+
*/
|
|
202
|
+
function toolsOf(deps, sessionId) {
|
|
203
|
+
const sessionTools = deps.runtime.sessionTools;
|
|
204
|
+
if (sessionId === null || sessionTools === undefined)
|
|
205
|
+
return deps.runtime.tools();
|
|
206
|
+
return sessionTools.call(deps.runtime, sessionId);
|
|
207
|
+
}
|
|
208
|
+
/** Effective base_url: host override wins over the engine profile. */
|
|
209
|
+
export function baseUrlOf(deps) {
|
|
210
|
+
return deps.settings.baseUrlOverride() ?? deps.runtime.profile().base_url;
|
|
211
|
+
}
|
|
212
|
+
/** GET /api/config body (also the POST /api/config response). */
|
|
213
|
+
export function configView(deps) {
|
|
214
|
+
const profile = deps.runtime.profile();
|
|
215
|
+
return {
|
|
216
|
+
model: profile.model,
|
|
217
|
+
base_url: baseUrlOf(deps),
|
|
218
|
+
max_steps: profile.max_steps,
|
|
219
|
+
max_parallel_tool_calls: profile.max_parallel_tool_calls,
|
|
220
|
+
reasoning_effort: profile.reasoning_effort,
|
|
221
|
+
max_output_tokens: profile.max_output_tokens,
|
|
222
|
+
context_window: profile.context_window,
|
|
223
|
+
system_prompt: assembleSystemPromptFor(deps),
|
|
224
|
+
api_key_env: deps.config.apiKeyEnv,
|
|
225
|
+
available: { models: availableModels(deps), efforts: [...EFFORTS] },
|
|
226
|
+
};
|
|
227
|
+
}
|