@prismshadow/penguin-server 0.0.1 → 0.1.0
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/README.md +1 -1
- package/dist/api/types.d.ts +29 -9
- package/dist/index.js +555 -313
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/web-dist/assets/{index-7oqK2MPp.js → index-B-18IFfR.js} +1 -1
- package/web-dist/assets/index-BfFhfbzq.css +1 -0
- package/web-dist/assets/index-BhliHHUc.js +101 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-DGY2Myoq.js +0 -51
- package/web-dist/assets/index-SaWO9NKB.css +0 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { serve } from "@hono/node-server";
|
|
|
5
5
|
// src/app.ts
|
|
6
6
|
import fs16 from "fs";
|
|
7
7
|
import fsp from "fs/promises";
|
|
8
|
-
import
|
|
8
|
+
import path15 from "path";
|
|
9
9
|
import { Hono as Hono19 } from "hono";
|
|
10
10
|
|
|
11
11
|
// src/db/database.ts
|
|
@@ -15,30 +15,30 @@ import path from "path";
|
|
|
15
15
|
// src/db/schema.ts
|
|
16
16
|
var SCHEMA_SQL = `
|
|
17
17
|
CREATE TABLE IF NOT EXISTS users (
|
|
18
|
-
user_id TEXT PRIMARY KEY, --
|
|
18
|
+
user_id TEXT PRIMARY KEY, -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$
|
|
19
19
|
password_hash TEXT NOT NULL, -- scrypt$N$r$p$salt$hash(base64)
|
|
20
|
-
is_admin INTEGER NOT NULL DEFAULT 0, --
|
|
21
|
-
password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1
|
|
20
|
+
is_admin INTEGER NOT NULL DEFAULT 0, -- 1 for the built-in admin (seeded at startup)
|
|
21
|
+
password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=initial password (seeded/admin-set); cleared once the user changes it
|
|
22
22
|
created_at TEXT NOT NULL
|
|
23
23
|
);
|
|
24
24
|
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
25
|
-
token_hash TEXT PRIMARY KEY, -- sha256(token) hex
|
|
25
|
+
token_hash TEXT PRIMARY KEY, -- sha256(token) hex; the cookie stores only the raw token
|
|
26
26
|
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
|
27
27
|
created_at TEXT NOT NULL,
|
|
28
|
-
expires_at TEXT NOT NULL -- 7
|
|
28
|
+
expires_at TEXT NOT NULL -- 7-day sliding renewal (topped up when <6 days remain)
|
|
29
29
|
);
|
|
30
30
|
CREATE TABLE IF NOT EXISTS projects (
|
|
31
|
-
project_id TEXT PRIMARY KEY, --
|
|
31
|
+
project_id TEXT PRIMARY KEY, -- directory name doubles as id; display name lives in project_config.toml
|
|
32
32
|
owner_user_id TEXT NOT NULL REFERENCES users(user_id),
|
|
33
33
|
created_at TEXT NOT NULL
|
|
34
34
|
);
|
|
35
|
-
CREATE TABLE IF NOT EXISTS project_members ( --
|
|
35
|
+
CREATE TABLE IF NOT EXISTS project_members ( -- member grants only; owners are not stored here
|
|
36
36
|
project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
|
|
37
37
|
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
|
38
38
|
created_at TEXT NOT NULL,
|
|
39
39
|
PRIMARY KEY (project_id, user_id)
|
|
40
40
|
);
|
|
41
|
-
CREATE TABLE IF NOT EXISTS agents ( --
|
|
41
|
+
CREATE TABLE IF NOT EXISTS agents ( -- index only; name/description live in system_config.yaml
|
|
42
42
|
project_id TEXT NOT NULL,
|
|
43
43
|
agent_id TEXT NOT NULL,
|
|
44
44
|
created_at TEXT NOT NULL,
|
|
@@ -48,64 +48,64 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
48
48
|
session_id TEXT PRIMARY KEY,
|
|
49
49
|
project_id TEXT NOT NULL,
|
|
50
50
|
agent_id TEXT NOT NULL,
|
|
51
|
-
provider TEXT NOT NULL, --
|
|
52
|
-
model_id TEXT NOT NULL, --
|
|
51
|
+
provider TEXT NOT NULL, -- provider group of the session model (paired with model_id as a model reference)
|
|
52
|
+
model_id TEXT NOT NULL, -- upstream model id (sent to AgentHub as-is; never concatenate <provider>/<id>)
|
|
53
53
|
workspace TEXT NOT NULL,
|
|
54
54
|
approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask
|
|
55
|
-
title TEXT, --
|
|
56
|
-
archived_at TEXT, --
|
|
57
|
-
source TEXT, --
|
|
55
|
+
title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
|
|
56
|
+
archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under "Archived")
|
|
57
|
+
source TEXT, -- session origin: NULL=user-created | schedule (scheduled task) | subagent (child session)
|
|
58
58
|
created_at TEXT NOT NULL
|
|
59
59
|
);
|
|
60
60
|
CREATE TABLE IF NOT EXISTS usage_records (
|
|
61
61
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
62
62
|
ts TEXT NOT NULL,
|
|
63
|
-
date TEXT NOT NULL, --
|
|
63
|
+
date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)
|
|
64
64
|
project_id TEXT NOT NULL,
|
|
65
65
|
agent_id TEXT NOT NULL,
|
|
66
|
-
session_id TEXT NOT NULL, --
|
|
67
|
-
origin_session_id TEXT, --
|
|
68
|
-
provider TEXT NOT NULL, --
|
|
69
|
-
model_id TEXT NOT NULL, --
|
|
66
|
+
session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session)
|
|
67
|
+
origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session
|
|
68
|
+
provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id
|
|
69
|
+
model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately)
|
|
70
70
|
cache_read INTEGER NOT NULL,
|
|
71
71
|
cache_write INTEGER NOT NULL,
|
|
72
72
|
output INTEGER NOT NULL,
|
|
73
|
-
total INTEGER NOT NULL, --
|
|
74
|
-
status TEXT NOT NULL DEFAULT 'completed' --
|
|
75
|
-
); --
|
|
73
|
+
total INTEGER NOT NULL, -- from token_usage.request (one row per Request)
|
|
74
|
+
status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)
|
|
75
|
+
); -- cost is not stored: computed at query time from current pricing
|
|
76
76
|
CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);
|
|
77
77
|
CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_records(session_id);
|
|
78
|
-
CREATE TABLE IF NOT EXISTS error_records ( --
|
|
78
|
+
CREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's "Errors" tab)
|
|
79
79
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
80
80
|
ts TEXT NOT NULL,
|
|
81
|
-
date TEXT NOT NULL, --
|
|
82
|
-
project_id TEXT, --
|
|
81
|
+
date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records)
|
|
82
|
+
project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context
|
|
83
83
|
agent_id TEXT,
|
|
84
84
|
session_id TEXT,
|
|
85
85
|
source TEXT NOT NULL, -- http | session | usage | title | subagent | process | llm | environment | schedule
|
|
86
|
-
kind TEXT NOT NULL, -- expected
|
|
86
|
+
kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime)
|
|
87
87
|
code TEXT NOT NULL, -- HttpError.code / internal / session_run_failed / ...
|
|
88
|
-
status INTEGER, -- HTTP
|
|
89
|
-
message TEXT NOT NULL --
|
|
88
|
+
status INTEGER, -- HTTP status code; NULL for non-HTTP sources
|
|
89
|
+
message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only)
|
|
90
90
|
);
|
|
91
91
|
CREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date);
|
|
92
|
-
CREATE TABLE IF NOT EXISTS schedule_state ( --
|
|
92
|
+
CREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back)
|
|
93
93
|
project_id TEXT NOT NULL,
|
|
94
94
|
agent_id TEXT NOT NULL,
|
|
95
|
-
name TEXT NOT NULL, --
|
|
96
|
-
creator_user_id TEXT, --
|
|
97
|
-
start_at_ms INTEGER NOT NULL, --
|
|
98
|
-
def_hash TEXT NOT NULL, --
|
|
99
|
-
last_slot_ms INTEGER, --
|
|
100
|
-
last_fired_at TEXT, --
|
|
101
|
-
fired_once INTEGER NOT NULL DEFAULT 0, --
|
|
102
|
-
missed INTEGER NOT NULL DEFAULT 0, --
|
|
103
|
-
invalid_reason TEXT, --
|
|
95
|
+
name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier
|
|
96
|
+
creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)
|
|
97
|
+
start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state
|
|
98
|
+
def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)
|
|
99
|
+
last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)
|
|
100
|
+
last_fired_at TEXT, -- last actual fire time (for display)
|
|
101
|
+
fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired
|
|
102
|
+
missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)
|
|
103
|
+
invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy
|
|
104
104
|
PRIMARY KEY (project_id, agent_id, name)
|
|
105
105
|
);
|
|
106
106
|
CREATE TABLE IF NOT EXISTS ui_prefs (
|
|
107
107
|
user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
|
|
108
|
-
prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...}
|
|
108
|
+
prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
|
|
109
109
|
);
|
|
110
110
|
`;
|
|
111
111
|
|
|
@@ -460,7 +460,7 @@ var SchedulesRepo = class {
|
|
|
460
460
|
).run(args.defHash, args.projectId, args.agentId, args.name);
|
|
461
461
|
}
|
|
462
462
|
const row = this.find(args.projectId, args.agentId, args.name);
|
|
463
|
-
if (!row) throw new Error("schedule_state
|
|
463
|
+
if (!row) throw new Error("Failed to read back schedule_state after registration");
|
|
464
464
|
return { row, fresh };
|
|
465
465
|
}
|
|
466
466
|
/** Advance the consumed scheduled time (advances whether triggered or skipped; restarts don't re-trigger). */
|
|
@@ -857,15 +857,15 @@ function modelCredentialMissing(modelId) {
|
|
|
857
857
|
return new HttpError(
|
|
858
858
|
400,
|
|
859
859
|
"model_credential_missing",
|
|
860
|
-
|
|
860
|
+
`Model ${modelId} has no API key yet. Configure it on the Models page first.`
|
|
861
861
|
);
|
|
862
862
|
}
|
|
863
863
|
function handleError(err, c) {
|
|
864
864
|
if (err instanceof HttpError) {
|
|
865
865
|
return c.json(errorBody(err.code, err.message), err.status);
|
|
866
866
|
}
|
|
867
|
-
console.error(`[server]
|
|
868
|
-
return c.json(errorBody("internal", "
|
|
867
|
+
console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);
|
|
868
|
+
return c.json(errorBody("internal", "Internal server error."), 500);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
// src/auth/middleware.ts
|
|
@@ -875,7 +875,7 @@ function authMiddleware(auth) {
|
|
|
875
875
|
const token = getCookie(c, SESSION_COOKIE);
|
|
876
876
|
const user = token ? auth.authenticate(token) : null;
|
|
877
877
|
if (!user) {
|
|
878
|
-
throw new HttpError(401, "unauthorized", "
|
|
878
|
+
throw new HttpError(401, "unauthorized", "Not signed in or the sign-in has expired.");
|
|
879
879
|
}
|
|
880
880
|
c.set("user", user);
|
|
881
881
|
await next();
|
|
@@ -886,7 +886,11 @@ var jsonOnlyWrites = async (c, next) => {
|
|
|
886
886
|
if (WRITE_METHODS.has(c.req.method)) {
|
|
887
887
|
const contentType = c.req.header("content-type");
|
|
888
888
|
if (contentType && !contentType.toLowerCase().startsWith("application/json")) {
|
|
889
|
-
throw new HttpError(
|
|
889
|
+
throw new HttpError(
|
|
890
|
+
415,
|
|
891
|
+
"unsupported_media_type",
|
|
892
|
+
"Write requests only accept application/json."
|
|
893
|
+
);
|
|
890
894
|
}
|
|
891
895
|
}
|
|
892
896
|
await next();
|
|
@@ -950,7 +954,7 @@ async function verifyPassword(password, stored) {
|
|
|
950
954
|
// src/auth/service.ts
|
|
951
955
|
var MIN_PASSWORD_LENGTH = 8;
|
|
952
956
|
var ADMIN_USER_ID = "admin";
|
|
953
|
-
var ADMIN_INITIAL_PASSWORD = "
|
|
957
|
+
var ADMIN_INITIAL_PASSWORD = "penguin-2026";
|
|
954
958
|
function sha256Hex(value) {
|
|
955
959
|
return createHash("sha256").update(value).digest("hex");
|
|
956
960
|
}
|
|
@@ -995,7 +999,7 @@ var AuthService = class {
|
|
|
995
999
|
const row = this.deps.users.findById(userId);
|
|
996
1000
|
const ok = row !== null && await verifyPassword(password, row.passwordHash);
|
|
997
1001
|
if (!row || !ok) {
|
|
998
|
-
throw new HttpError(401, "invalid_credentials", "
|
|
1002
|
+
throw new HttpError(401, "invalid_credentials", "Incorrect username or password.");
|
|
999
1003
|
}
|
|
1000
1004
|
this.deps.authSessions.deleteExpired(this.now().toISOString());
|
|
1001
1005
|
return { user: toUserInfo(row), token: this.issueSession(row.userId) };
|
|
@@ -1004,10 +1008,10 @@ var AuthService = class {
|
|
|
1004
1008
|
async changePassword(userId, oldPassword, newPassword) {
|
|
1005
1009
|
const row = this.deps.users.findById(userId);
|
|
1006
1010
|
if (!row || !await verifyPassword(oldPassword, row.passwordHash)) {
|
|
1007
|
-
throw new HttpError(400, "password_mismatch", "
|
|
1011
|
+
throw new HttpError(400, "password_mismatch", "Current password is incorrect.");
|
|
1008
1012
|
}
|
|
1009
1013
|
if (newPassword.length < MIN_PASSWORD_LENGTH) {
|
|
1010
|
-
throw new HttpError(400, "invalid_password", "
|
|
1014
|
+
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
1011
1015
|
}
|
|
1012
1016
|
this.deps.users.updatePassword(userId, await hashPassword(newPassword), false);
|
|
1013
1017
|
}
|
|
@@ -1057,28 +1061,29 @@ function badRequest(message) {
|
|
|
1057
1061
|
function pathParam(c, name) {
|
|
1058
1062
|
const v = c.req.param(name);
|
|
1059
1063
|
if (v === void 0 || v === "") {
|
|
1060
|
-
throw new HttpError(404, "not_found", "
|
|
1064
|
+
throw new HttpError(404, "not_found", "Missing path parameter.");
|
|
1061
1065
|
}
|
|
1062
1066
|
return v;
|
|
1063
1067
|
}
|
|
1064
1068
|
function requireValidId(c, name) {
|
|
1065
1069
|
const v = pathParam(c, name);
|
|
1066
1070
|
if (!isValidId(v)) {
|
|
1067
|
-
throw new HttpError(404, "not_found", "
|
|
1071
|
+
throw new HttpError(404, "not_found", "Resource does not exist or you do not have access.");
|
|
1068
1072
|
}
|
|
1069
1073
|
return v;
|
|
1070
1074
|
}
|
|
1071
1075
|
function positiveIntParam(c, name) {
|
|
1072
1076
|
const v = Number.parseInt(pathParam(c, name), 10);
|
|
1073
|
-
if (!Number.isInteger(v) || v < 1) throw badRequest(`${name}
|
|
1077
|
+
if (!Number.isInteger(v) || v < 1) throw badRequest(`${name} must be a positive integer.`);
|
|
1074
1078
|
return v;
|
|
1075
1079
|
}
|
|
1076
1080
|
function paginationQuery(c) {
|
|
1077
1081
|
const offset = Number.parseInt(c.req.query("offset") ?? "0", 10);
|
|
1078
1082
|
const limit = Number.parseInt(c.req.query("limit") ?? "200", 10);
|
|
1079
|
-
if (!Number.isInteger(offset) || offset < 0)
|
|
1083
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
1084
|
+
throw badRequest("offset must be a non-negative integer.");
|
|
1080
1085
|
if (!Number.isInteger(limit) || limit < 1 || limit > 1e3) {
|
|
1081
|
-
throw badRequest("limit
|
|
1086
|
+
throw badRequest("limit must be an integer between 1 and 1000.");
|
|
1082
1087
|
}
|
|
1083
1088
|
return { offset, limit };
|
|
1084
1089
|
}
|
|
@@ -1087,25 +1092,25 @@ async function readJson(c) {
|
|
|
1087
1092
|
try {
|
|
1088
1093
|
body = await c.req.json();
|
|
1089
1094
|
} catch {
|
|
1090
|
-
throw badRequest("
|
|
1095
|
+
throw badRequest("Request body must be valid JSON.");
|
|
1091
1096
|
}
|
|
1092
1097
|
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
1093
|
-
throw badRequest("
|
|
1098
|
+
throw badRequest("Request body must be a JSON object.");
|
|
1094
1099
|
}
|
|
1095
1100
|
return body;
|
|
1096
1101
|
}
|
|
1097
1102
|
function requireString(obj, key, rule = {}) {
|
|
1098
1103
|
const v = obj[key];
|
|
1099
1104
|
const label = rule.label ?? key;
|
|
1100
|
-
if (typeof v !== "string") throw badRequest(`${label}
|
|
1105
|
+
if (typeof v !== "string") throw badRequest(`${label} must be a string.`);
|
|
1101
1106
|
if (rule.minLen !== void 0 && v.length < rule.minLen) {
|
|
1102
|
-
throw badRequest(`${label}
|
|
1107
|
+
throw badRequest(`${label} must be at least ${rule.minLen} characters.`);
|
|
1103
1108
|
}
|
|
1104
1109
|
if (rule.maxLen !== void 0 && v.length > rule.maxLen) {
|
|
1105
|
-
throw badRequest(`${label}
|
|
1110
|
+
throw badRequest(`${label} must be at most ${rule.maxLen} characters.`);
|
|
1106
1111
|
}
|
|
1107
1112
|
if (rule.pattern !== void 0 && !rule.pattern.test(v)) {
|
|
1108
|
-
throw badRequest(`${label}
|
|
1113
|
+
throw badRequest(`${label} has an invalid format.`);
|
|
1109
1114
|
}
|
|
1110
1115
|
return v;
|
|
1111
1116
|
}
|
|
@@ -1116,7 +1121,7 @@ function optionalString(obj, key, rule = {}) {
|
|
|
1116
1121
|
function requireEnum(obj, key, values, label = key) {
|
|
1117
1122
|
const v = obj[key];
|
|
1118
1123
|
if (typeof v !== "string" || !values.includes(v)) {
|
|
1119
|
-
throw badRequest(`${label}
|
|
1124
|
+
throw badRequest(`${label} must be one of ${values.join(" / ")}.`);
|
|
1120
1125
|
}
|
|
1121
1126
|
return v;
|
|
1122
1127
|
}
|
|
@@ -1128,18 +1133,18 @@ function optionalNumber(obj, key, rule = {}) {
|
|
|
1128
1133
|
const v = obj[key];
|
|
1129
1134
|
if (v === void 0) return void 0;
|
|
1130
1135
|
const label = rule.label ?? key;
|
|
1131
|
-
if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label}
|
|
1132
|
-
if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label}
|
|
1136
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw badRequest(`${label} must be a number.`);
|
|
1137
|
+
if (rule.integer && !Number.isInteger(v)) throw badRequest(`${label} must be an integer.`);
|
|
1133
1138
|
if (rule.positiveOrMinusOne && !(v > 0 || v === -1)) {
|
|
1134
|
-
throw badRequest(`${label}
|
|
1139
|
+
throw badRequest(`${label} must be greater than 0 or equal to -1.`);
|
|
1135
1140
|
}
|
|
1136
|
-
if (rule.nonNegative && v < 0) throw badRequest(`${label}
|
|
1141
|
+
if (rule.nonNegative && v < 0) throw badRequest(`${label} must not be negative.`);
|
|
1137
1142
|
return v;
|
|
1138
1143
|
}
|
|
1139
1144
|
function optionalDateParam(value, label) {
|
|
1140
1145
|
if (value === void 0 || value === "") return void 0;
|
|
1141
1146
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
1142
|
-
throw badRequest(`${label}
|
|
1147
|
+
throw badRequest(`${label} must be in YYYY-MM-DD format.`);
|
|
1143
1148
|
}
|
|
1144
1149
|
return value;
|
|
1145
1150
|
}
|
|
@@ -1149,7 +1154,7 @@ function adminUsersRoutes(deps2) {
|
|
|
1149
1154
|
const app2 = new Hono();
|
|
1150
1155
|
app2.use("*", async (c, next) => {
|
|
1151
1156
|
if (!c.var.user.isAdmin) {
|
|
1152
|
-
throw new HttpError(403, "admin_required", "
|
|
1157
|
+
throw new HttpError(403, "admin_required", "Only an admin can perform this operation.");
|
|
1153
1158
|
}
|
|
1154
1159
|
await next();
|
|
1155
1160
|
});
|
|
@@ -1386,7 +1391,7 @@ function membersRoutes(deps2) {
|
|
|
1386
1391
|
import { Hono as Hono7 } from "hono";
|
|
1387
1392
|
function parseRef(value, label) {
|
|
1388
1393
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
1389
|
-
throw badRequest(`${label}
|
|
1394
|
+
throw badRequest(`${label} must be a { provider, modelId } object.`);
|
|
1390
1395
|
}
|
|
1391
1396
|
const r = value;
|
|
1392
1397
|
return {
|
|
@@ -1395,10 +1400,10 @@ function parseRef(value, label) {
|
|
|
1395
1400
|
};
|
|
1396
1401
|
}
|
|
1397
1402
|
function parseModelsUpdate(body) {
|
|
1398
|
-
if (!Array.isArray(body.models)) throw badRequest("models
|
|
1403
|
+
if (!Array.isArray(body.models)) throw badRequest("models must be an array.");
|
|
1399
1404
|
const models = body.models.map((item, i) => {
|
|
1400
1405
|
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
1401
|
-
throw badRequest(`models[${i}]
|
|
1406
|
+
throw badRequest(`models[${i}] must be an object.`);
|
|
1402
1407
|
}
|
|
1403
1408
|
const m = item;
|
|
1404
1409
|
const entry = {
|
|
@@ -1415,7 +1420,7 @@ function parseModelsUpdate(body) {
|
|
|
1415
1420
|
};
|
|
1416
1421
|
if (m.displayName !== void 0) {
|
|
1417
1422
|
if (typeof m.displayName !== "string" || m.displayName.length > 100) {
|
|
1418
|
-
throw badRequest(`models[${i}].displayName
|
|
1423
|
+
throw badRequest(`models[${i}].displayName must be a string of at most 100 characters.`);
|
|
1419
1424
|
}
|
|
1420
1425
|
if (m.displayName) entry.displayName = m.displayName;
|
|
1421
1426
|
}
|
|
@@ -1424,31 +1429,31 @@ function parseModelsUpdate(body) {
|
|
|
1424
1429
|
}
|
|
1425
1430
|
if (m.contextWindow !== void 0) {
|
|
1426
1431
|
if (typeof m.contextWindow !== "number" || !(m.contextWindow > 0)) {
|
|
1427
|
-
throw badRequest(`models[${i}].contextWindow
|
|
1432
|
+
throw badRequest(`models[${i}].contextWindow must be a positive number.`);
|
|
1428
1433
|
}
|
|
1429
1434
|
entry.contextWindow = m.contextWindow;
|
|
1430
1435
|
}
|
|
1431
1436
|
if (m.clientType !== void 0) {
|
|
1432
1437
|
if (typeof m.clientType !== "string" || m.clientType.length > 64) {
|
|
1433
|
-
throw badRequest(`models[${i}].clientType
|
|
1438
|
+
throw badRequest(`models[${i}].clientType must be a string of at most 64 characters.`);
|
|
1434
1439
|
}
|
|
1435
1440
|
if (m.clientType) entry.clientType = m.clientType;
|
|
1436
1441
|
}
|
|
1437
1442
|
if (m.vision !== void 0) {
|
|
1438
1443
|
if (typeof m.vision !== "boolean") {
|
|
1439
|
-
throw badRequest(`models[${i}].vision
|
|
1444
|
+
throw badRequest(`models[${i}].vision must be a boolean.`);
|
|
1440
1445
|
}
|
|
1441
1446
|
entry.vision = m.vision;
|
|
1442
1447
|
}
|
|
1443
1448
|
if (m.pricing !== void 0) {
|
|
1444
1449
|
const p = m.pricing;
|
|
1445
1450
|
if (p === null || typeof p !== "object" || Array.isArray(p)) {
|
|
1446
|
-
throw badRequest(`models[${i}].pricing
|
|
1451
|
+
throw badRequest(`models[${i}].pricing must be an object.`);
|
|
1447
1452
|
}
|
|
1448
1453
|
for (const key of ["cacheRead", "cacheWrite", "output"]) {
|
|
1449
1454
|
const v = p[key];
|
|
1450
1455
|
if (typeof v !== "number" || !Number.isFinite(v) || v < 0) {
|
|
1451
|
-
throw badRequest(`models[${i}].pricing.${key}
|
|
1456
|
+
throw badRequest(`models[${i}].pricing.${key} must be a non-negative number.`);
|
|
1452
1457
|
}
|
|
1453
1458
|
}
|
|
1454
1459
|
entry.pricing = {
|
|
@@ -1459,19 +1464,19 @@ function parseModelsUpdate(body) {
|
|
|
1459
1464
|
}
|
|
1460
1465
|
if (m.apiKey !== void 0) {
|
|
1461
1466
|
if (typeof m.apiKey !== "string" || m.apiKey.length === 0) {
|
|
1462
|
-
throw badRequest(`models[${i}].apiKey
|
|
1467
|
+
throw badRequest(`models[${i}].apiKey must be a non-empty string.`);
|
|
1463
1468
|
}
|
|
1464
1469
|
entry.apiKey = m.apiKey;
|
|
1465
1470
|
}
|
|
1466
1471
|
if (m.clearApiKey !== void 0) {
|
|
1467
1472
|
if (typeof m.clearApiKey !== "boolean") {
|
|
1468
|
-
throw badRequest(`models[${i}].clearApiKey
|
|
1473
|
+
throw badRequest(`models[${i}].clearApiKey must be a boolean.`);
|
|
1469
1474
|
}
|
|
1470
1475
|
entry.clearApiKey = m.clearApiKey;
|
|
1471
1476
|
}
|
|
1472
1477
|
if (m.baseUrl !== void 0) {
|
|
1473
1478
|
if (m.baseUrl !== null && typeof m.baseUrl !== "string") {
|
|
1474
|
-
throw badRequest(`models[${i}].baseUrl
|
|
1479
|
+
throw badRequest(`models[${i}].baseUrl must be a string or null.`);
|
|
1475
1480
|
}
|
|
1476
1481
|
entry.baseUrl = m.baseUrl;
|
|
1477
1482
|
}
|
|
@@ -1508,21 +1513,25 @@ function modelsRoutes(deps2) {
|
|
|
1508
1513
|
modelId: requireString(body, "modelId", { minLen: 1, maxLen: 200 })
|
|
1509
1514
|
};
|
|
1510
1515
|
if (body.apiKey !== void 0) {
|
|
1511
|
-
if (typeof body.apiKey !== "string") throw badRequest("apiKey
|
|
1516
|
+
if (typeof body.apiKey !== "string") throw badRequest("apiKey must be a string.");
|
|
1512
1517
|
if (body.apiKey) req.apiKey = body.apiKey;
|
|
1513
1518
|
}
|
|
1514
1519
|
if (body.clearApiKey !== void 0) {
|
|
1515
|
-
if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey
|
|
1520
|
+
if (typeof body.clearApiKey !== "boolean") throw badRequest("clearApiKey must be a boolean.");
|
|
1516
1521
|
req.clearApiKey = body.clearApiKey;
|
|
1517
1522
|
}
|
|
1523
|
+
if (body.speed !== void 0) {
|
|
1524
|
+
if (typeof body.speed !== "boolean") throw badRequest("speed must be a boolean.");
|
|
1525
|
+
req.speed = body.speed;
|
|
1526
|
+
}
|
|
1518
1527
|
if (body.baseUrl !== void 0) {
|
|
1519
1528
|
if (body.baseUrl !== null && typeof body.baseUrl !== "string") {
|
|
1520
|
-
throw badRequest("baseUrl
|
|
1529
|
+
throw badRequest("baseUrl must be a string or null.");
|
|
1521
1530
|
}
|
|
1522
1531
|
req.baseUrl = body.baseUrl ? body.baseUrl : null;
|
|
1523
1532
|
}
|
|
1524
1533
|
if (body.clientType !== void 0) {
|
|
1525
|
-
if (typeof body.clientType !== "string") throw badRequest("clientType
|
|
1534
|
+
if (typeof body.clientType !== "string") throw badRequest("clientType must be a string.");
|
|
1526
1535
|
if (body.clientType) req.clientType = body.clientType;
|
|
1527
1536
|
}
|
|
1528
1537
|
return c.json(await deps2.projectConfigService.testModel(projectId, req));
|
|
@@ -1533,10 +1542,10 @@ function modelsRoutes(deps2) {
|
|
|
1533
1542
|
// src/http/routes/vault.ts
|
|
1534
1543
|
import { Hono as Hono8 } from "hono";
|
|
1535
1544
|
function parseVaultUpdate(body) {
|
|
1536
|
-
if (!Array.isArray(body.entries)) throw badRequest("entries
|
|
1545
|
+
if (!Array.isArray(body.entries)) throw badRequest("entries must be an array.");
|
|
1537
1546
|
const entries = body.entries.map((item, i) => {
|
|
1538
1547
|
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
1539
|
-
throw badRequest(`entries[${i}]
|
|
1548
|
+
throw badRequest(`entries[${i}] must be an object.`);
|
|
1540
1549
|
}
|
|
1541
1550
|
const e = item;
|
|
1542
1551
|
const entry = {
|
|
@@ -1544,7 +1553,7 @@ function parseVaultUpdate(body) {
|
|
|
1544
1553
|
};
|
|
1545
1554
|
if (e.value !== void 0) {
|
|
1546
1555
|
if (typeof e.value !== "string" || e.value.length === 0) {
|
|
1547
|
-
throw badRequest(`entries[${i}].value
|
|
1556
|
+
throw badRequest(`entries[${i}].value must be a non-empty string.`);
|
|
1548
1557
|
}
|
|
1549
1558
|
entry.value = e.value;
|
|
1550
1559
|
}
|
|
@@ -1566,6 +1575,7 @@ function vaultRoutes(deps2) {
|
|
|
1566
1575
|
deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
|
1567
1576
|
const req = parseVaultUpdate(await readJson(c));
|
|
1568
1577
|
const res = await deps2.agentConfigService.updateVault(projectId, agentId, req);
|
|
1578
|
+
deps2.manager.invalidateAgentRuntimes(projectId, agentId);
|
|
1569
1579
|
return c.json(res);
|
|
1570
1580
|
});
|
|
1571
1581
|
return app2;
|
|
@@ -1603,72 +1613,77 @@ function parseScheduleFile(name, raw) {
|
|
|
1603
1613
|
} catch (err) {
|
|
1604
1614
|
return {
|
|
1605
1615
|
ok: false,
|
|
1606
|
-
error: `TOML
|
|
1616
|
+
error: `Failed to parse TOML: ${err instanceof Error ? err.message : String(err)}`
|
|
1607
1617
|
};
|
|
1608
1618
|
}
|
|
1609
1619
|
if (parsed === null || typeof parsed !== "object")
|
|
1610
|
-
return { ok: false, error: "
|
|
1620
|
+
return { ok: false, error: "Content is not a TOML table" };
|
|
1611
1621
|
const t = parsed;
|
|
1612
1622
|
const prompt = t["prompt"];
|
|
1613
1623
|
if (typeof prompt !== "string" || prompt.trim() === "") {
|
|
1614
|
-
return { ok: false, error: "
|
|
1624
|
+
return { ok: false, error: "Missing required field prompt" };
|
|
1615
1625
|
}
|
|
1616
1626
|
const enabled = t["enabled"] === void 0 ? false : t["enabled"];
|
|
1617
|
-
if (typeof enabled !== "boolean") return { ok: false, error: "enabled
|
|
1627
|
+
if (typeof enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
|
|
1618
1628
|
const startAt = parseInstant(t["start_at"]);
|
|
1619
|
-
if (startAt === null)
|
|
1629
|
+
if (startAt === null)
|
|
1630
|
+
return { ok: false, error: "start_at is missing or not a valid ISO 8601 instant" };
|
|
1620
1631
|
let period;
|
|
1621
1632
|
let periodMs;
|
|
1622
1633
|
if (t["period"] !== void 0) {
|
|
1623
|
-
if (typeof t["period"] !== "string") return { ok: false, error: "period
|
|
1634
|
+
if (typeof t["period"] !== "string") return { ok: false, error: "period must be a string" };
|
|
1624
1635
|
const ms = parsePeriod(t["period"]);
|
|
1625
|
-
if (ms === null) return { ok: false, error: "period
|
|
1626
|
-
if (ms < MIN_PERIOD_MS) return { ok: false, error: "period
|
|
1636
|
+
if (ms === null) return { ok: false, error: "period must look like 30m / 12h / 7d" };
|
|
1637
|
+
if (ms < MIN_PERIOD_MS) return { ok: false, error: "period is below the 5m minimum" };
|
|
1627
1638
|
period = t["period"].trim();
|
|
1628
1639
|
periodMs = ms;
|
|
1629
1640
|
}
|
|
1630
1641
|
let endAt;
|
|
1631
1642
|
if (t["end_at"] !== void 0) {
|
|
1632
1643
|
const parsedEnd = parseInstant(t["end_at"]);
|
|
1633
|
-
if (parsedEnd === null) return { ok: false, error: "end_at
|
|
1634
|
-
if (parsedEnd.ms <= startAt.ms)
|
|
1644
|
+
if (parsedEnd === null) return { ok: false, error: "end_at is not a valid ISO 8601 instant" };
|
|
1645
|
+
if (parsedEnd.ms <= startAt.ms)
|
|
1646
|
+
return { ok: false, error: "end_at must be later than start_at" };
|
|
1635
1647
|
endAt = parsedEnd;
|
|
1636
1648
|
}
|
|
1637
1649
|
let sessionId;
|
|
1638
1650
|
if (t["session_id"] !== void 0) {
|
|
1639
1651
|
if (typeof t["session_id"] !== "string" || t["session_id"] === "") {
|
|
1640
|
-
return { ok: false, error: "session_id
|
|
1652
|
+
return { ok: false, error: "session_id must be a non-empty string" };
|
|
1641
1653
|
}
|
|
1642
1654
|
sessionId = t["session_id"];
|
|
1643
1655
|
}
|
|
1644
1656
|
let workspace;
|
|
1645
1657
|
if (t["workspace"] !== void 0) {
|
|
1646
1658
|
if (typeof t["workspace"] !== "string" || t["workspace"] === "") {
|
|
1647
|
-
return { ok: false, error: "workspace
|
|
1659
|
+
return { ok: false, error: "workspace must be a non-empty string" };
|
|
1648
1660
|
}
|
|
1649
1661
|
workspace = t["workspace"];
|
|
1650
1662
|
}
|
|
1651
1663
|
let modelId;
|
|
1652
1664
|
if (t["model_id"] !== void 0) {
|
|
1653
1665
|
if (typeof t["model_id"] !== "string" || t["model_id"] === "") {
|
|
1654
|
-
return { ok: false, error: "model_id
|
|
1666
|
+
return { ok: false, error: "model_id must be a non-empty string" };
|
|
1655
1667
|
}
|
|
1656
1668
|
modelId = t["model_id"];
|
|
1657
1669
|
}
|
|
1658
1670
|
let provider;
|
|
1659
1671
|
if (t["provider"] !== void 0) {
|
|
1660
1672
|
if (typeof t["provider"] !== "string" || t["provider"] === "") {
|
|
1661
|
-
return { ok: false, error: "provider
|
|
1673
|
+
return { ok: false, error: "provider must be a non-empty string" };
|
|
1662
1674
|
}
|
|
1663
1675
|
provider = t["provider"];
|
|
1664
1676
|
}
|
|
1665
|
-
if (provider !== void 0 && modelId === void 0) {
|
|
1666
|
-
return { ok: false, error: "provider \u4EC5\u4E0E model_id \u6210\u5BF9\u4F7F\u7528\uFF08\u6A21\u578B\u5F15\u7528\u987B\u6210\u5BF9\u7ED9\u51FA\uFF09" };
|
|
1667
|
-
}
|
|
1668
1677
|
if (sessionId !== void 0 && (workspace !== void 0 || modelId !== void 0 || provider !== void 0)) {
|
|
1669
1678
|
return {
|
|
1670
1679
|
ok: false,
|
|
1671
|
-
error: "
|
|
1680
|
+
error: "Pick one target: workspace and provider / model_id are only for new-Session mode"
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
if (modelId === void 0 !== (provider === void 0)) {
|
|
1684
|
+
return {
|
|
1685
|
+
ok: false,
|
|
1686
|
+
error: "provider and model_id must be given together (a model reference is always a pair); omit both to use the Project's default model"
|
|
1672
1687
|
};
|
|
1673
1688
|
}
|
|
1674
1689
|
return {
|
|
@@ -1763,7 +1778,10 @@ function serializeSchedule(fields) {
|
|
|
1763
1778
|
`;
|
|
1764
1779
|
}
|
|
1765
1780
|
async function validateScheduleModelRef(root, projectId, def) {
|
|
1766
|
-
if (def.modelId === void 0) return null;
|
|
1781
|
+
if (def.modelId === void 0 && def.provider === void 0) return null;
|
|
1782
|
+
if (def.modelId === void 0 || def.provider === void 0) {
|
|
1783
|
+
return "provider and model_id must be given together (a model reference is always a pair); omit both to use the Project's default model";
|
|
1784
|
+
}
|
|
1767
1785
|
try {
|
|
1768
1786
|
const cfg = await loadProjectConfig(root, projectId);
|
|
1769
1787
|
resolveModelRef(cfg, def.modelId, def.provider);
|
|
@@ -1789,7 +1807,7 @@ async function deleteScheduleFile(root, projectId, agentId, name) {
|
|
|
1789
1807
|
|
|
1790
1808
|
// src/http/routes/schedules.ts
|
|
1791
1809
|
function parseUpsertBody(body) {
|
|
1792
|
-
if (typeof body.enabled !== "boolean") throw badRequest("enabled
|
|
1810
|
+
if (typeof body.enabled !== "boolean") throw badRequest("enabled must be a boolean.");
|
|
1793
1811
|
const prompt = requireString(body, "prompt", { minLen: 1, maxLen: 1e5 });
|
|
1794
1812
|
const startAt = requireString(body, "startAt", { minLen: 1, maxLen: 100 });
|
|
1795
1813
|
const period = optionalString(body, "period", { minLen: 1, maxLen: 20 });
|
|
@@ -1849,7 +1867,7 @@ function toItem(def, state, queued, nowMs) {
|
|
|
1849
1867
|
};
|
|
1850
1868
|
}
|
|
1851
1869
|
function requireScheduleName(raw) {
|
|
1852
|
-
if (!raw || !isValidId2(raw)) throw badRequest("
|
|
1870
|
+
if (!raw || !isValidId2(raw)) throw badRequest("Invalid schedule name.");
|
|
1853
1871
|
return raw;
|
|
1854
1872
|
}
|
|
1855
1873
|
function scheduleRoutes(deps2) {
|
|
@@ -1875,7 +1893,7 @@ function scheduleRoutes(deps2) {
|
|
|
1875
1893
|
const body = await readJson(c);
|
|
1876
1894
|
const name = requireScheduleName(requireString(body, "name", { minLen: 1, maxLen: 100 }));
|
|
1877
1895
|
if (await readScheduleFile(deps2.config.root, projectId, agentId, name)) {
|
|
1878
|
-
throw new HttpError(409, "schedule_exists",
|
|
1896
|
+
throw new HttpError(409, "schedule_exists", `Schedule already exists: ${name}`);
|
|
1879
1897
|
}
|
|
1880
1898
|
await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
|
|
1881
1899
|
return c.json(await readItem(deps2, projectId, agentId, name), 201);
|
|
@@ -1894,7 +1912,7 @@ function scheduleRoutes(deps2) {
|
|
|
1894
1912
|
deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
|
1895
1913
|
const name = requireScheduleName(c.req.param("name"));
|
|
1896
1914
|
if (!await readScheduleFile(deps2.config.root, projectId, agentId, name)) {
|
|
1897
|
-
throw new HttpError(404, "schedule_not_found",
|
|
1915
|
+
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
|
1898
1916
|
}
|
|
1899
1917
|
const body = await readJson(c);
|
|
1900
1918
|
await upsert(deps2, c.var.user.userId, projectId, agentId, name, body);
|
|
@@ -1906,7 +1924,8 @@ function scheduleRoutes(deps2) {
|
|
|
1906
1924
|
deps2.projectService.requireProjectOwner(c.var.user.userId, projectId);
|
|
1907
1925
|
const name = requireScheduleName(c.req.param("name"));
|
|
1908
1926
|
const removed = await deleteScheduleFile(deps2.config.root, projectId, agentId, name);
|
|
1909
|
-
if (!removed)
|
|
1927
|
+
if (!removed)
|
|
1928
|
+
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
|
1910
1929
|
deps2.scheduler.dropEntry(projectId, agentId, name);
|
|
1911
1930
|
return c.body(null, 204);
|
|
1912
1931
|
});
|
|
@@ -1916,9 +1935,9 @@ async function upsert(deps2, userId, projectId, agentId, name, body) {
|
|
|
1916
1935
|
const fields = parseUpsertBody(body);
|
|
1917
1936
|
const raw = serializeSchedule(fields);
|
|
1918
1937
|
const parsed = parseScheduleFile(name, raw);
|
|
1919
|
-
if (!parsed.ok) throw badRequest(
|
|
1938
|
+
if (!parsed.ok) throw badRequest(`Invalid schedule configuration: ${parsed.error}`);
|
|
1920
1939
|
const refError = await validateScheduleModelRef(deps2.config.root, projectId, parsed.def);
|
|
1921
|
-
if (refError !== null) throw badRequest(
|
|
1940
|
+
if (refError !== null) throw badRequest(`Invalid schedule configuration: ${refError}`);
|
|
1922
1941
|
await writeScheduleFile(deps2.config.root, projectId, agentId, name, raw);
|
|
1923
1942
|
deps2.schedulesRepo.registerOrSync({
|
|
1924
1943
|
projectId,
|
|
@@ -1935,8 +1954,8 @@ async function readItem(deps2, projectId, agentId, name) {
|
|
|
1935
1954
|
const entry = entries.find((e) => e.def.name === name);
|
|
1936
1955
|
if (entry) return toItem(entry.def, entry.state, entry.queued, Date.now());
|
|
1937
1956
|
const bad = invalid.find((i) => i.name === name);
|
|
1938
|
-
if (bad) throw badRequest(
|
|
1939
|
-
throw new HttpError(404, "schedule_not_found",
|
|
1957
|
+
if (bad) throw badRequest(`Invalid schedule file: ${bad.error}`);
|
|
1958
|
+
throw new HttpError(404, "schedule_not_found", `Schedule does not exist: ${name}`);
|
|
1940
1959
|
}
|
|
1941
1960
|
|
|
1942
1961
|
// src/http/routes/benchmarks.ts
|
|
@@ -1987,11 +2006,11 @@ function libraryResponse() {
|
|
|
1987
2006
|
}
|
|
1988
2007
|
function parseInstallNames(body) {
|
|
1989
2008
|
if (!Array.isArray(body.names) || body.names.length === 0) {
|
|
1990
|
-
throw badRequest("names
|
|
2009
|
+
throw badRequest("names must be a non-empty array.");
|
|
1991
2010
|
}
|
|
1992
2011
|
return body.names.map((v, i) => {
|
|
1993
2012
|
if (typeof v !== "string" || v.length === 0) {
|
|
1994
|
-
throw badRequest(`names[${i}]
|
|
2013
|
+
throw badRequest(`names[${i}] must be a non-empty string.`);
|
|
1995
2014
|
}
|
|
1996
2015
|
return v;
|
|
1997
2016
|
});
|
|
@@ -2021,7 +2040,7 @@ function agentSkillsRoutes(deps2) {
|
|
|
2021
2040
|
const names = parseInstallNames(await readJson(c));
|
|
2022
2041
|
const skills = names.map((name) => {
|
|
2023
2042
|
const skill = librarySkill(name);
|
|
2024
|
-
if (!skill) throw new HttpError(404, "unknown_skill", `Skill
|
|
2043
|
+
if (!skill) throw new HttpError(404, "unknown_skill", `Skill is not in the library: ${name}`);
|
|
2025
2044
|
return skill;
|
|
2026
2045
|
});
|
|
2027
2046
|
for (const skill of skills) {
|
|
@@ -2038,7 +2057,7 @@ function agentSkillsRoutes(deps2) {
|
|
|
2038
2057
|
try {
|
|
2039
2058
|
await fs2.access(file);
|
|
2040
2059
|
} catch {
|
|
2041
|
-
throw new HttpError(404, "not_found", `Skill
|
|
2060
|
+
throw new HttpError(404, "not_found", `Skill is not installed: ${name}`);
|
|
2042
2061
|
}
|
|
2043
2062
|
await removeSkill(deps2.config.root, projectId, agentId, name);
|
|
2044
2063
|
return c.body(null, 204);
|
|
@@ -2077,10 +2096,11 @@ function agentTransferRoutes(deps2) {
|
|
|
2077
2096
|
try {
|
|
2078
2097
|
archive = Buffer.from(dataBase64, "base64");
|
|
2079
2098
|
} catch {
|
|
2080
|
-
throw badRequest("dataBase64
|
|
2099
|
+
throw badRequest("dataBase64 is not valid base64.");
|
|
2081
2100
|
}
|
|
2082
|
-
if (archive.byteLength === 0) throw badRequest("
|
|
2083
|
-
if (archive.byteLength > MAX_ARCHIVE_BYTES)
|
|
2101
|
+
if (archive.byteLength === 0) throw badRequest("Import package is empty.");
|
|
2102
|
+
if (archive.byteLength > MAX_ARCHIVE_BYTES)
|
|
2103
|
+
throw badRequest("Import package exceeds the 14MB limit.");
|
|
2084
2104
|
const { version } = await deps2.snapshots.importArchive(projectId, agentId, archive, confirm);
|
|
2085
2105
|
const res = { version };
|
|
2086
2106
|
return c.json(res);
|
|
@@ -2175,17 +2195,21 @@ function dirsRoutes(deps2) {
|
|
|
2175
2195
|
const raw = c.req.query("path");
|
|
2176
2196
|
const target = raw && raw.trim() ? raw.trim() : os.homedir();
|
|
2177
2197
|
if (!path4.isAbsolute(target)) {
|
|
2178
|
-
throw new HttpError(400, "dir_not_absolute", "
|
|
2198
|
+
throw new HttpError(400, "dir_not_absolute", "Directory must be an absolute path.");
|
|
2179
2199
|
}
|
|
2180
2200
|
let real;
|
|
2181
2201
|
try {
|
|
2182
2202
|
real = await fs4.realpath(target);
|
|
2183
2203
|
} catch {
|
|
2184
|
-
throw new HttpError(
|
|
2204
|
+
throw new HttpError(
|
|
2205
|
+
404,
|
|
2206
|
+
"dir_not_found",
|
|
2207
|
+
`Directory does not exist or is inaccessible: ${target}.`
|
|
2208
|
+
);
|
|
2185
2209
|
}
|
|
2186
2210
|
const stat = await fs4.stat(real);
|
|
2187
2211
|
if (!stat.isDirectory()) {
|
|
2188
|
-
throw new HttpError(400, "not_a_dir", "
|
|
2212
|
+
throw new HttpError(400, "not_a_dir", "Not a directory.");
|
|
2189
2213
|
}
|
|
2190
2214
|
let dirents = [];
|
|
2191
2215
|
try {
|
|
@@ -2228,7 +2252,7 @@ function agentConfigRoutes(deps2) {
|
|
|
2228
2252
|
if (agentsMd !== void 0) req.agentsMd = agentsMd;
|
|
2229
2253
|
if (body.config !== void 0) {
|
|
2230
2254
|
if (body.config === null || typeof body.config !== "object" || Array.isArray(body.config)) {
|
|
2231
|
-
throw badRequest("config
|
|
2255
|
+
throw badRequest("config must be an object.");
|
|
2232
2256
|
}
|
|
2233
2257
|
req.config = body.config;
|
|
2234
2258
|
}
|
|
@@ -2284,7 +2308,7 @@ function usageRoutes(deps2) {
|
|
|
2284
2308
|
deps2.projectService.requireProjectAccess(c.var.user.userId, projectId);
|
|
2285
2309
|
const groupByRaw = c.req.query("groupBy") ?? "date";
|
|
2286
2310
|
if (!GROUP_BYS.includes(groupByRaw)) {
|
|
2287
|
-
throw badRequest(`groupBy
|
|
2311
|
+
throw badRequest(`groupBy must be one of ${GROUP_BYS.join(" / ")}.`);
|
|
2288
2312
|
}
|
|
2289
2313
|
const from = optionalDateParam(c.req.query("from"), "from");
|
|
2290
2314
|
const to = optionalDateParam(c.req.query("to"), "to");
|
|
@@ -2326,12 +2350,16 @@ async function assertWorkspaceAllowed(args) {
|
|
|
2326
2350
|
throw new HttpError(
|
|
2327
2351
|
400,
|
|
2328
2352
|
"workspace_not_found",
|
|
2329
|
-
`Workspace
|
|
2353
|
+
`Workspace does not exist or is inaccessible: ${args.workspace}. Specify an existing directory, or leave it empty to use a temporary directory.`
|
|
2330
2354
|
);
|
|
2331
2355
|
}
|
|
2332
2356
|
const stat = await fs5.stat(ws);
|
|
2333
2357
|
if (!stat.isDirectory()) {
|
|
2334
|
-
throw new HttpError(
|
|
2358
|
+
throw new HttpError(
|
|
2359
|
+
400,
|
|
2360
|
+
"workspace_not_found",
|
|
2361
|
+
`Workspace is not a directory: ${args.workspace}.`
|
|
2362
|
+
);
|
|
2335
2363
|
}
|
|
2336
2364
|
return ws;
|
|
2337
2365
|
}
|
|
@@ -2373,7 +2401,7 @@ var WorkspaceFilesService = class {
|
|
|
2373
2401
|
try {
|
|
2374
2402
|
return await fs6.realpath(path5.resolve(workspace));
|
|
2375
2403
|
} catch {
|
|
2376
|
-
throw new HttpError(404, "workspace_missing", "
|
|
2404
|
+
throw new HttpError(404, "workspace_missing", "This Session's Workspace no longer exists.");
|
|
2377
2405
|
}
|
|
2378
2406
|
}
|
|
2379
2407
|
/**
|
|
@@ -2389,16 +2417,16 @@ var WorkspaceFilesService = class {
|
|
|
2389
2417
|
}
|
|
2390
2418
|
/** Lexical prefix check (a relative path, once resolved, must still be inside the Workspace); returns the absolute target path. */
|
|
2391
2419
|
lexicalTarget(base, rel) {
|
|
2392
|
-
if (rel.includes("\0")) throw badRequest("path
|
|
2420
|
+
if (rel.includes("\0")) throw badRequest("path is invalid.");
|
|
2393
2421
|
const target = path5.resolve(base, rel === "" ? "." : rel);
|
|
2394
2422
|
if (!this.isInside(target, base)) {
|
|
2395
|
-
throw badRequest("path
|
|
2423
|
+
throw badRequest("path must be inside the Workspace.");
|
|
2396
2424
|
}
|
|
2397
2425
|
return target;
|
|
2398
2426
|
}
|
|
2399
2427
|
assertInside(real, realBase) {
|
|
2400
2428
|
if (!this.isInside(real, realBase)) {
|
|
2401
|
-
throw badRequest("path
|
|
2429
|
+
throw badRequest("path must be inside the Workspace.");
|
|
2402
2430
|
}
|
|
2403
2431
|
}
|
|
2404
2432
|
/**
|
|
@@ -2416,7 +2444,7 @@ var WorkspaceFilesService = class {
|
|
|
2416
2444
|
canonical = await fs6.realpath(target);
|
|
2417
2445
|
} catch (err) {
|
|
2418
2446
|
if (err.code === "ENOENT") {
|
|
2419
|
-
throw new HttpError(404, "path_not_found", "
|
|
2447
|
+
throw new HttpError(404, "path_not_found", "File does not exist.");
|
|
2420
2448
|
}
|
|
2421
2449
|
throw err;
|
|
2422
2450
|
}
|
|
@@ -2441,7 +2469,7 @@ var WorkspaceFilesService = class {
|
|
|
2441
2469
|
const realBase = await this.realBase(workspace);
|
|
2442
2470
|
const target = this.lexicalTarget(path5.resolve(workspace), rel);
|
|
2443
2471
|
const name = path5.basename(target);
|
|
2444
|
-
if (name === "" || name === "." || name === "..") throw badRequest("path
|
|
2472
|
+
if (name === "" || name === "." || name === "..") throw badRequest("path must be a file path.");
|
|
2445
2473
|
const parent = path5.dirname(target);
|
|
2446
2474
|
let canonicalParent;
|
|
2447
2475
|
try {
|
|
@@ -2456,7 +2484,7 @@ var WorkspaceFilesService = class {
|
|
|
2456
2484
|
} catch (probeErr) {
|
|
2457
2485
|
if (probeErr.code !== "ENOENT") throw probeErr;
|
|
2458
2486
|
const up = path5.dirname(probe);
|
|
2459
|
-
if (up === probe) throw badRequest("path
|
|
2487
|
+
if (up === probe) throw badRequest("path is invalid.");
|
|
2460
2488
|
probe = up;
|
|
2461
2489
|
}
|
|
2462
2490
|
}
|
|
@@ -2496,10 +2524,10 @@ var WorkspaceFilesService = class {
|
|
|
2496
2524
|
dirents = await fs6.readdir(dir, { withFileTypes: true });
|
|
2497
2525
|
} catch (err) {
|
|
2498
2526
|
if (err.code === "ENOENT") {
|
|
2499
|
-
throw new HttpError(404, "path_not_found", "
|
|
2527
|
+
throw new HttpError(404, "path_not_found", "Directory does not exist.");
|
|
2500
2528
|
}
|
|
2501
2529
|
if (err.code === "ENOTDIR") {
|
|
2502
|
-
throw badRequest("path
|
|
2530
|
+
throw badRequest("path is not a directory.");
|
|
2503
2531
|
}
|
|
2504
2532
|
throw err;
|
|
2505
2533
|
}
|
|
@@ -2535,11 +2563,11 @@ var WorkspaceFilesService = class {
|
|
|
2535
2563
|
try {
|
|
2536
2564
|
stat = await fs6.stat(file);
|
|
2537
2565
|
} catch {
|
|
2538
|
-
throw new HttpError(404, "path_not_found", "
|
|
2566
|
+
throw new HttpError(404, "path_not_found", "File does not exist.");
|
|
2539
2567
|
}
|
|
2540
|
-
if (stat.isDirectory()) throw badRequest("path
|
|
2568
|
+
if (stat.isDirectory()) throw badRequest("path is a directory.");
|
|
2541
2569
|
if (stat.size > MAX_READ_BYTES) {
|
|
2542
|
-
throw new HttpError(413, "file_too_large", "
|
|
2570
|
+
throw new HttpError(413, "file_too_large", "File exceeds the 50MB read limit.");
|
|
2543
2571
|
}
|
|
2544
2572
|
const data = await fs6.readFile(file);
|
|
2545
2573
|
const ext = path5.extname(file).toLowerCase();
|
|
@@ -2559,9 +2587,9 @@ var WorkspaceFilesService = class {
|
|
|
2559
2587
|
* sandbox escapes).
|
|
2560
2588
|
*/
|
|
2561
2589
|
async write(workspace, rel, data) {
|
|
2562
|
-
if (rel === "" || rel.endsWith("/")) throw badRequest("path
|
|
2590
|
+
if (rel === "" || rel.endsWith("/")) throw badRequest("path must be a file path.");
|
|
2563
2591
|
if (data.length > MAX_UPLOAD_BYTES) {
|
|
2564
|
-
throw new HttpError(413, "file_too_large", "
|
|
2592
|
+
throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
|
|
2565
2593
|
}
|
|
2566
2594
|
const { dir, name } = await this.resolveWriteParent(workspace, rel);
|
|
2567
2595
|
const file = path5.join(dir, name);
|
|
@@ -2571,9 +2599,10 @@ var WorkspaceFilesService = class {
|
|
|
2571
2599
|
handle = await fs6.open(file, flags, 420);
|
|
2572
2600
|
} catch (err) {
|
|
2573
2601
|
const code = err.code;
|
|
2574
|
-
if (code === "ELOOP") throw badRequest("path
|
|
2575
|
-
if (code === "ENOENT")
|
|
2576
|
-
|
|
2602
|
+
if (code === "ELOOP") throw badRequest("path must not be a symlink.");
|
|
2603
|
+
if (code === "ENOENT")
|
|
2604
|
+
throw new HttpError(404, "path_not_found", "Parent directory does not exist.");
|
|
2605
|
+
if (code === "EISDIR") throw badRequest("path is a directory.");
|
|
2577
2606
|
throw err;
|
|
2578
2607
|
}
|
|
2579
2608
|
try {
|
|
@@ -2597,27 +2626,27 @@ var APPROVAL_MODES = [
|
|
|
2597
2626
|
function parseTaskInput(body) {
|
|
2598
2627
|
const input = body.input;
|
|
2599
2628
|
if (!Array.isArray(input) || input.length === 0) {
|
|
2600
|
-
throw badRequest("input
|
|
2629
|
+
throw badRequest("input must be an array with at least one item.");
|
|
2601
2630
|
}
|
|
2602
2631
|
return input.map((item, i) => {
|
|
2603
2632
|
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
2604
|
-
throw badRequest(`input[${i}]
|
|
2633
|
+
throw badRequest(`input[${i}] must be an object.`);
|
|
2605
2634
|
}
|
|
2606
2635
|
const part = item;
|
|
2607
2636
|
if (part.type === "text") {
|
|
2608
2637
|
if (typeof part.text !== "string" || part.text.length === 0) {
|
|
2609
|
-
throw badRequest(`input[${i}].text
|
|
2638
|
+
throw badRequest(`input[${i}].text must be a non-empty string.`);
|
|
2610
2639
|
}
|
|
2611
2640
|
return userText(part.text);
|
|
2612
2641
|
}
|
|
2613
2642
|
if (part.type === "image_url") {
|
|
2614
2643
|
const url = part.imageUrl;
|
|
2615
2644
|
if (typeof url !== "string" || !(url.startsWith("data:") || url.startsWith("http://") || url.startsWith("https://"))) {
|
|
2616
|
-
throw badRequest(`input[${i}].imageUrl
|
|
2645
|
+
throw badRequest(`input[${i}].imageUrl only supports data: or http(s) URLs.`);
|
|
2617
2646
|
}
|
|
2618
2647
|
return imageUrlMessage(url);
|
|
2619
2648
|
}
|
|
2620
|
-
throw badRequest(`input[${i}].type
|
|
2649
|
+
throw badRequest(`input[${i}].type must be one of text / image_url.`);
|
|
2621
2650
|
});
|
|
2622
2651
|
}
|
|
2623
2652
|
function agentSessionsRoutes(deps2) {
|
|
@@ -2638,8 +2667,10 @@ function agentSessionsRoutes(deps2) {
|
|
|
2638
2667
|
const body = await readJson(c);
|
|
2639
2668
|
const modelId = optionalString(body, "modelId", { minLen: 1, label: "modelId" });
|
|
2640
2669
|
const provider = optionalString(body, "provider", { minLen: 1, label: "provider" });
|
|
2641
|
-
if (
|
|
2642
|
-
throw badRequest(
|
|
2670
|
+
if (modelId === void 0 !== (provider === void 0)) {
|
|
2671
|
+
throw badRequest(
|
|
2672
|
+
"modelId and provider must be given together as a model reference pair: specify both, or neither to use the Project's default model."
|
|
2673
|
+
);
|
|
2643
2674
|
}
|
|
2644
2675
|
const approvalMode = optionalEnum(body, "approvalMode", APPROVAL_MODES);
|
|
2645
2676
|
let workspace = optionalString(body, "workspace", { minLen: 1, label: "workspace" });
|
|
@@ -2664,12 +2695,20 @@ function sessionsRoutes(deps2) {
|
|
|
2664
2695
|
const sessionId = c.req.param("sessionId");
|
|
2665
2696
|
const row = sessionId ? deps2.sessionsRepo.findById(sessionId) : null;
|
|
2666
2697
|
if (!row) {
|
|
2667
|
-
throw new HttpError(
|
|
2698
|
+
throw new HttpError(
|
|
2699
|
+
404,
|
|
2700
|
+
"session_not_found",
|
|
2701
|
+
"Session does not exist or you do not have access."
|
|
2702
|
+
);
|
|
2668
2703
|
}
|
|
2669
2704
|
try {
|
|
2670
2705
|
deps2.projectService.requireProjectAccess(c.var.user.userId, row.projectId);
|
|
2671
2706
|
} catch {
|
|
2672
|
-
throw new HttpError(
|
|
2707
|
+
throw new HttpError(
|
|
2708
|
+
404,
|
|
2709
|
+
"session_not_found",
|
|
2710
|
+
"Session does not exist or you do not have access."
|
|
2711
|
+
);
|
|
2673
2712
|
}
|
|
2674
2713
|
return row;
|
|
2675
2714
|
};
|
|
@@ -2688,15 +2727,23 @@ function sessionsRoutes(deps2) {
|
|
|
2688
2727
|
let title;
|
|
2689
2728
|
if (titleRaw !== void 0) {
|
|
2690
2729
|
if (typeof titleRaw !== "string") {
|
|
2691
|
-
throw new HttpError(400, "invalid_title", "title
|
|
2730
|
+
throw new HttpError(400, "invalid_title", "title must be a string.");
|
|
2692
2731
|
}
|
|
2693
2732
|
title = titleRaw.trim();
|
|
2694
2733
|
if (!title || title.length > SESSION_TITLE_MAX) {
|
|
2695
|
-
throw new HttpError(
|
|
2734
|
+
throw new HttpError(
|
|
2735
|
+
400,
|
|
2736
|
+
"invalid_title",
|
|
2737
|
+
`title must be 1\u2013${SESSION_TITLE_MAX} characters.`
|
|
2738
|
+
);
|
|
2696
2739
|
}
|
|
2697
2740
|
}
|
|
2698
2741
|
if (approvalMode === void 0 && archived === void 0 && title === void 0) {
|
|
2699
|
-
throw new HttpError(
|
|
2742
|
+
throw new HttpError(
|
|
2743
|
+
400,
|
|
2744
|
+
"no_update",
|
|
2745
|
+
"No updatable field provided (approvalMode / archived / title)."
|
|
2746
|
+
);
|
|
2700
2747
|
}
|
|
2701
2748
|
let updated = { ...row };
|
|
2702
2749
|
if (title !== void 0) {
|
|
@@ -2742,7 +2789,7 @@ function sessionsRoutes(deps2) {
|
|
|
2742
2789
|
const row = resolveSession(c);
|
|
2743
2790
|
const fileName = c.req.param("fileName") ?? "";
|
|
2744
2791
|
if (!/^[A-Za-z0-9._-]+$/.test(fileName) || fileName.includes("..")) {
|
|
2745
|
-
throw new HttpError(404, "file_not_found", "
|
|
2792
|
+
throw new HttpError(404, "file_not_found", "File does not exist.");
|
|
2746
2793
|
}
|
|
2747
2794
|
const filePath = path6.join(
|
|
2748
2795
|
scratchpadDir(deps2.config.root, row.projectId, row.agentId),
|
|
@@ -2753,7 +2800,7 @@ function sessionsRoutes(deps2) {
|
|
|
2753
2800
|
try {
|
|
2754
2801
|
bytes = await fs7.readFile(filePath);
|
|
2755
2802
|
} catch {
|
|
2756
|
-
throw new HttpError(404, "file_not_found", "
|
|
2803
|
+
throw new HttpError(404, "file_not_found", "File does not exist.");
|
|
2757
2804
|
}
|
|
2758
2805
|
const MIME_BY_EXT = {
|
|
2759
2806
|
".png": "image/png",
|
|
@@ -2802,7 +2849,11 @@ function sessionsRoutes(deps2) {
|
|
|
2802
2849
|
const decision = requireEnum(body, "decision", ["allow", "deny"]);
|
|
2803
2850
|
const ok = deps2.manager.decideApproval(row.sessionId, pathParam(c, "toolCallId"), decision);
|
|
2804
2851
|
if (!ok) {
|
|
2805
|
-
throw new HttpError(
|
|
2852
|
+
throw new HttpError(
|
|
2853
|
+
404,
|
|
2854
|
+
"approval_not_found",
|
|
2855
|
+
"Approval does not exist or has already been decided."
|
|
2856
|
+
);
|
|
2806
2857
|
}
|
|
2807
2858
|
return c.body(null, 204);
|
|
2808
2859
|
});
|
|
@@ -2825,18 +2876,22 @@ function sessionsRoutes(deps2) {
|
|
|
2825
2876
|
const row = resolveSession(c);
|
|
2826
2877
|
const rel = c.req.query("path") ?? "";
|
|
2827
2878
|
const download = c.req.query("download") === "1";
|
|
2879
|
+
const preview = !download && c.req.query("preview") === "1";
|
|
2828
2880
|
const { data, fileName, contentType, scriptable } = await deps2.workspaceFiles.read(
|
|
2829
2881
|
row.workspace,
|
|
2830
2882
|
rel
|
|
2831
2883
|
);
|
|
2832
2884
|
const disposition = download ? "attachment" : "inline";
|
|
2833
|
-
const effectiveType = !download && scriptable ? "text/plain; charset=utf-8" : contentType;
|
|
2885
|
+
const effectiveType = !download && scriptable && !preview ? "text/plain; charset=utf-8" : contentType;
|
|
2834
2886
|
return new Response(new Uint8Array(data), {
|
|
2835
2887
|
status: 200,
|
|
2836
2888
|
headers: {
|
|
2837
2889
|
"Content-Type": effectiveType,
|
|
2838
2890
|
"Content-Disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
|
2839
|
-
"X-Content-Type-Options": "nosniff"
|
|
2891
|
+
"X-Content-Type-Options": "nosniff",
|
|
2892
|
+
...preview && scriptable ? {
|
|
2893
|
+
"Content-Security-Policy": "sandbox allow-scripts allow-popups allow-modals allow-forms"
|
|
2894
|
+
} : {}
|
|
2840
2895
|
}
|
|
2841
2896
|
});
|
|
2842
2897
|
});
|
|
@@ -2846,7 +2901,7 @@ function sessionsRoutes(deps2) {
|
|
|
2846
2901
|
const paths = body.paths;
|
|
2847
2902
|
if (!Array.isArray(paths) || paths.length > STAT_MAX_PATHS || !paths.every((p) => typeof p === "string" && p.length <= STAT_MAX_PATH_LEN)) {
|
|
2848
2903
|
throw badRequest(
|
|
2849
|
-
`paths \
|
|
2904
|
+
`paths must be an array of strings (\u2264${STAT_MAX_PATHS} items, each \u2264${STAT_MAX_PATH_LEN} characters).`
|
|
2850
2905
|
);
|
|
2851
2906
|
}
|
|
2852
2907
|
const existing = await deps2.workspaceFiles.statExisting(row.workspace, paths);
|
|
@@ -2857,11 +2912,11 @@ function sessionsRoutes(deps2) {
|
|
|
2857
2912
|
const rel = c.req.query("path") ?? "";
|
|
2858
2913
|
const body = await readJson(c);
|
|
2859
2914
|
if (typeof body.dataBase64 !== "string") {
|
|
2860
|
-
throw badRequest("dataBase64
|
|
2915
|
+
throw badRequest("dataBase64 must be a base64 string.");
|
|
2861
2916
|
}
|
|
2862
2917
|
const data = Buffer.from(body.dataBase64, "base64");
|
|
2863
2918
|
if (data.length > MAX_UPLOAD_BYTES) {
|
|
2864
|
-
throw new HttpError(413, "file_too_large", "
|
|
2919
|
+
throw new HttpError(413, "file_too_large", "Uploaded file exceeds the 14MB limit.");
|
|
2865
2920
|
}
|
|
2866
2921
|
await deps2.workspaceFiles.write(row.workspace, rel, data);
|
|
2867
2922
|
return c.body(null, 204);
|
|
@@ -2898,8 +2953,8 @@ function sessionsRoutes(deps2) {
|
|
|
2898
2953
|
|
|
2899
2954
|
// src/runtime/channel.ts
|
|
2900
2955
|
import { randomUUID } from "crypto";
|
|
2901
|
-
var DEFAULT_MAX_COUNT =
|
|
2902
|
-
var DEFAULT_MAX_BYTES =
|
|
2956
|
+
var DEFAULT_MAX_COUNT = 1e4;
|
|
2957
|
+
var DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
2903
2958
|
var Channel = class {
|
|
2904
2959
|
/** Channel epoch: generated at instance creation, prefixed onto event ids (necessarily changes after recycle/recreate or restart). */
|
|
2905
2960
|
epoch = randomUUID().slice(0, 8);
|
|
@@ -3169,12 +3224,16 @@ import path7 from "path";
|
|
|
3169
3224
|
var TOOL_NAMES_MAX = 1e3;
|
|
3170
3225
|
var ORIGIN_CTX_MAX = 200;
|
|
3171
3226
|
var LLM_FAILURES = {
|
|
3172
|
-
failed: { code: "llm_failed", kind: "unexpected", text: "LLM
|
|
3173
|
-
timeout: {
|
|
3227
|
+
failed: { code: "llm_failed", kind: "unexpected", text: "LLM request failed (not retryable)." },
|
|
3228
|
+
timeout: {
|
|
3229
|
+
code: "llm_timeout",
|
|
3230
|
+
kind: "expected",
|
|
3231
|
+
text: "LLM request timed out (the engine reconnects and retries)."
|
|
3232
|
+
},
|
|
3174
3233
|
malformed: {
|
|
3175
3234
|
code: "llm_malformed",
|
|
3176
3235
|
kind: "expected",
|
|
3177
|
-
text: "LLM
|
|
3236
|
+
text: "LLM response could not be parsed (the engine reconnects and retries)."
|
|
3178
3237
|
}
|
|
3179
3238
|
};
|
|
3180
3239
|
function isLlmFailure(s) {
|
|
@@ -3191,7 +3250,7 @@ function originKey(msg) {
|
|
|
3191
3250
|
return origin && origin.length > 0 ? origin[origin.length - 1] : "";
|
|
3192
3251
|
}
|
|
3193
3252
|
function toolFailureText(output) {
|
|
3194
|
-
if (!output) return "
|
|
3253
|
+
if (!output) return "Tool execution failed (no output).";
|
|
3195
3254
|
if (output.length <= MESSAGE_MAX) return output;
|
|
3196
3255
|
return `\u2026${output.slice(output.length - (MESSAGE_MAX - 1))}`;
|
|
3197
3256
|
}
|
|
@@ -3331,9 +3390,9 @@ var StreamErrorWatcher = class {
|
|
|
3331
3390
|
// src/runtime/session-manager.ts
|
|
3332
3391
|
function compactUnavailable(why) {
|
|
3333
3392
|
const messages = {
|
|
3334
|
-
unsupported: "
|
|
3335
|
-
empty: "
|
|
3336
|
-
just_compacted: "
|
|
3393
|
+
unsupported: "This Agent does not have context compaction configured.",
|
|
3394
|
+
empty: "The current context has nothing to compact (no completed conversation turns yet).",
|
|
3395
|
+
just_compacted: "The context was just compacted and there is no new conversation since; no need to compact again."
|
|
3337
3396
|
};
|
|
3338
3397
|
return new HttpError(409, "nothing_to_compact", messages[why]);
|
|
3339
3398
|
}
|
|
@@ -3364,7 +3423,7 @@ function createCoreSessionLoader(root) {
|
|
|
3364
3423
|
throw new HttpError(
|
|
3365
3424
|
409,
|
|
3366
3425
|
"workspace_missing",
|
|
3367
|
-
|
|
3426
|
+
`This Session's Workspace no longer exists: ${row.workspace}, so it cannot continue. Create a new Session.`
|
|
3368
3427
|
);
|
|
3369
3428
|
}
|
|
3370
3429
|
try {
|
|
@@ -3391,6 +3450,7 @@ function toUnrecoverableError(err) {
|
|
|
3391
3450
|
var ENTRY_IDLE_MS = 30 * 60 * 1e3;
|
|
3392
3451
|
var ENTRY_SWEEP_INTERVAL_MS = 60 * 1e3;
|
|
3393
3452
|
var TITLE_EXCERPT_LIMIT = 4e3;
|
|
3453
|
+
var EARLY_TITLE_BODY_CHARS = 1e3;
|
|
3394
3454
|
function agentKey(projectId, agentId) {
|
|
3395
3455
|
return `${projectId}\0${agentId}`;
|
|
3396
3456
|
}
|
|
@@ -3447,6 +3507,8 @@ var SessionManager = class {
|
|
|
3447
3507
|
deletingAgents = /* @__PURE__ */ new Set();
|
|
3448
3508
|
/** Sessions currently being deleted (guards against the entry/Trace file being rebuilt and reviving it inside the deletion race window). */
|
|
3449
3509
|
deletingSessions = /* @__PURE__ */ new Set();
|
|
3510
|
+
/** Per-Agent config generation (key = agentKey), bumped by invalidateAgentRuntimes on vault updates. */
|
|
3511
|
+
agentGenerations = /* @__PURE__ */ new Map();
|
|
3450
3512
|
sweepTimer;
|
|
3451
3513
|
// —— Query surface (used by Session listing / Agent active-count / SSE subscription replay) ——
|
|
3452
3514
|
statusOf(sessionId) {
|
|
@@ -3479,9 +3541,23 @@ var SessionManager = class {
|
|
|
3479
3541
|
approvals: new ApprovalRegistry(),
|
|
3480
3542
|
abort: null,
|
|
3481
3543
|
running: null,
|
|
3544
|
+
generation: this.generationOf(row.projectId, row.agentId),
|
|
3482
3545
|
lastActivityMs: Date.now()
|
|
3483
3546
|
});
|
|
3484
3547
|
}
|
|
3548
|
+
/**
|
|
3549
|
+
* After an Agent's vault is updated: bump the Agent's config generation so every
|
|
3550
|
+
* runtime built before the update is discarded on its next idle access and
|
|
3551
|
+
* re-resumed via the loader — resume re-reads agent_state/.vault.toml, so the next
|
|
3552
|
+
* Task on any of this Agent's Sessions runs with the new values (history is
|
|
3553
|
+
* preserved through the Trace). A Task already in flight is neither aborted nor
|
|
3554
|
+
* hot-swapped: it keeps the values it started with, and its entry is rebuilt on
|
|
3555
|
+
* the first access after it returns to idle (see ensureEntry).
|
|
3556
|
+
*/
|
|
3557
|
+
invalidateAgentRuntimes(projectId, agentId) {
|
|
3558
|
+
const key = agentKey(projectId, agentId);
|
|
3559
|
+
this.agentGenerations.set(key, this.generationOf(projectId, agentId) + 1);
|
|
3560
|
+
}
|
|
3485
3561
|
// —— Task / compaction drive ——
|
|
3486
3562
|
/**
|
|
3487
3563
|
* Start a Task: get-or-load → 409
|
|
@@ -3657,38 +3733,70 @@ var SessionManager = class {
|
|
|
3657
3733
|
// —— Internal ——
|
|
3658
3734
|
assertOpen() {
|
|
3659
3735
|
if (this.closed) {
|
|
3660
|
-
throw new HttpError(
|
|
3736
|
+
throw new HttpError(
|
|
3737
|
+
503,
|
|
3738
|
+
"shutting_down",
|
|
3739
|
+
"Server is shutting down; not accepting new Tasks."
|
|
3740
|
+
);
|
|
3661
3741
|
}
|
|
3662
3742
|
}
|
|
3663
3743
|
/** The Agent owning this Session is being deleted → 409 (guards against directory recreation inside the deletion race window). */
|
|
3664
3744
|
assertAgentNotDeleting(sessionId) {
|
|
3665
3745
|
const row = this.deps.sessions.findById(sessionId);
|
|
3666
3746
|
if (row && this.deletingAgents.has(agentKey(row.projectId, row.agentId))) {
|
|
3667
|
-
throw new HttpError(
|
|
3747
|
+
throw new HttpError(
|
|
3748
|
+
409,
|
|
3749
|
+
"agent_deleting",
|
|
3750
|
+
"This Agent is being deleted; not accepting new Tasks."
|
|
3751
|
+
);
|
|
3668
3752
|
}
|
|
3669
3753
|
}
|
|
3670
3754
|
/** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
|
|
3671
3755
|
assertSessionNotDeleting(sessionId) {
|
|
3672
3756
|
if (this.deletingSessions.has(sessionId)) {
|
|
3673
|
-
throw new HttpError(
|
|
3757
|
+
throw new HttpError(
|
|
3758
|
+
409,
|
|
3759
|
+
"session_deleting",
|
|
3760
|
+
"This Session is being deleted; not accepting new Tasks."
|
|
3761
|
+
);
|
|
3674
3762
|
}
|
|
3675
3763
|
}
|
|
3676
3764
|
assertIdle(entry) {
|
|
3677
3765
|
if (entry.status === "running") {
|
|
3678
|
-
throw new HttpError(409, "task_in_progress", "
|
|
3766
|
+
throw new HttpError(409, "task_in_progress", "This Session already has a Task in progress.");
|
|
3679
3767
|
}
|
|
3680
3768
|
if (entry.status === "compacting") {
|
|
3681
|
-
throw new HttpError(
|
|
3769
|
+
throw new HttpError(
|
|
3770
|
+
409,
|
|
3771
|
+
"compacting",
|
|
3772
|
+
"This Session is compacting its context; not accepting new input."
|
|
3773
|
+
);
|
|
3682
3774
|
}
|
|
3683
3775
|
}
|
|
3776
|
+
generationOf(projectId, agentId) {
|
|
3777
|
+
return this.agentGenerations.get(agentKey(projectId, agentId)) ?? 0;
|
|
3778
|
+
}
|
|
3684
3779
|
/** get-or-resume-or-heal: use directly on an active-table hit; otherwise load via the loader, updating the index's primary key on self-heal. */
|
|
3685
3780
|
async ensureEntry(sessionId) {
|
|
3686
3781
|
const existing = this.entries.get(sessionId);
|
|
3687
|
-
if (existing)
|
|
3782
|
+
if (existing) {
|
|
3783
|
+
if (existing.generation === this.generationOf(existing.projectId, existing.agentId)) {
|
|
3784
|
+
return existing;
|
|
3785
|
+
}
|
|
3786
|
+
if (existing.status !== "idle" || existing.running !== null || existing.approvals.size !== 0) {
|
|
3787
|
+
return existing;
|
|
3788
|
+
}
|
|
3789
|
+
this.entries.delete(sessionId);
|
|
3790
|
+
}
|
|
3688
3791
|
const row = this.deps.sessions.findById(sessionId);
|
|
3689
3792
|
if (!row) {
|
|
3690
|
-
throw new HttpError(
|
|
3793
|
+
throw new HttpError(
|
|
3794
|
+
404,
|
|
3795
|
+
"session_not_found",
|
|
3796
|
+
"Session does not exist or you do not have access."
|
|
3797
|
+
);
|
|
3691
3798
|
}
|
|
3799
|
+
const generation = this.generationOf(row.projectId, row.agentId);
|
|
3692
3800
|
const session = await this.deps.loader.load(row);
|
|
3693
3801
|
this.assertSessionNotDeleting(row.sessionId);
|
|
3694
3802
|
this.assertAgentNotDeleting(row.sessionId);
|
|
@@ -3708,6 +3816,7 @@ var SessionManager = class {
|
|
|
3708
3816
|
approvals: new ApprovalRegistry(),
|
|
3709
3817
|
abort: null,
|
|
3710
3818
|
running: null,
|
|
3819
|
+
generation,
|
|
3711
3820
|
lastActivityMs: Date.now()
|
|
3712
3821
|
};
|
|
3713
3822
|
this.entries.set(currentId, entry);
|
|
@@ -3720,6 +3829,8 @@ var SessionManager = class {
|
|
|
3720
3829
|
* a title): it collects model text for automatic title generation.
|
|
3721
3830
|
*/
|
|
3722
3831
|
async drive(entry, gen, titleSource) {
|
|
3832
|
+
let earlyTitleFired = false;
|
|
3833
|
+
let mainBodyChars = 0;
|
|
3723
3834
|
const ctx = {
|
|
3724
3835
|
projectId: entry.projectId,
|
|
3725
3836
|
agentId: entry.agentId,
|
|
@@ -3755,7 +3866,7 @@ var SessionManager = class {
|
|
|
3755
3866
|
}
|
|
3756
3867
|
} catch (err) {
|
|
3757
3868
|
this.log(
|
|
3758
|
-
`[subagent]
|
|
3869
|
+
`[subagent] Failed to register child session: ${err instanceof Error ? err.message : String(err)}`
|
|
3759
3870
|
);
|
|
3760
3871
|
this.deps.errors?.record({
|
|
3761
3872
|
source: "subagent",
|
|
@@ -3771,18 +3882,30 @@ var SessionManager = class {
|
|
|
3771
3882
|
child.assistantExcerpt += (child.assistantExcerpt ? "\n" : "") + nested.text;
|
|
3772
3883
|
}
|
|
3773
3884
|
}
|
|
3885
|
+
if (!earlyTitleFired && titleSource?.userExcerpt.trim()) {
|
|
3886
|
+
const p = msg.payload;
|
|
3887
|
+
if ((!msg.origin || msg.origin.length === 0) && msg.type === "model_msg" && p.type === "text" && p.role === "assistant" && p.text) {
|
|
3888
|
+
mainBodyChars += p.text.length;
|
|
3889
|
+
if (mainBodyChars >= EARLY_TITLE_BODY_CHARS) {
|
|
3890
|
+
earlyTitleFired = true;
|
|
3891
|
+
this.deps.titles?.maybeGenerate(ctx, entry.session, {
|
|
3892
|
+
fallbackText: titleSource.userExcerpt
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3774
3897
|
this.deps.channels.get(entry.sessionId).publish(msg);
|
|
3775
3898
|
watcher?.observe(msg);
|
|
3776
3899
|
try {
|
|
3777
3900
|
await this.deps.recorder.record(ctx, msg);
|
|
3778
3901
|
} catch (err) {
|
|
3779
|
-
this.log(`[usage]
|
|
3902
|
+
this.log(`[usage] Insert failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3780
3903
|
this.deps.errors?.record({ source: "usage", err, ctx, code: "usage_insert_failed" });
|
|
3781
3904
|
}
|
|
3782
3905
|
}
|
|
3783
3906
|
} catch (err) {
|
|
3784
3907
|
this.log(
|
|
3785
|
-
`[session]
|
|
3908
|
+
`[session] Run failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
|
|
3786
3909
|
);
|
|
3787
3910
|
this.deps.errors?.record({ source: "session", err, ctx, code: "session_run_failed" });
|
|
3788
3911
|
} finally {
|
|
@@ -3913,7 +4036,7 @@ var Scheduler = class {
|
|
|
3913
4036
|
now;
|
|
3914
4037
|
intervalMs;
|
|
3915
4038
|
timer = null;
|
|
3916
|
-
/** key = `${projectId}
|
|
4039
|
+
/** key = `${projectId}\0${agentId}\0${name}` */
|
|
3917
4040
|
pending = /* @__PURE__ */ new Map();
|
|
3918
4041
|
ticking = false;
|
|
3919
4042
|
/** Start: run one reconcile immediately (startup semantics: no backfill), then enter the periodic tick. */
|
|
@@ -4010,7 +4133,7 @@ var Scheduler = class {
|
|
|
4010
4133
|
if (!file.parsed.ok) {
|
|
4011
4134
|
this.deps.errors.record({
|
|
4012
4135
|
source: "schedule",
|
|
4013
|
-
err: new Error(
|
|
4136
|
+
err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${file.parsed.error}`),
|
|
4014
4137
|
code: "schedule_invalid_file",
|
|
4015
4138
|
ctx: { projectId, agentId }
|
|
4016
4139
|
});
|
|
@@ -4021,7 +4144,7 @@ var Scheduler = class {
|
|
|
4021
4144
|
if (refError !== null) {
|
|
4022
4145
|
this.deps.errors.record({
|
|
4023
4146
|
source: "schedule",
|
|
4024
|
-
err: new Error(
|
|
4147
|
+
err: new Error(`Invalid schedule file: ${file.name}.toml \u2014 ${refError}`),
|
|
4025
4148
|
code: "schedule_invalid_file",
|
|
4026
4149
|
ctx: { projectId, agentId }
|
|
4027
4150
|
});
|
|
@@ -4081,7 +4204,9 @@ var Scheduler = class {
|
|
|
4081
4204
|
if (!row || row.projectId !== projectId || row.agentId !== agentId) {
|
|
4082
4205
|
this.deps.errors.record({
|
|
4083
4206
|
source: "schedule",
|
|
4084
|
-
err: new Error(
|
|
4207
|
+
err: new Error(
|
|
4208
|
+
`Schedule ${def.name} is bound to a Session that does not exist: ${def.sessionId}`
|
|
4209
|
+
),
|
|
4085
4210
|
code: "schedule_session_missing",
|
|
4086
4211
|
ctx: { projectId, agentId, sessionId: def.sessionId }
|
|
4087
4212
|
});
|
|
@@ -4179,7 +4304,12 @@ var Scheduler = class {
|
|
|
4179
4304
|
};
|
|
4180
4305
|
|
|
4181
4306
|
// src/runtime/title-generator.ts
|
|
4182
|
-
import {
|
|
4307
|
+
import {
|
|
4308
|
+
emptyTokenCounts,
|
|
4309
|
+
sanitizeTitle,
|
|
4310
|
+
stripConversationMarkers,
|
|
4311
|
+
tokenUsage
|
|
4312
|
+
} from "@prismshadow/penguin-core";
|
|
4183
4313
|
var TitleGenerator = class {
|
|
4184
4314
|
constructor(deps2) {
|
|
4185
4315
|
this.deps = deps2;
|
|
@@ -4195,7 +4325,7 @@ var TitleGenerator = class {
|
|
|
4195
4325
|
if (this.inflight.has(ctx.sessionId)) return;
|
|
4196
4326
|
this.inflight.add(ctx.sessionId);
|
|
4197
4327
|
void this.generate(ctx, session, req).catch((err) => {
|
|
4198
|
-
this.log(`[title]
|
|
4328
|
+
this.log(`[title] Generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4199
4329
|
this.deps.errors?.record({ source: "title", err, ctx, code: "title_failed" });
|
|
4200
4330
|
}).finally(() => {
|
|
4201
4331
|
this.inflight.delete(ctx.sessionId);
|
|
@@ -4212,7 +4342,9 @@ var TitleGenerator = class {
|
|
|
4212
4342
|
try {
|
|
4213
4343
|
await this.deps.recorder.record(ctx, tokenUsage(emptyTokenCounts(), res.usage));
|
|
4214
4344
|
} catch (err) {
|
|
4215
|
-
this.log(
|
|
4345
|
+
this.log(
|
|
4346
|
+
`[title] Usage insert failed: ${err instanceof Error ? err.message : String(err)}`
|
|
4347
|
+
);
|
|
4216
4348
|
this.deps.errors?.record({
|
|
4217
4349
|
source: "title",
|
|
4218
4350
|
err,
|
|
@@ -4222,7 +4354,7 @@ var TitleGenerator = class {
|
|
|
4222
4354
|
}
|
|
4223
4355
|
}
|
|
4224
4356
|
} catch (err) {
|
|
4225
|
-
this.log(`[title]
|
|
4357
|
+
this.log(`[title] Model request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4226
4358
|
this.deps.errors?.record({ source: "title", err, ctx, code: "title_llm_failed" });
|
|
4227
4359
|
}
|
|
4228
4360
|
const finalTitle = title ?? fallbackTitle(req.fallbackText);
|
|
@@ -4237,7 +4369,7 @@ var TitleGenerator = class {
|
|
|
4237
4369
|
}
|
|
4238
4370
|
};
|
|
4239
4371
|
function fallbackTitle(text) {
|
|
4240
|
-
const firstLine = text.split("\n").find((l) => l.trim().length > 0);
|
|
4372
|
+
const firstLine = stripConversationMarkers(text).split("\n").find((l) => l.trim().length > 0);
|
|
4241
4373
|
if (!firstLine) return null;
|
|
4242
4374
|
return sanitizeTitle(firstLine) ?? firstLine.trim().slice(0, 30);
|
|
4243
4375
|
}
|
|
@@ -4317,7 +4449,7 @@ var SEMANTIC_ID_PATTERN = /^[a-z][a-z0-9_]{1,63}$/;
|
|
|
4317
4449
|
var USERNAME_PATTERN = /^[a-z][a-z0-9_]{1,31}$/;
|
|
4318
4450
|
var PROJECT_SUFFIX_PATTERN = /^[a-z0-9_]+$/;
|
|
4319
4451
|
var PROJECT_ID_MAX_LENGTH = 64;
|
|
4320
|
-
var SEMANTIC_ID_RULE = "
|
|
4452
|
+
var SEMANTIC_ID_RULE = "starts with a lowercase letter; lowercase letters, digits and underscores only";
|
|
4321
4453
|
|
|
4322
4454
|
// src/services/admin-service.ts
|
|
4323
4455
|
var AdminService = class {
|
|
@@ -4332,13 +4464,17 @@ var AdminService = class {
|
|
|
4332
4464
|
}
|
|
4333
4465
|
async createUser(userId, password) {
|
|
4334
4466
|
if (!USERNAME_PATTERN.test(userId)) {
|
|
4335
|
-
throw new HttpError(
|
|
4467
|
+
throw new HttpError(
|
|
4468
|
+
400,
|
|
4469
|
+
"invalid_user_id",
|
|
4470
|
+
`Username must be 2\u201332 characters: ${SEMANTIC_ID_RULE}.`
|
|
4471
|
+
);
|
|
4336
4472
|
}
|
|
4337
4473
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
4338
|
-
throw new HttpError(400, "invalid_password", "
|
|
4474
|
+
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
4339
4475
|
}
|
|
4340
4476
|
if (this.deps.users.findById(userId)) {
|
|
4341
|
-
throw new HttpError(409, "user_exists",
|
|
4477
|
+
throw new HttpError(409, "user_exists", `User already exists: ${userId}.`);
|
|
4342
4478
|
}
|
|
4343
4479
|
const user = {
|
|
4344
4480
|
userId,
|
|
@@ -4359,10 +4495,10 @@ var AdminService = class {
|
|
|
4359
4495
|
/** Reset another user's password: flags it as initial and clears all their sessions (prompts a password change on next login). */
|
|
4360
4496
|
async resetPassword(userId, password) {
|
|
4361
4497
|
if (!this.deps.users.findById(userId)) {
|
|
4362
|
-
throw new HttpError(404, "user_not_found",
|
|
4498
|
+
throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
|
|
4363
4499
|
}
|
|
4364
4500
|
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
4365
|
-
throw new HttpError(400, "invalid_password", "
|
|
4501
|
+
throw new HttpError(400, "invalid_password", "Password must be at least 8 characters.");
|
|
4366
4502
|
}
|
|
4367
4503
|
this.deps.users.updatePassword(userId, await hashPassword(password), true);
|
|
4368
4504
|
this.deps.authSessions.deleteByUser(userId);
|
|
@@ -4371,10 +4507,10 @@ var AdminService = class {
|
|
|
4371
4507
|
async deleteUser(userId) {
|
|
4372
4508
|
const target = this.deps.users.findById(userId);
|
|
4373
4509
|
if (!target) {
|
|
4374
|
-
throw new HttpError(404, "user_not_found",
|
|
4510
|
+
throw new HttpError(404, "user_not_found", `User does not exist: ${userId}.`);
|
|
4375
4511
|
}
|
|
4376
4512
|
if (target.isAdmin) {
|
|
4377
|
-
throw new HttpError(409, "cannot_delete_admin", "
|
|
4513
|
+
throw new HttpError(409, "cannot_delete_admin", "The built-in admin cannot be deleted.");
|
|
4378
4514
|
}
|
|
4379
4515
|
for (const project of this.deps.projects.listByOwner(userId)) {
|
|
4380
4516
|
await this.deps.projectService.destroyProject(project.projectId);
|
|
@@ -4441,6 +4577,8 @@ function refKey(provider, modelId) {
|
|
|
4441
4577
|
function showRef(provider, modelId) {
|
|
4442
4578
|
return `(provider=${provider}, model_id=${modelId})`;
|
|
4443
4579
|
}
|
|
4580
|
+
var PROBE_PROMPT = 'ping - reply with the single word "pong" and nothing else. Do not think or explain.\n<think></think>';
|
|
4581
|
+
var SPEED_PROBE_PROMPT = "Count from 1 to 50 as a comma-separated list, and nothing else. Do not think or explain.\n<think></think>";
|
|
4444
4582
|
var ProjectConfigService = class {
|
|
4445
4583
|
constructor(root) {
|
|
4446
4584
|
this.root = root;
|
|
@@ -4537,12 +4675,19 @@ var ProjectConfigService = class {
|
|
|
4537
4675
|
* as a pair in the request body; sends one minimal request using that model's
|
|
4538
4676
|
* config (optionally overridden with an unsaved apiKey / baseUrl) — no tools, no
|
|
4539
4677
|
* system prompt, thinking disabled, a tiny output cap, 20s timeout — just to see
|
|
4540
|
-
* whether
|
|
4678
|
+
* whether the endpoint answers. The model id sent to AgentHub is `modelId`
|
|
4541
4679
|
* itself (the upstream id verbatim; client_type inference follows it).
|
|
4542
4680
|
*
|
|
4681
|
+
* A reasoning-heavy model may ignore the disabled thinking level and burn the
|
|
4682
|
+
* whole tiny output cap on thinking (finish_reason=length with no text — AgentHub
|
|
4683
|
+
* raises EmptyResponseError, collapsed to a malformed outcome): the endpoint
|
|
4684
|
+
* demonstrably streamed model output, which is everything a connectivity test
|
|
4685
|
+
* proves, so that case counts as ok too (see probeVerdict).
|
|
4686
|
+
*
|
|
4543
4687
|
* Never throws: the LLM layer collapses auth/parameter/network errors into an
|
|
4544
4688
|
* `LLMOutcome`, which is translated here into ok / message. Consumes very few
|
|
4545
|
-
* Tokens (single-digit output
|
|
4689
|
+
* Tokens (single-digit output; speed mode spends up to its 64-token cap to have a
|
|
4690
|
+
* window worth timing), and writes no Trace and records no usage.
|
|
4546
4691
|
*/
|
|
4547
4692
|
async testModel(projectId, req) {
|
|
4548
4693
|
const raw = await this.readRaw(projectId);
|
|
@@ -4561,18 +4706,39 @@ var ProjectConfigService = class {
|
|
|
4561
4706
|
...clientType ? { clientType } : {},
|
|
4562
4707
|
tools: [],
|
|
4563
4708
|
thinkingLevel: "none",
|
|
4564
|
-
|
|
4709
|
+
// Speed mode pairs a raised cap with a prompt that keeps generating (see
|
|
4710
|
+
// SPEED_PROBE_PROMPT), so the stream lasts long enough for TTFT/TPS to describe
|
|
4711
|
+
// decoding rather than one round trip; the plain connectivity test keeps the
|
|
4712
|
+
// single-digit-token budget.
|
|
4713
|
+
maxTokens: req.speed ? 64 : 16,
|
|
4565
4714
|
requestTimeoutMs: 2e4
|
|
4566
4715
|
});
|
|
4567
|
-
const gen = llm.streamGenerate({
|
|
4716
|
+
const gen = llm.streamGenerate({
|
|
4717
|
+
newMessages: [userText3(req.speed ? SPEED_PROBE_PROMPT : PROBE_PROMPT)]
|
|
4718
|
+
});
|
|
4719
|
+
let sawContent = false;
|
|
4720
|
+
let firstContentAt = null;
|
|
4721
|
+
let outputTokens = 0;
|
|
4568
4722
|
for (; ; ) {
|
|
4569
4723
|
const step = await gen.next();
|
|
4570
4724
|
if (step.done) {
|
|
4571
|
-
const
|
|
4572
|
-
if (
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4725
|
+
const verdict = probeVerdict(step.value, sawContent);
|
|
4726
|
+
if (!verdict.ok) return verdict;
|
|
4727
|
+
const res = { ok: true, latencyMs: Date.now() - startedAt };
|
|
4728
|
+
if (firstContentAt !== null) {
|
|
4729
|
+
res.ttftMs = firstContentAt - startedAt;
|
|
4730
|
+
const tps = probeTps(outputTokens, Date.now() - firstContentAt);
|
|
4731
|
+
if (tps !== void 0) res.tps = tps;
|
|
4732
|
+
}
|
|
4733
|
+
return res;
|
|
4734
|
+
}
|
|
4735
|
+
if (isProbeContent(step.value)) {
|
|
4736
|
+
sawContent = true;
|
|
4737
|
+
if (firstContentAt === null) firstContentAt = Date.now();
|
|
4738
|
+
}
|
|
4739
|
+
const p = step.value.payload;
|
|
4740
|
+
if (p.type === "token_usage" && typeof p.request?.output === "number") {
|
|
4741
|
+
outputTokens = p.request.output;
|
|
4576
4742
|
}
|
|
4577
4743
|
}
|
|
4578
4744
|
} catch (err) {
|
|
@@ -4662,7 +4828,7 @@ var ProjectConfigService = class {
|
|
|
4662
4828
|
const key = refKey(entry.provider, entry.modelId);
|
|
4663
4829
|
if (seen.has(key)) {
|
|
4664
4830
|
throw badRequest(
|
|
4665
|
-
`models
|
|
4831
|
+
`models contains a duplicate model reference: ${showRef(entry.provider, entry.modelId)}.`
|
|
4666
4832
|
);
|
|
4667
4833
|
}
|
|
4668
4834
|
seen.add(key);
|
|
@@ -4712,7 +4878,7 @@ var ProjectConfigService = class {
|
|
|
4712
4878
|
if (req.defaultModel !== void 0) {
|
|
4713
4879
|
if (!seen.has(refKey(req.defaultModel.provider, req.defaultModel.modelId))) {
|
|
4714
4880
|
throw badRequest(
|
|
4715
|
-
`defaultModel
|
|
4881
|
+
`defaultModel must be included in models: ${showRef(req.defaultModel.provider, req.defaultModel.modelId)}.`
|
|
4716
4882
|
);
|
|
4717
4883
|
}
|
|
4718
4884
|
defaultModel = req.defaultModel;
|
|
@@ -4732,12 +4898,12 @@ var ProjectConfigService = class {
|
|
|
4732
4898
|
if (req.visionModel !== void 0) {
|
|
4733
4899
|
if (!seen.has(refKey(req.visionModel.provider, req.visionModel.modelId))) {
|
|
4734
4900
|
throw badRequest(
|
|
4735
|
-
`visionModel
|
|
4901
|
+
`visionModel must be included in models: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
|
|
4736
4902
|
);
|
|
4737
4903
|
}
|
|
4738
4904
|
if (targetOf(req.visionModel)?.vision === false) {
|
|
4739
4905
|
throw badRequest(
|
|
4740
|
-
`visionModel
|
|
4906
|
+
`visionModel must not point to a model annotated as not supporting images: ${showRef(req.visionModel.provider, req.visionModel.modelId)}.`
|
|
4741
4907
|
);
|
|
4742
4908
|
}
|
|
4743
4909
|
visionModel = req.visionModel;
|
|
@@ -4767,6 +4933,24 @@ var ProjectConfigService = class {
|
|
|
4767
4933
|
return this.getModels(projectId);
|
|
4768
4934
|
}
|
|
4769
4935
|
};
|
|
4936
|
+
function isProbeContent(msg) {
|
|
4937
|
+
const p = msg.payload;
|
|
4938
|
+
if (p.type === "partial_thinking" || p.type === "thinking") return Boolean(p.thinking);
|
|
4939
|
+
if (p.type === "partial_text" || p.type === "text") return Boolean(p.text);
|
|
4940
|
+
return false;
|
|
4941
|
+
}
|
|
4942
|
+
function probeVerdict(outcome, sawContent) {
|
|
4943
|
+
if (outcome.status === "completed") return { ok: true };
|
|
4944
|
+
if (outcome.status === "malformed" && sawContent) return { ok: true };
|
|
4945
|
+
const detail = "message" in outcome && outcome.message ? outcome.message : outcome.status;
|
|
4946
|
+
return { ok: false, message: String(detail).slice(0, 300) };
|
|
4947
|
+
}
|
|
4948
|
+
var PROBE_TPS_MIN_TOKENS = 16;
|
|
4949
|
+
var PROBE_TPS_MIN_WINDOW_MS = 100;
|
|
4950
|
+
function probeTps(outputTokens, windowMs) {
|
|
4951
|
+
if (outputTokens < PROBE_TPS_MIN_TOKENS || windowMs <= PROBE_TPS_MIN_WINDOW_MS) return void 0;
|
|
4952
|
+
return Math.round(outputTokens / (windowMs / 1e3) * 10) / 10;
|
|
4953
|
+
}
|
|
4770
4954
|
|
|
4771
4955
|
// src/services/agent-config-service.ts
|
|
4772
4956
|
var THINKING_LEVELS = ["none", "low", "medium", "high", "xhigh"];
|
|
@@ -4790,7 +4974,7 @@ var AgentConfigService = class {
|
|
|
4790
4974
|
}
|
|
4791
4975
|
async requireExists(projectId, agentId) {
|
|
4792
4976
|
if (!await this.exists(projectId, agentId)) {
|
|
4793
|
-
throw new HttpError(404, "agent_not_found", "Agent
|
|
4977
|
+
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
|
|
4794
4978
|
}
|
|
4795
4979
|
}
|
|
4796
4980
|
/**
|
|
@@ -4875,8 +5059,8 @@ var AgentConfigService = class {
|
|
|
4875
5059
|
const cfg = config2;
|
|
4876
5060
|
const yamlPath = systemConfigPath(this.root, projectId, agentId);
|
|
4877
5061
|
const doc = parseDocument(await fs10.readFile(yamlPath, "utf8"));
|
|
4878
|
-
const setIfProvided = (
|
|
4879
|
-
if (value !== void 0) doc.setIn(
|
|
5062
|
+
const setIfProvided = (path17, value) => {
|
|
5063
|
+
if (value !== void 0) doc.setIn(path17, value);
|
|
4880
5064
|
};
|
|
4881
5065
|
setIfProvided(["name"], optionalString(cfg, "name", { maxLen: 100, label: "name" }));
|
|
4882
5066
|
setIfProvided(
|
|
@@ -4952,23 +5136,25 @@ var AgentConfigService = class {
|
|
|
4952
5136
|
for (const entry of req.entries) {
|
|
4953
5137
|
if (!isValidVaultKey(entry.key)) {
|
|
4954
5138
|
throw badRequest(
|
|
4955
|
-
`vault
|
|
5139
|
+
`Invalid vault key name: ${entry.key} (letters, digits and underscores only, and must not start with a digit).`
|
|
4956
5140
|
);
|
|
4957
5141
|
}
|
|
4958
5142
|
if (seen.has(entry.key)) {
|
|
4959
|
-
throw badRequest(`entries
|
|
5143
|
+
throw badRequest(`entries contains a duplicate key: ${entry.key}.`);
|
|
4960
5144
|
}
|
|
4961
5145
|
seen.add(entry.key);
|
|
4962
5146
|
const prevValue = prev[entry.key];
|
|
4963
5147
|
if (entry.value !== void 0) {
|
|
4964
5148
|
if (entry.value.length > VAULT_VALUE_MAX_LENGTH) {
|
|
4965
|
-
throw badRequest(
|
|
5149
|
+
throw badRequest(
|
|
5150
|
+
`Vault value too long: ${entry.key} (limit ${VAULT_VALUE_MAX_LENGTH} characters).`
|
|
5151
|
+
);
|
|
4966
5152
|
}
|
|
4967
5153
|
nextVault[entry.key] = entry.value;
|
|
4968
5154
|
} else if (prevValue !== void 0) {
|
|
4969
5155
|
nextVault[entry.key] = prevValue;
|
|
4970
5156
|
} else {
|
|
4971
|
-
throw badRequest(
|
|
5157
|
+
throw badRequest(`New key ${entry.key} must provide a value.`);
|
|
4972
5158
|
}
|
|
4973
5159
|
}
|
|
4974
5160
|
await saveAgentVault(this.root, projectId, agentId, nextVault);
|
|
@@ -4976,20 +5162,20 @@ var AgentConfigService = class {
|
|
|
4976
5162
|
}
|
|
4977
5163
|
};
|
|
4978
5164
|
function validateToolsBuiltin(value) {
|
|
4979
|
-
if (!Array.isArray(value)) throw badRequest("toolsBuiltin
|
|
5165
|
+
if (!Array.isArray(value)) throw badRequest("toolsBuiltin must be an array.");
|
|
4980
5166
|
return value.map((item, i) => {
|
|
4981
5167
|
const t = asRecord(item);
|
|
4982
5168
|
if (typeof t.name !== "string" || t.name.length === 0) {
|
|
4983
|
-
throw badRequest(`toolsBuiltin[${i}].name
|
|
5169
|
+
throw badRequest(`toolsBuiltin[${i}].name must be a non-empty string.`);
|
|
4984
5170
|
}
|
|
4985
5171
|
if (typeof t.description !== "string") {
|
|
4986
|
-
throw badRequest(`toolsBuiltin[${i}].description
|
|
5172
|
+
throw badRequest(`toolsBuiltin[${i}].description must be a string.`);
|
|
4987
5173
|
}
|
|
4988
5174
|
if (t.permission !== void 0 && t.permission !== "r" && t.permission !== "rw") {
|
|
4989
|
-
throw badRequest(`toolsBuiltin[${i}].permission
|
|
5175
|
+
throw badRequest(`toolsBuiltin[${i}].permission must be one of r / rw.`);
|
|
4990
5176
|
}
|
|
4991
5177
|
if (t.forModel !== void 0 && t.forModel !== "vision" && t.forModel !== "text-only") {
|
|
4992
|
-
throw badRequest(`toolsBuiltin[${i}].forModel
|
|
5178
|
+
throw badRequest(`toolsBuiltin[${i}].forModel must be one of vision / text-only.`);
|
|
4993
5179
|
}
|
|
4994
5180
|
optionalNumber(t, "timeoutMs", {
|
|
4995
5181
|
integer: true,
|
|
@@ -5005,14 +5191,14 @@ function validateToolsBuiltin(value) {
|
|
|
5005
5191
|
});
|
|
5006
5192
|
}
|
|
5007
5193
|
function validateMcpServers(value) {
|
|
5008
|
-
if (!Array.isArray(value)) throw badRequest("mcpServers
|
|
5194
|
+
if (!Array.isArray(value)) throw badRequest("mcpServers must be an array.");
|
|
5009
5195
|
return value.map((item, i) => {
|
|
5010
5196
|
const s = asRecord(item);
|
|
5011
5197
|
if (typeof s.name !== "string" || s.name.length === 0) {
|
|
5012
|
-
throw badRequest(`mcpServers[${i}].name
|
|
5198
|
+
throw badRequest(`mcpServers[${i}].name must be a non-empty string.`);
|
|
5013
5199
|
}
|
|
5014
5200
|
if (s.config === null || typeof s.config !== "object" || Array.isArray(s.config)) {
|
|
5015
|
-
throw badRequest(`mcpServers[${i}].config
|
|
5201
|
+
throw badRequest(`mcpServers[${i}].config must be an object.`);
|
|
5016
5202
|
}
|
|
5017
5203
|
return s;
|
|
5018
5204
|
});
|
|
@@ -5020,6 +5206,7 @@ function validateMcpServers(value) {
|
|
|
5020
5206
|
|
|
5021
5207
|
// src/services/agent-service.ts
|
|
5022
5208
|
import fs11 from "fs/promises";
|
|
5209
|
+
import path10 from "path";
|
|
5023
5210
|
import {
|
|
5024
5211
|
agentDir,
|
|
5025
5212
|
agentsDir as agentsDir2,
|
|
@@ -5029,6 +5216,7 @@ import {
|
|
|
5029
5216
|
isValidId as isValidId3,
|
|
5030
5217
|
loadAgentVault as loadAgentVault2,
|
|
5031
5218
|
scheduleDir as scheduleDir2,
|
|
5219
|
+
skillsDir as skillsDir2,
|
|
5032
5220
|
systemConfigPath as systemConfigPath2
|
|
5033
5221
|
} from "@prismshadow/penguin-core";
|
|
5034
5222
|
var AgentService = class {
|
|
@@ -5068,11 +5256,12 @@ var AgentService = class {
|
|
|
5068
5256
|
);
|
|
5069
5257
|
return Promise.all(
|
|
5070
5258
|
sorted.map(async (row) => {
|
|
5071
|
-
const [meta, updatedAt, vaultKeyCount, scheduleCount] = await Promise.all([
|
|
5259
|
+
const [meta, updatedAt, vaultKeyCount, scheduleCount, skillCount] = await Promise.all([
|
|
5072
5260
|
this.agentConfig.readCardMeta(projectId, row.agentId),
|
|
5073
5261
|
this.configUpdatedAt(projectId, row.agentId),
|
|
5074
5262
|
this.vaultKeyCount(projectId, row.agentId),
|
|
5075
|
-
this.scheduleCount(projectId, row.agentId)
|
|
5263
|
+
this.scheduleCount(projectId, row.agentId),
|
|
5264
|
+
this.skillCount(projectId, row.agentId)
|
|
5076
5265
|
]);
|
|
5077
5266
|
return {
|
|
5078
5267
|
agentId: row.agentId,
|
|
@@ -5080,7 +5269,8 @@ var AgentService = class {
|
|
|
5080
5269
|
createdAt: row.createdAt,
|
|
5081
5270
|
...updatedAt !== void 0 ? { updatedAt } : {},
|
|
5082
5271
|
vaultKeyCount,
|
|
5083
|
-
scheduleCount
|
|
5272
|
+
scheduleCount,
|
|
5273
|
+
skillCount
|
|
5084
5274
|
};
|
|
5085
5275
|
})
|
|
5086
5276
|
);
|
|
@@ -5102,6 +5292,27 @@ var AgentService = class {
|
|
|
5102
5292
|
return 0;
|
|
5103
5293
|
}
|
|
5104
5294
|
}
|
|
5295
|
+
/** Number of installed Skills: count of skills/<name>/ directories containing a SKILL.md (0 if the directory doesn't exist). */
|
|
5296
|
+
async skillCount(projectId, agentId) {
|
|
5297
|
+
const base = skillsDir2(this.root, projectId, agentId);
|
|
5298
|
+
let dirents;
|
|
5299
|
+
try {
|
|
5300
|
+
dirents = await fs11.readdir(base, { withFileTypes: true });
|
|
5301
|
+
} catch {
|
|
5302
|
+
return 0;
|
|
5303
|
+
}
|
|
5304
|
+
const present = await Promise.all(
|
|
5305
|
+
dirents.filter((d) => d.isDirectory()).map(async (d) => {
|
|
5306
|
+
try {
|
|
5307
|
+
await fs11.access(path10.join(base, d.name, "SKILL.md"));
|
|
5308
|
+
return true;
|
|
5309
|
+
} catch {
|
|
5310
|
+
return false;
|
|
5311
|
+
}
|
|
5312
|
+
})
|
|
5313
|
+
);
|
|
5314
|
+
return present.filter(Boolean).length;
|
|
5315
|
+
}
|
|
5105
5316
|
/** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
|
|
5106
5317
|
async configUpdatedAt(projectId, agentId) {
|
|
5107
5318
|
const paths = [
|
|
@@ -5133,7 +5344,7 @@ var AgentService = class {
|
|
|
5133
5344
|
throw new HttpError(
|
|
5134
5345
|
409,
|
|
5135
5346
|
"cannot_delete_builtin_agent",
|
|
5136
|
-
"
|
|
5347
|
+
"Built-in Agents (default_agent) are provisioned with the Project and cannot be deleted from the web."
|
|
5137
5348
|
);
|
|
5138
5349
|
}
|
|
5139
5350
|
await fs11.rm(agentDir(this.root, projectId, agentId), { recursive: true, force: true });
|
|
@@ -5147,14 +5358,18 @@ var AgentService = class {
|
|
|
5147
5358
|
*/
|
|
5148
5359
|
async createAgent(projectId, agentId, name, description) {
|
|
5149
5360
|
if (!SEMANTIC_ID_PATTERN.test(agentId)) {
|
|
5150
|
-
throw new HttpError(
|
|
5361
|
+
throw new HttpError(
|
|
5362
|
+
400,
|
|
5363
|
+
"invalid_agent_id",
|
|
5364
|
+
`Agent id must be 2\u201364 characters: ${SEMANTIC_ID_RULE}.`
|
|
5365
|
+
);
|
|
5151
5366
|
}
|
|
5152
5367
|
const taken = this.agents.exists(projectId, agentId) || await fs11.stat(agentDir(this.root, projectId, agentId)).then(
|
|
5153
5368
|
() => true,
|
|
5154
5369
|
() => false
|
|
5155
5370
|
);
|
|
5156
5371
|
if (taken) {
|
|
5157
|
-
throw new HttpError(409, "agent_exists", `Agent id
|
|
5372
|
+
throw new HttpError(409, "agent_exists", `Agent id is already taken: ${agentId}.`);
|
|
5158
5373
|
}
|
|
5159
5374
|
const displayName = name ?? agentId;
|
|
5160
5375
|
await coreCreateAgent({ root: this.root, projectId, agentId });
|
|
@@ -5179,14 +5394,16 @@ var AgentService = class {
|
|
|
5179
5394
|
toolCount: meta.toolCount,
|
|
5180
5395
|
version: meta.version,
|
|
5181
5396
|
vaultKeyCount: 0,
|
|
5182
|
-
scheduleCount: 0
|
|
5397
|
+
scheduleCount: 0,
|
|
5398
|
+
// Read the real count: coreCreateAgent seeds the default skill set for default_agent.
|
|
5399
|
+
skillCount: await this.skillCount(projectId, agentId)
|
|
5183
5400
|
};
|
|
5184
5401
|
}
|
|
5185
5402
|
};
|
|
5186
5403
|
|
|
5187
5404
|
// src/services/benchmark-service.ts
|
|
5188
5405
|
import fs12 from "fs/promises";
|
|
5189
|
-
import
|
|
5406
|
+
import path11 from "path";
|
|
5190
5407
|
import { parse as parseToml3 } from "smol-toml";
|
|
5191
5408
|
import { parse as parseYaml2 } from "yaml";
|
|
5192
5409
|
import { benchmarksDir } from "@prismshadow/penguin-core";
|
|
@@ -5288,7 +5505,7 @@ var BenchmarkService = class {
|
|
|
5288
5505
|
}
|
|
5289
5506
|
const benchmarks = [];
|
|
5290
5507
|
for (const item of items.filter((i) => i.isDir).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5291
|
-
benchmarks.push(await this.readBenchmark(
|
|
5508
|
+
benchmarks.push(await this.readBenchmark(path11.join(dir, item.name), item.name));
|
|
5292
5509
|
}
|
|
5293
5510
|
return { benchmarks };
|
|
5294
5511
|
}
|
|
@@ -5298,7 +5515,7 @@ var BenchmarkService = class {
|
|
|
5298
5515
|
let runs;
|
|
5299
5516
|
try {
|
|
5300
5517
|
const config2 = asRecord2(
|
|
5301
|
-
parseToml3(await fs12.readFile(
|
|
5518
|
+
parseToml3(await fs12.readFile(path11.join(benchDir, "benchmark_config.toml"), "utf8"))
|
|
5302
5519
|
);
|
|
5303
5520
|
if (typeof config2.title === "string" && config2.title !== "") title = config2.title;
|
|
5304
5521
|
if (typeof config2.description === "string" && config2.description !== "") {
|
|
@@ -5313,7 +5530,7 @@ var BenchmarkService = class {
|
|
|
5313
5530
|
let evaluations = [];
|
|
5314
5531
|
try {
|
|
5315
5532
|
const scoreboard = asRecord2(
|
|
5316
|
-
parseYaml2(await fs12.readFile(
|
|
5533
|
+
parseYaml2(await fs12.readFile(path11.join(benchDir, "scoreboard.yaml"), "utf8"))
|
|
5317
5534
|
);
|
|
5318
5535
|
if (Array.isArray(scoreboard.evaluations)) {
|
|
5319
5536
|
evaluations = scoreboard.evaluations.map(toEvaluation).filter((e) => e !== null);
|
|
@@ -5340,7 +5557,7 @@ var BenchmarkService = class {
|
|
|
5340
5557
|
// src/services/snapshot-service.ts
|
|
5341
5558
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
5342
5559
|
import fs13 from "fs/promises";
|
|
5343
|
-
import
|
|
5560
|
+
import path12 from "path";
|
|
5344
5561
|
import * as tar from "tar";
|
|
5345
5562
|
import { parse as parseYaml3 } from "yaml";
|
|
5346
5563
|
import {
|
|
@@ -5353,7 +5570,7 @@ import {
|
|
|
5353
5570
|
} from "@prismshadow/penguin-core";
|
|
5354
5571
|
var VAULT_BASENAME = ".vault.toml";
|
|
5355
5572
|
function isVaultEntry(entryPath) {
|
|
5356
|
-
return
|
|
5573
|
+
return path12.posix.basename(entryPath.replaceAll("\\", "/")) === VAULT_BASENAME;
|
|
5357
5574
|
}
|
|
5358
5575
|
var SnapshotService = class {
|
|
5359
5576
|
constructor(root) {
|
|
@@ -5366,7 +5583,7 @@ var SnapshotService = class {
|
|
|
5366
5583
|
try {
|
|
5367
5584
|
raw = await fs13.readFile(systemConfigPath3(this.root, projectId, agentId), "utf8");
|
|
5368
5585
|
} catch {
|
|
5369
|
-
throw new HttpError(404, "agent_not_found", "Agent
|
|
5586
|
+
throw new HttpError(404, "agent_not_found", "Agent does not exist.");
|
|
5370
5587
|
}
|
|
5371
5588
|
const parsed = parseYaml3(raw);
|
|
5372
5589
|
return agentStateVersion2({
|
|
@@ -5377,7 +5594,7 @@ var SnapshotService = class {
|
|
|
5377
5594
|
async ensureSnapshot(projectId, agentId) {
|
|
5378
5595
|
const version = await this.currentVersion(projectId, agentId);
|
|
5379
5596
|
const dir = snapshotsDir(this.root, projectId, agentId);
|
|
5380
|
-
const file =
|
|
5597
|
+
const file = path12.join(dir, `v${version}.tar.gz`);
|
|
5381
5598
|
try {
|
|
5382
5599
|
await fs13.access(file);
|
|
5383
5600
|
return { version, file };
|
|
@@ -5412,12 +5629,12 @@ var SnapshotService = class {
|
|
|
5412
5629
|
async importArchive(projectId, agentId, archive, confirm) {
|
|
5413
5630
|
const current = await this.currentVersion(projectId, agentId);
|
|
5414
5631
|
const base = agentDir2(this.root, projectId, agentId);
|
|
5415
|
-
const staging =
|
|
5632
|
+
const staging = path12.join(base, `.import-${randomBytes4(6).toString("hex")}`);
|
|
5416
5633
|
await fs13.mkdir(staging, { recursive: true });
|
|
5417
5634
|
try {
|
|
5418
|
-
const archiveFile =
|
|
5635
|
+
const archiveFile = path12.join(staging, "archive.tar.gz");
|
|
5419
5636
|
await fs13.writeFile(archiveFile, archive);
|
|
5420
|
-
const extractDir =
|
|
5637
|
+
const extractDir = path12.join(staging, "extracted");
|
|
5421
5638
|
await fs13.mkdir(extractDir, { recursive: true });
|
|
5422
5639
|
try {
|
|
5423
5640
|
await tar.extract({
|
|
@@ -5426,21 +5643,21 @@ var SnapshotService = class {
|
|
|
5426
5643
|
filter: (p) => !isVaultEntry(p)
|
|
5427
5644
|
});
|
|
5428
5645
|
} catch {
|
|
5429
|
-
throw badRequest("
|
|
5646
|
+
throw badRequest("Import failed: not a valid tar.gz snapshot package.");
|
|
5430
5647
|
}
|
|
5431
|
-
const configPath =
|
|
5648
|
+
const configPath = path12.join(extractDir, "agent_state", "system_config.yaml");
|
|
5432
5649
|
let parsed;
|
|
5433
5650
|
try {
|
|
5434
5651
|
parsed = parseYaml3(await fs13.readFile(configPath, "utf8"));
|
|
5435
5652
|
} catch {
|
|
5436
|
-
throw badRequest("
|
|
5653
|
+
throw badRequest("Import failed: the package is missing agent_state/system_config.yaml.");
|
|
5437
5654
|
}
|
|
5438
5655
|
if (parsed === null || typeof parsed !== "object" || typeof parsed.system_prompt !== "string") {
|
|
5439
|
-
throw badRequest("
|
|
5656
|
+
throw badRequest("Import failed: the package's system_config.yaml is invalid.");
|
|
5440
5657
|
}
|
|
5441
5658
|
const incomingRaw = parsed.version;
|
|
5442
5659
|
if (incomingRaw !== void 0 && (!Number.isInteger(incomingRaw) || incomingRaw < 1)) {
|
|
5443
|
-
throw badRequest("
|
|
5660
|
+
throw badRequest("Import failed: the package's version is invalid.");
|
|
5444
5661
|
}
|
|
5445
5662
|
const incoming = agentStateVersion2({
|
|
5446
5663
|
version: typeof incomingRaw === "number" ? incomingRaw : void 0
|
|
@@ -5449,21 +5666,21 @@ var SnapshotService = class {
|
|
|
5449
5666
|
throw new HttpError(
|
|
5450
5667
|
409,
|
|
5451
5668
|
"version_conflict",
|
|
5452
|
-
|
|
5669
|
+
`Package version v${incoming} is not higher than the current v${current}; confirmation is required to import.`
|
|
5453
5670
|
);
|
|
5454
5671
|
}
|
|
5455
5672
|
await this.ensureSnapshot(projectId, agentId);
|
|
5456
5673
|
const stateDir = agentStateDir2(this.root, projectId, agentId);
|
|
5457
|
-
const incomingState =
|
|
5674
|
+
const incomingState = path12.join(extractDir, "agent_state");
|
|
5458
5675
|
let vault = null;
|
|
5459
5676
|
try {
|
|
5460
5677
|
vault = await fs13.readFile(agentVaultPath(this.root, projectId, agentId));
|
|
5461
5678
|
} catch {
|
|
5462
5679
|
}
|
|
5463
5680
|
if (vault !== null) {
|
|
5464
|
-
await fs13.writeFile(
|
|
5681
|
+
await fs13.writeFile(path12.join(incomingState, VAULT_BASENAME), vault, { mode: 384 });
|
|
5465
5682
|
}
|
|
5466
|
-
const trash =
|
|
5683
|
+
const trash = path12.join(staging, "replaced-agent_state");
|
|
5467
5684
|
await fs13.rename(stateDir, trash);
|
|
5468
5685
|
try {
|
|
5469
5686
|
await fs13.rename(incomingState, stateDir);
|
|
@@ -5482,9 +5699,9 @@ var SnapshotService = class {
|
|
|
5482
5699
|
import fs14 from "fs/promises";
|
|
5483
5700
|
import { DEFAULT_PROJECT_ID, projectDir, provisionProjectAgents } from "@prismshadow/penguin-core";
|
|
5484
5701
|
var ABORT_SETTLE_TIMEOUT_MS = 5e3;
|
|
5485
|
-
async function dirExists(
|
|
5702
|
+
async function dirExists(path17) {
|
|
5486
5703
|
try {
|
|
5487
|
-
const stat = await fs14.stat(
|
|
5704
|
+
const stat = await fs14.stat(path17);
|
|
5488
5705
|
return stat.isDirectory();
|
|
5489
5706
|
} catch {
|
|
5490
5707
|
return false;
|
|
@@ -5514,7 +5731,11 @@ var ProjectService = class {
|
|
|
5514
5731
|
requireProjectAccess(userId, projectId) {
|
|
5515
5732
|
const row = this.resolveAccess(userId, projectId);
|
|
5516
5733
|
if (!row) {
|
|
5517
|
-
throw new HttpError(
|
|
5734
|
+
throw new HttpError(
|
|
5735
|
+
404,
|
|
5736
|
+
"project_not_found",
|
|
5737
|
+
"Project does not exist or you do not have access."
|
|
5738
|
+
);
|
|
5518
5739
|
}
|
|
5519
5740
|
return row;
|
|
5520
5741
|
}
|
|
@@ -5531,7 +5752,11 @@ var ProjectService = class {
|
|
|
5531
5752
|
requireProjectOwner(userId, projectId) {
|
|
5532
5753
|
const row = this.requireProjectAccess(userId, projectId);
|
|
5533
5754
|
if (row.role !== "owner") {
|
|
5534
|
-
throw new HttpError(
|
|
5755
|
+
throw new HttpError(
|
|
5756
|
+
403,
|
|
5757
|
+
"owner_required",
|
|
5758
|
+
"Only the Project owner can perform this operation."
|
|
5759
|
+
);
|
|
5535
5760
|
}
|
|
5536
5761
|
return row;
|
|
5537
5762
|
}
|
|
@@ -5573,7 +5798,7 @@ var ProjectService = class {
|
|
|
5573
5798
|
throw new HttpError(
|
|
5574
5799
|
400,
|
|
5575
5800
|
"invalid_project_id",
|
|
5576
|
-
`Project id
|
|
5801
|
+
`Project id must be 2\u201364 characters: ${SEMANTIC_ID_RULE} (the hyphen is reserved as the user namespace separator).`
|
|
5577
5802
|
);
|
|
5578
5803
|
}
|
|
5579
5804
|
} else {
|
|
@@ -5583,12 +5808,12 @@ var ProjectService = class {
|
|
|
5583
5808
|
throw new HttpError(
|
|
5584
5809
|
400,
|
|
5585
5810
|
"project_id_prefix_required",
|
|
5586
|
-
`Project id
|
|
5811
|
+
`Project id must start with ${prefix}, followed by lowercase letters, digits or underscores (at most ${PROJECT_ID_MAX_LENGTH} in total).`
|
|
5587
5812
|
);
|
|
5588
5813
|
}
|
|
5589
5814
|
}
|
|
5590
5815
|
if (this.deps.projects.findById(projectId) !== null || await dirExists(projectDir(this.deps.root, projectId))) {
|
|
5591
|
-
throw new HttpError(409, "project_exists", `Project id
|
|
5816
|
+
throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
|
|
5592
5817
|
}
|
|
5593
5818
|
const displayName = name ?? projectId;
|
|
5594
5819
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5596,7 +5821,7 @@ var ProjectService = class {
|
|
|
5596
5821
|
this.deps.projects.insert({ projectId, ownerUserId: owner.userId, createdAt });
|
|
5597
5822
|
} catch (err) {
|
|
5598
5823
|
if (err instanceof Error && err.message.includes("UNIQUE")) {
|
|
5599
|
-
throw new HttpError(409, "project_exists", `Project id
|
|
5824
|
+
throw new HttpError(409, "project_exists", `Project id is already taken: ${projectId}.`);
|
|
5600
5825
|
}
|
|
5601
5826
|
throw err;
|
|
5602
5827
|
}
|
|
@@ -5653,14 +5878,14 @@ var ProjectService = class {
|
|
|
5653
5878
|
throw new HttpError(
|
|
5654
5879
|
409,
|
|
5655
5880
|
"cannot_delete_default_project",
|
|
5656
|
-
"default_project
|
|
5881
|
+
"default_project is shared with the CLI and cannot be deleted from the web."
|
|
5657
5882
|
);
|
|
5658
5883
|
}
|
|
5659
5884
|
if (this.deps.projects.listAccessible(userId).length <= 1) {
|
|
5660
5885
|
throw new HttpError(
|
|
5661
5886
|
409,
|
|
5662
5887
|
"cannot_delete_last_project",
|
|
5663
|
-
"
|
|
5888
|
+
"This is the account's last Project; deleting it would leave no Project available. Create a new Project first."
|
|
5664
5889
|
);
|
|
5665
5890
|
}
|
|
5666
5891
|
await this.destroyProject(projectId);
|
|
@@ -5707,13 +5932,21 @@ var ProjectService = class {
|
|
|
5707
5932
|
const project = this.requireProjectOwner(userId, projectId);
|
|
5708
5933
|
const target = this.deps.users.findById(targetUserId);
|
|
5709
5934
|
if (!target) {
|
|
5710
|
-
throw new HttpError(404, "user_not_found",
|
|
5935
|
+
throw new HttpError(404, "user_not_found", `User does not exist: ${targetUserId}.`);
|
|
5711
5936
|
}
|
|
5712
5937
|
if (target.userId === project.ownerUserId) {
|
|
5713
|
-
throw new HttpError(
|
|
5938
|
+
throw new HttpError(
|
|
5939
|
+
409,
|
|
5940
|
+
"already_owner",
|
|
5941
|
+
"The owner does not need to grant access to themselves."
|
|
5942
|
+
);
|
|
5714
5943
|
}
|
|
5715
5944
|
if (this.deps.members.isMember(projectId, target.userId)) {
|
|
5716
|
-
throw new HttpError(
|
|
5945
|
+
throw new HttpError(
|
|
5946
|
+
409,
|
|
5947
|
+
"already_member",
|
|
5948
|
+
`${targetUserId} is already a member of this Project.`
|
|
5949
|
+
);
|
|
5717
5950
|
}
|
|
5718
5951
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5719
5952
|
this.deps.members.insert({ projectId, userId: target.userId, createdAt });
|
|
@@ -5723,7 +5956,7 @@ var ProjectService = class {
|
|
|
5723
5956
|
removeMember(userId, projectId, targetUserId) {
|
|
5724
5957
|
this.requireProjectOwner(userId, projectId);
|
|
5725
5958
|
if (!this.deps.members.isMember(projectId, targetUserId)) {
|
|
5726
|
-
throw new HttpError(404, "member_not_found",
|
|
5959
|
+
throw new HttpError(404, "member_not_found", `This Project has no member: ${targetUserId}.`);
|
|
5727
5960
|
}
|
|
5728
5961
|
this.deps.members.delete(projectId, targetUserId);
|
|
5729
5962
|
}
|
|
@@ -5749,7 +5982,7 @@ var ProjectService = class {
|
|
|
5749
5982
|
};
|
|
5750
5983
|
|
|
5751
5984
|
// src/services/session-service.ts
|
|
5752
|
-
import
|
|
5985
|
+
import path13 from "path";
|
|
5753
5986
|
import { readdir as readdir2 } from "fs/promises";
|
|
5754
5987
|
import {
|
|
5755
5988
|
createAgent as createAgent2,
|
|
@@ -5828,7 +6061,7 @@ var SessionService = class {
|
|
|
5828
6061
|
};
|
|
5829
6062
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
5830
6063
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
5831
|
-
for (const file of await listFilesSafe(
|
|
6064
|
+
for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
|
|
5832
6065
|
const match = TRACE_FILE_RE.exec(file);
|
|
5833
6066
|
if (match) mark(dateDir, match[1]);
|
|
5834
6067
|
}
|
|
@@ -5847,23 +6080,32 @@ var SessionService = class {
|
|
|
5847
6080
|
return { sessionCount: all.size, activity };
|
|
5848
6081
|
}
|
|
5849
6082
|
/**
|
|
5850
|
-
* Create a Session: model reference `(provider, modelId)`
|
|
5851
|
-
*
|
|
5852
|
-
*
|
|
5853
|
-
*
|
|
6083
|
+
* Create a Session: the model reference is a complete `(provider, modelId)` pair.
|
|
6084
|
+
* Half a reference is a client error, never something to resolve — the missing half
|
|
6085
|
+
* is never guessed, since a guessed provider would send an entry's credential to a
|
|
6086
|
+
* vendor nobody named. Omitting both falls back to the Project's default reference
|
|
6087
|
+
* (400 prompting to configure a model first if there is none). `workspace` is already
|
|
5854
6088
|
* validated by the route guard. The new Session is added to the active table
|
|
5855
6089
|
* (idle).
|
|
5856
6090
|
*/
|
|
5857
6091
|
async createSession(args) {
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
6092
|
+
if (args.modelId === void 0 !== (args.provider === void 0)) {
|
|
6093
|
+
throw badRequest(
|
|
6094
|
+
"modelId and provider must be given together as a (provider, modelId) pair: specify both, or neither to use the Project's default model."
|
|
6095
|
+
);
|
|
6096
|
+
}
|
|
6097
|
+
let modelId;
|
|
6098
|
+
let provider;
|
|
6099
|
+
if (args.modelId !== void 0 && args.provider !== void 0) {
|
|
6100
|
+
modelId = args.modelId;
|
|
6101
|
+
provider = args.provider;
|
|
6102
|
+
} else {
|
|
5861
6103
|
const def = await this.deps.projectConfig.getDefaultModelRef(args.projectId);
|
|
5862
6104
|
if (def === void 0) {
|
|
5863
6105
|
throw new HttpError(
|
|
5864
6106
|
400,
|
|
5865
6107
|
"no_default_model",
|
|
5866
|
-
"
|
|
6108
|
+
"This Project has no default model yet. Add a model on the Models page and set it as the default first."
|
|
5867
6109
|
);
|
|
5868
6110
|
}
|
|
5869
6111
|
modelId = def.model_id;
|
|
@@ -5878,7 +6120,7 @@ var SessionService = class {
|
|
|
5878
6120
|
try {
|
|
5879
6121
|
session = await agent.createSession({
|
|
5880
6122
|
modelId,
|
|
5881
|
-
|
|
6123
|
+
provider,
|
|
5882
6124
|
...args.workspace !== void 0 ? { workspaceDir: args.workspace } : {}
|
|
5883
6125
|
});
|
|
5884
6126
|
} catch (err) {
|
|
@@ -5910,7 +6152,7 @@ var SessionService = class {
|
|
|
5910
6152
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
5911
6153
|
const ids = /* @__PURE__ */ new Set();
|
|
5912
6154
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
5913
|
-
for (const file of await listFilesSafe(
|
|
6155
|
+
for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
|
|
5914
6156
|
const match = TRACE_FILE_RE.exec(file);
|
|
5915
6157
|
if (match) ids.add(match[1]);
|
|
5916
6158
|
}
|
|
@@ -5922,12 +6164,12 @@ var SessionService = class {
|
|
|
5922
6164
|
const dir = tracesDir2(this.deps.root, projectId, agentId);
|
|
5923
6165
|
let earliest = null;
|
|
5924
6166
|
for (const dateDir of await listDirsSafe(dir)) {
|
|
5925
|
-
for (const file of await listFilesSafe(
|
|
6167
|
+
for (const file of await listFilesSafe(path13.join(dir, dateDir))) {
|
|
5926
6168
|
const match = TRACE_FILE_RE.exec(file);
|
|
5927
6169
|
if (!match || match[1] !== sessionId) continue;
|
|
5928
6170
|
const index = Number(match[2]);
|
|
5929
6171
|
if (!earliest || index < earliest.index) {
|
|
5930
|
-
earliest = { path:
|
|
6172
|
+
earliest = { path: path13.join(dir, dateDir, file), index };
|
|
5931
6173
|
}
|
|
5932
6174
|
}
|
|
5933
6175
|
}
|
|
@@ -5980,7 +6222,7 @@ async function listFilesSafe(dir) {
|
|
|
5980
6222
|
|
|
5981
6223
|
// src/services/trace-service.ts
|
|
5982
6224
|
import fs15 from "fs/promises";
|
|
5983
|
-
import
|
|
6225
|
+
import path14 from "path";
|
|
5984
6226
|
import { agentsDir as agentsDir3, readTraceTolerant as readTraceTolerant2, tracesDir as tracesDir3 } from "@prismshadow/penguin-core";
|
|
5985
6227
|
var TRACE_FILE_RE2 = /^(.+)_(\d{3})\.jsonl$/;
|
|
5986
6228
|
var MAX_SUBAGENT_DEPTH = 4;
|
|
@@ -6018,10 +6260,10 @@ var TraceService = class {
|
|
|
6018
6260
|
const dir = tracesDir3(this.root, projectId, agentId);
|
|
6019
6261
|
const out = [];
|
|
6020
6262
|
for (const dateDir of await listDirs(dir)) {
|
|
6021
|
-
for (const file of await listFiles(
|
|
6263
|
+
for (const file of await listFiles(path14.join(dir, dateDir))) {
|
|
6022
6264
|
const match = TRACE_FILE_RE2.exec(file);
|
|
6023
6265
|
if (!match || match[1] !== sessionId) continue;
|
|
6024
|
-
out.push({ path:
|
|
6266
|
+
out.push({ path: path14.join(dir, dateDir, file), date: dateDir, index: Number(match[2]) });
|
|
6025
6267
|
}
|
|
6026
6268
|
}
|
|
6027
6269
|
return out.sort((a, b) => a.index - b.index);
|
|
@@ -6068,7 +6310,7 @@ var TraceService = class {
|
|
|
6068
6310
|
for (const agentId of await listDirs(agentsDir3(this.root, projectId))) {
|
|
6069
6311
|
const dir = tracesDir3(this.root, projectId, agentId);
|
|
6070
6312
|
for (const dateDir of await listDirs(dir)) {
|
|
6071
|
-
for (const file of await listFiles(
|
|
6313
|
+
for (const file of await listFiles(path14.join(dir, dateDir))) {
|
|
6072
6314
|
const match = TRACE_FILE_RE2.exec(file);
|
|
6073
6315
|
if (match && !index.has(match[1])) index.set(match[1], agentId);
|
|
6074
6316
|
}
|
|
@@ -6368,11 +6610,11 @@ var TraceService = class {
|
|
|
6368
6610
|
const out = { dates: [] };
|
|
6369
6611
|
for (const date of dates) {
|
|
6370
6612
|
const bySession = /* @__PURE__ */ new Map();
|
|
6371
|
-
for (const file of await listFiles(
|
|
6613
|
+
for (const file of await listFiles(path14.join(dir, date))) {
|
|
6372
6614
|
const match = TRACE_FILE_RE2.exec(file);
|
|
6373
6615
|
if (!match) continue;
|
|
6374
6616
|
const sessionId = match[1];
|
|
6375
|
-
const stat = await fs15.stat(
|
|
6617
|
+
const stat = await fs15.stat(path14.join(dir, date, file));
|
|
6376
6618
|
const files = bySession.get(sessionId) ?? [];
|
|
6377
6619
|
files.push({ index: Number(match[2]), sizeBytes: stat.size });
|
|
6378
6620
|
bySession.set(sessionId, files);
|
|
@@ -6396,7 +6638,7 @@ var TraceService = class {
|
|
|
6396
6638
|
throw new HttpError(
|
|
6397
6639
|
404,
|
|
6398
6640
|
"trace_not_found",
|
|
6399
|
-
|
|
6641
|
+
`This Session has no Trace file with index ${index}.`
|
|
6400
6642
|
);
|
|
6401
6643
|
}
|
|
6402
6644
|
return readTraceTolerant2(file.path);
|
|
@@ -6699,7 +6941,7 @@ function createApp(deps2) {
|
|
|
6699
6941
|
});
|
|
6700
6942
|
return handleError(err, c);
|
|
6701
6943
|
});
|
|
6702
|
-
app2.notFound((c) => c.json(errorBody("not_found", "
|
|
6944
|
+
app2.notFound((c) => c.json(errorBody("not_found", "Endpoint does not exist."), 404));
|
|
6703
6945
|
app2.use("*", async (c, next) => {
|
|
6704
6946
|
const start = performance.now();
|
|
6705
6947
|
await next();
|
|
@@ -6709,7 +6951,7 @@ function createApp(deps2) {
|
|
|
6709
6951
|
app2.use("/api/*", async (c, next) => {
|
|
6710
6952
|
const contentLength = Number(c.req.header("content-length") ?? 0);
|
|
6711
6953
|
if (contentLength > MAX_BODY_BYTES) {
|
|
6712
|
-
throw new HttpError(413, "payload_too_large", "
|
|
6954
|
+
throw new HttpError(413, "payload_too_large", "Request body exceeds the 20MB limit.");
|
|
6713
6955
|
}
|
|
6714
6956
|
await next();
|
|
6715
6957
|
});
|
|
@@ -6769,27 +7011,27 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
6769
7011
|
app2.get("*", async (c) => {
|
|
6770
7012
|
const reqPath = decodeURIComponent(c.req.path);
|
|
6771
7013
|
if (reqPath.startsWith("/api/")) {
|
|
6772
|
-
return c.json(errorBody("not_found", "
|
|
7014
|
+
return c.json(errorBody("not_found", "Endpoint does not exist."), 404);
|
|
6773
7015
|
}
|
|
6774
7016
|
const rel = reqPath.replace(/^\/+/, "");
|
|
6775
|
-
const resolved =
|
|
6776
|
-
const base =
|
|
6777
|
-
const target = resolved === base || resolved.startsWith(base +
|
|
7017
|
+
const resolved = path15.resolve(webDist, rel === "" ? "index.html" : rel);
|
|
7018
|
+
const base = path15.resolve(webDist);
|
|
7019
|
+
const target = resolved === base || resolved.startsWith(base + path15.sep) ? resolved : path15.join(base, "index.html");
|
|
6778
7020
|
let file = target;
|
|
6779
7021
|
try {
|
|
6780
7022
|
const stat = await fsp.stat(file);
|
|
6781
|
-
if (stat.isDirectory()) file =
|
|
7023
|
+
if (stat.isDirectory()) file = path15.join(file, "index.html");
|
|
6782
7024
|
await fsp.access(file);
|
|
6783
7025
|
} catch {
|
|
6784
|
-
file =
|
|
7026
|
+
file = path15.join(base, "index.html");
|
|
6785
7027
|
}
|
|
6786
7028
|
let content;
|
|
6787
7029
|
try {
|
|
6788
7030
|
content = await fsp.readFile(file);
|
|
6789
7031
|
} catch {
|
|
6790
|
-
return c.json(errorBody("not_found", "
|
|
7032
|
+
return c.json(errorBody("not_found", "Resource does not exist."), 404);
|
|
6791
7033
|
}
|
|
6792
|
-
const type = CONTENT_TYPES2[
|
|
7034
|
+
const type = CONTENT_TYPES2[path15.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
6793
7035
|
return new Response(new Uint8Array(content), {
|
|
6794
7036
|
status: 200,
|
|
6795
7037
|
headers: { "Content-Type": type }
|
|
@@ -6799,27 +7041,27 @@ function registerStaticRoutes(app2, webDist) {
|
|
|
6799
7041
|
|
|
6800
7042
|
// src/config.ts
|
|
6801
7043
|
import fs17 from "fs";
|
|
6802
|
-
import
|
|
7044
|
+
import path16 from "path";
|
|
6803
7045
|
import { fileURLToPath } from "url";
|
|
6804
7046
|
import { resolveRoot } from "@prismshadow/penguin-core";
|
|
6805
7047
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
6806
7048
|
function defaultWebDist() {
|
|
6807
|
-
const here =
|
|
6808
|
-
const bundled =
|
|
7049
|
+
const here = path16.dirname(fileURLToPath(import.meta.url));
|
|
7050
|
+
const bundled = path16.resolve(here, "..", "web-dist");
|
|
6809
7051
|
if (fs17.existsSync(bundled)) return bundled;
|
|
6810
|
-
return
|
|
7052
|
+
return path16.resolve(here, "..", "..", "web", "dist");
|
|
6811
7053
|
}
|
|
6812
7054
|
function resolveServerConfig(env = process.env) {
|
|
6813
7055
|
const root = env.PENGUIN_HOME ?? resolveRoot();
|
|
6814
7056
|
const port = Number(env.PORT || 7364);
|
|
6815
7057
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
6816
|
-
throw new Error(
|
|
7058
|
+
throw new Error(`Invalid port configuration PORT=${env.PORT}`);
|
|
6817
7059
|
}
|
|
6818
7060
|
return {
|
|
6819
7061
|
root,
|
|
6820
7062
|
host: env.HOST ?? "127.0.0.1",
|
|
6821
7063
|
port,
|
|
6822
|
-
dbPath: env.PENGUIN_WEB_DB ??
|
|
7064
|
+
dbPath: env.PENGUIN_WEB_DB ?? path16.join(root, "web.db"),
|
|
6823
7065
|
webDist: env.PENGUIN_WEB_DIST ?? defaultWebDist(),
|
|
6824
7066
|
authSessionTtlMs: 7 * DAY_MS,
|
|
6825
7067
|
authSessionRenewMs: 6 * DAY_MS
|
|
@@ -6834,15 +7076,15 @@ var app = createApp(deps);
|
|
|
6834
7076
|
await deps.authService.seedAdmin();
|
|
6835
7077
|
await deps.scheduler.start();
|
|
6836
7078
|
var server = serve({ fetch: app.fetch, hostname: config.host, port: config.port }, (info) => {
|
|
6837
|
-
console.log(`penguin-server
|
|
6838
|
-
console.log(
|
|
7079
|
+
console.log(`penguin-server started: http://${config.host}:${info.port}`);
|
|
7080
|
+
console.log(`Data root: ${config.root}`);
|
|
6839
7081
|
console.log(`SQLite: ${config.dbPath}`);
|
|
6840
7082
|
});
|
|
6841
7083
|
var shuttingDown = false;
|
|
6842
7084
|
async function shutdown(signal, exitCode = 0) {
|
|
6843
7085
|
if (shuttingDown) return;
|
|
6844
7086
|
shuttingDown = true;
|
|
6845
|
-
console.log(
|
|
7087
|
+
console.log(`Received ${signal}, shutting down\u2026`);
|
|
6846
7088
|
deps.scheduler.stop();
|
|
6847
7089
|
await deps.manager.shutdown(5e3);
|
|
6848
7090
|
deps.channels.dispose();
|
|
@@ -6855,13 +7097,13 @@ async function shutdown(signal, exitCode = 0) {
|
|
|
6855
7097
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
6856
7098
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6857
7099
|
process.on("uncaughtException", (err) => {
|
|
6858
|
-
console.error(`[server]
|
|
7100
|
+
console.error(`[server] Uncaught exception: ${err.stack ?? err.message}`);
|
|
6859
7101
|
deps.errors.record({ source: "process", err, code: "uncaught_exception" });
|
|
6860
7102
|
void shutdown("uncaughtException", 1).catch(() => process.exit(1));
|
|
6861
7103
|
});
|
|
6862
7104
|
process.on("unhandledRejection", (reason) => {
|
|
6863
7105
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
6864
|
-
console.error(`[server]
|
|
7106
|
+
console.error(`[server] Unhandled promise rejection: ${err.stack ?? err.message}`);
|
|
6865
7107
|
deps.errors.record({ source: "process", err, code: "unhandled_rejection" });
|
|
6866
7108
|
});
|
|
6867
7109
|
//# sourceMappingURL=index.js.map
|