@sema-agent/server 7.79.0 → 7.79.2
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/dist/file-history-disclosure.d.ts +53 -0
- package/dist/file-history-disclosure.js +89 -0
- package/dist/http/admission.d.ts +56 -0
- package/dist/http/admission.js +560 -0
- package/dist/http/approval-sweeps.d.ts +21 -0
- package/dist/http/approval-sweeps.js +138 -0
- package/dist/http/dispatch.d.ts +32 -0
- package/dist/http/dispatch.js +230 -0
- package/dist/http/request-guards.d.ts +51 -0
- package/dist/http/request-guards.js +205 -0
- package/dist/http/resume-legs.d.ts +80 -0
- package/dist/http/resume-legs.js +1230 -0
- package/dist/http/route-table.d.ts +113 -0
- package/dist/http/route-table.js +235 -0
- package/dist/http/routes/admin-memory-optout.d.ts +1 -1
- package/dist/http/routes/memory-compliance.d.ts +1 -1
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/rules.js +4 -4
- package/dist/http/server.d.ts +20 -104
- package/dist/http/server.js +24 -2589
- package/dist/main.js +2 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/plugins/file-history-store-sql.js +12 -11
- package/dist/trace/redact.d.ts +9 -0
- package/dist/trace/redact.js +210 -78
- package/package.json +1 -1
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { isThinkingLevel, expandTiers } from "@sema-agent/core";
|
|
2
|
+
import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
|
|
3
|
+
import { HttpError } from "../security.js";
|
|
4
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
|
+
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
6
|
+
import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
7
|
+
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
8
|
+
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
9
|
+
import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
10
|
+
import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
11
|
+
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
12
|
+
import { sendError, httpErrorCode } from "./send.js";
|
|
13
|
+
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
14
|
+
export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
|
|
15
|
+
export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
|
|
16
|
+
export function validateUserSkills(skills) {
|
|
17
|
+
if (skills === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
if (!Array.isArray(skills))
|
|
20
|
+
return "skills must be an array of { name, description, content }";
|
|
21
|
+
for (const s of skills) {
|
|
22
|
+
if (s === null || typeof s !== "object")
|
|
23
|
+
return "each skill must be an object { name, description, content }";
|
|
24
|
+
if (typeof s["name"] !== "string" || s["name"].length === 0)
|
|
25
|
+
return "skill.name must be a non-empty string";
|
|
26
|
+
if (typeof s["description"] !== "string")
|
|
27
|
+
return "skill.description must be a string";
|
|
28
|
+
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
29
|
+
return `skill.content must be a non-empty string of at most ${MAX_SKILL_CONTENT_CHARS} characters (the engine rejects skills over this size outright at load time)`;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const MAX_IMAGES_PER_REQUEST = 20;
|
|
34
|
+
const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
|
|
35
|
+
function mentionableNames(config, catalog) {
|
|
36
|
+
const allowlist = config.atModelAllowlist ?? [];
|
|
37
|
+
return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
|
|
38
|
+
}
|
|
39
|
+
export function createAdmission(ctx) {
|
|
40
|
+
const { deps, readJson, runnerFor } = ctx;
|
|
41
|
+
async function prepareSpec(req, res, bodyIn, onTypedFailure, legFacts) {
|
|
42
|
+
const body = bodyIn ?? (await readJson(req));
|
|
43
|
+
if (!body || typeof body.objective !== "string") {
|
|
44
|
+
sendError(res, 400, "request.field_invalid", "missing 'objective' string");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (body.objective.trim().length === 0) {
|
|
48
|
+
sendError(res, 400, "request.field_invalid", "objective must not be empty or whitespace-only (an empty user message poisons the session history on strict providers)");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
{
|
|
52
|
+
const bodyKeyIssue = taskBodyKeyIssue(body);
|
|
53
|
+
if (bodyKeyIssue) {
|
|
54
|
+
sendError(res, 400, "request.body_shape", bodyKeyIssue.message, { unknownKeys: bodyKeyIssue.unknownKeys, unsupportedKeys: bodyKeyIssue.unsupportedKeys });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
|
|
59
|
+
sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (typeof body.sessionId === "string" && body.sessionId.length > 64) {
|
|
63
|
+
sendError(res, 400, "request.field_invalid", "sessionId must be at most 64 characters");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
67
|
+
sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
if (body.appendSystemPrompt !== undefined) {
|
|
71
|
+
if (typeof body.appendSystemPrompt !== "string" || body.appendSystemPrompt.length === 0) {
|
|
72
|
+
sendError(res, 400, "request.field_invalid", "appendSystemPrompt must be a non-empty string");
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (body.appendSystemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
76
|
+
sendError(res, 400, "request.field_invalid", `appendSystemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (body.cwd !== undefined && !isValidCwd(body.cwd)) {
|
|
81
|
+
sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const addDirsRaw = body.additionalDirectories;
|
|
85
|
+
if (addDirsRaw !== undefined) {
|
|
86
|
+
if (!Array.isArray(addDirsRaw)) {
|
|
87
|
+
sendError(res, 400, "request.field_invalid", "additionalDirectories must be an array of absolute host paths");
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (addDirsRaw.length > MAX_ADDITIONAL_DIRS) {
|
|
91
|
+
sendError(res, 400, "request.field_invalid", `additionalDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const bad = addDirsRaw.find((d) => !isValidCwd(d));
|
|
95
|
+
if (bad !== undefined) {
|
|
96
|
+
sendError(res, 400, "request.field_invalid", "each additionalDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const addReadDirsRaw = body.additionalReadDirectories;
|
|
101
|
+
if (addReadDirsRaw !== undefined) {
|
|
102
|
+
if (!Array.isArray(addReadDirsRaw)) {
|
|
103
|
+
sendError(res, 400, "request.field_invalid", "additionalReadDirectories must be an array of absolute host paths");
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (addReadDirsRaw.length > MAX_ADDITIONAL_DIRS) {
|
|
107
|
+
sendError(res, 400, "request.field_invalid", `additionalReadDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const badRead = addReadDirsRaw.find((d) => !isValidCwd(d));
|
|
111
|
+
if (badRead !== undefined) {
|
|
112
|
+
sendError(res, 400, "request.field_invalid", "each additionalReadDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const settingsRaw = body.settings;
|
|
117
|
+
if (settingsRaw !== undefined && settingsRaw !== null) {
|
|
118
|
+
if (typeof settingsRaw !== "object" || Array.isArray(settingsRaw)) {
|
|
119
|
+
sendError(res, 400, "request.field_invalid", "settings must be an object");
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const st = settingsRaw;
|
|
123
|
+
{
|
|
124
|
+
const keyIssue = taskSettingsKeyIssue(st);
|
|
125
|
+
if (keyIssue) {
|
|
126
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (typeof st.outputStyle === "string" && st.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
|
|
131
|
+
sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (typeof st.outputStyle === "string" &&
|
|
135
|
+
st.outputStyle.length > 0 &&
|
|
136
|
+
typeof body.appendSystemPrompt === "string" &&
|
|
137
|
+
body.appendSystemPrompt.length + 2 + st.outputStyle.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
138
|
+
sendError(res, 400, "request.field_conflict", `appendSystemPrompt + settings.outputStyle fold into one system-prompt block — combined they must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters (including the 2-char joiner)`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const perms = st.permissions;
|
|
142
|
+
if (perms !== undefined && perms !== null && (typeof perms !== "object" || Array.isArray(perms))) {
|
|
143
|
+
sendError(res, 400, "request.field_invalid", "settings.permissions must be an object");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
|
|
147
|
+
for (const k of ["allow", "deny", "ask"]) {
|
|
148
|
+
const arr = perms[k];
|
|
149
|
+
if (Array.isArray(arr) && arr.length > MAX_SETTINGS_PERMISSION_RULES) {
|
|
150
|
+
sendError(res, 400, "request.field_invalid", `settings.permissions.${k} must have at most ${MAX_SETTINGS_PERMISSION_RULES} entries`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const envRaw = st.env;
|
|
156
|
+
if (envRaw !== undefined && envRaw !== null) {
|
|
157
|
+
if (typeof envRaw !== "object" || Array.isArray(envRaw)) {
|
|
158
|
+
sendError(res, 400, "request.field_invalid", "settings.env must be an object of string→string");
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const entries = Object.entries(envRaw);
|
|
162
|
+
if (entries.length > MAX_SETTINGS_ENV_VARS) {
|
|
163
|
+
sendError(res, 400, "request.field_invalid", `settings.env must have at most ${MAX_SETTINGS_ENV_VARS} variables`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
for (const [k, val] of entries) {
|
|
167
|
+
if (k.length > MAX_SETTINGS_ENV_KEY_CHARS || (typeof val === "string" && val.length > MAX_SETTINGS_ENV_VALUE_CHARS)) {
|
|
168
|
+
sendError(res, 400, "request.field_invalid", `settings.env keys must be ≤${MAX_SETTINGS_ENV_KEY_CHARS} chars and values ≤${MAX_SETTINGS_ENV_VALUE_CHARS} chars`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (st.ultracode !== undefined && typeof st.ultracode !== "boolean") {
|
|
174
|
+
sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (st.skillListingBudgetFraction !== undefined && !skillListingBudgetFractionOk(st.skillListingBudgetFraction)) {
|
|
178
|
+
sendError(res, 400, "request.field_invalid", "settings.skillListingBudgetFraction must be a number greater than 0 and at most 1");
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (st.skillListingMaxDescChars !== undefined && !skillListingMaxDescOk(st.skillListingMaxDescChars)) {
|
|
182
|
+
sendError(res, 400, "request.field_invalid", "settings.skillListingMaxDescChars must be a positive integer");
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (st.hooks !== undefined && st.hooks !== null) {
|
|
186
|
+
const hooksParsed = parseHooksConfig(st.hooks);
|
|
187
|
+
if (!hooksParsed.config) {
|
|
188
|
+
sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const skillsErr = validateUserSkills(body.skills);
|
|
194
|
+
if (skillsErr) {
|
|
195
|
+
sendError(res, 400, "request.field_invalid", skillsErr);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(body.images)) {
|
|
199
|
+
if (body.images.length > MAX_IMAGES_PER_REQUEST) {
|
|
200
|
+
sendError(res, 413, "request.payload_too_large", `at most ${MAX_IMAGES_PER_REQUEST} images per request`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
for (const img of body.images) {
|
|
204
|
+
const data = img.data;
|
|
205
|
+
if (typeof data === "string" && Buffer.byteLength(data, "utf8") > MAX_IMAGE_BASE64_BYTES) {
|
|
206
|
+
sendError(res, 413, "request.payload_too_large", `each inline image must be at most ${MAX_IMAGE_BASE64_BYTES} base64 bytes`);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (body.clientContext !== undefined) {
|
|
212
|
+
const cc = body.clientContext;
|
|
213
|
+
if (typeof cc !== "object" || cc === null || Array.isArray(cc)) {
|
|
214
|
+
sendError(res, 400, "request.field_invalid", "clientContext must be an object");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
if (cc.timeZone !== undefined && (typeof cc.timeZone !== "string" || cc.timeZone.length > 64)) {
|
|
218
|
+
sendError(res, 400, "request.field_invalid", "clientContext.timeZone must be a string of at most 64 characters");
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (cc.userEmail !== undefined && (typeof cc.userEmail !== "string" || cc.userEmail.length > 256)) {
|
|
222
|
+
sendError(res, 400, "request.field_invalid", "clientContext.userEmail must be a string of at most 256 characters");
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (body.sandboxImageProfile !== undefined && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0 || body.sandboxImageProfile.length > 128)) {
|
|
227
|
+
sendError(res, 400, "request.field_invalid", "sandboxImageProfile must be a non-empty string of at most 128 characters");
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
if (body.capabilitiesNeeded !== undefined && (!Array.isArray(body.capabilitiesNeeded) || body.capabilitiesNeeded.some((c) => typeof c !== "string"))) {
|
|
231
|
+
sendError(res, 400, "request.field_invalid", "capabilitiesNeeded must be an array of strings");
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(body.capabilitiesNeeded) && body.capabilitiesNeeded.length > 0 && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0)) {
|
|
235
|
+
sendError(res, 400, "request.field_conflict", "capabilitiesNeeded requires sandboxImageProfile (it constrains the selected image)");
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 && (body.cascade === true || body.verify === true)) {
|
|
239
|
+
sendError(res, 400, "request.field_conflict", "sandboxImageProfile is not supported together with cascade/verify in v1 (the per-task image would not bind to the cascade rungs / verifier sub-run)");
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (body.outputSchema !== undefined) {
|
|
243
|
+
if (typeof body.outputSchema !== "object" || body.outputSchema === null || Array.isArray(body.outputSchema)) {
|
|
244
|
+
sendError(res, 400, "request.field_invalid", "outputSchema must be a JSON Schema object");
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
if (JSON.stringify(body.outputSchema).length > MAX_OUTPUT_SCHEMA_CHARS) {
|
|
248
|
+
sendError(res, 400, "request.field_invalid", `outputSchema must serialize to at most ${MAX_OUTPUT_SCHEMA_CHARS} characters`);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (body.reasoningEffort !== undefined && !isThinkingLevel(body.reasoningEffort)) {
|
|
253
|
+
sendError(res, 400, "request.field_invalid", "reasoningEffort must be one of: off, minimal, low, medium, high, xhigh, max");
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (body.resumeAt !== undefined) {
|
|
257
|
+
if (typeof body.resumeAt !== "string" || body.resumeAt.length === 0 || body.resumeAt.length > 64) {
|
|
258
|
+
sendError(res, 400, "request.field_invalid", "resumeAt must be a non-empty message eventId string (≤64 chars)");
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (body.verify === true || body.cascade === true) {
|
|
262
|
+
sendError(res, 400, "request.field_conflict", "resumeAt is not supported with verify or cascade (those start a fresh/replaced session)");
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (body.resumeAtMode !== undefined) {
|
|
267
|
+
if (body.resumeAtMode !== "at" && body.resumeAtMode !== "before") {
|
|
268
|
+
sendError(res, 400, "request.field_invalid", 'resumeAtMode must be "at" or "before"');
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (body.resumeAt === undefined) {
|
|
272
|
+
sendError(res, 400, "request.field_conflict", "resumeAtMode requires resumeAt (it qualifies the rewind target)");
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (body.suggestNextPrompts !== undefined && typeof body.suggestNextPrompts !== "boolean") {
|
|
277
|
+
const s = body.suggestNextPrompts;
|
|
278
|
+
if (typeof s !== "object" || s === null || Array.isArray(s)) {
|
|
279
|
+
sendError(res, 400, "request.field_invalid", "suggestNextPrompts must be a boolean or an object { count?, role? }");
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const o = s;
|
|
283
|
+
if (o.count !== undefined && (typeof o.count !== "number" || !Number.isFinite(o.count) || o.count < 1)) {
|
|
284
|
+
sendError(res, 400, "request.field_invalid", "suggestNextPrompts.count must be a positive number");
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
if (o.role !== undefined && typeof o.role !== "string") {
|
|
288
|
+
sendError(res, 400, "request.field_invalid", "suggestNextPrompts.role must be a string (a model role name)");
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (body.rewindFiles !== undefined && typeof body.rewindFiles !== "boolean") {
|
|
293
|
+
sendError(res, 400, "request.field_invalid", "rewindFiles must be a boolean");
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (body.restoreFiles !== undefined && typeof body.restoreFiles !== "boolean") {
|
|
297
|
+
sendError(res, 400, "request.field_invalid", "restoreFiles must be a boolean");
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (body.acceptPartialRestore !== undefined && typeof body.acceptPartialRestore !== "boolean") {
|
|
301
|
+
sendError(res, 400, "request.field_invalid", "acceptPartialRestore must be a boolean");
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
if (body.memoryWrite !== undefined && typeof body.memoryWrite !== "boolean") {
|
|
305
|
+
sendError(res, 400, "request.field_invalid", "memoryWrite must be a boolean (false = pause memory writes for this run)");
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
if (body.memoryCapture !== undefined && body.memoryCapture !== "off") {
|
|
309
|
+
sendError(res, 400, "request.field_invalid", "memoryCapture must be the string 'off' when present (the session-level memory-capture opt-out declaration; absent = capture as usual — there is no 'on' spelling)");
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
|
|
313
|
+
sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (body.enableFork !== undefined && typeof body.enableFork !== "boolean") {
|
|
317
|
+
sendError(res, 400, "request.field_invalid", "enableFork must be a boolean");
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
if (body.selfOrchestration !== undefined && typeof body.selfOrchestration !== "boolean") {
|
|
321
|
+
sendError(res, 400, "request.field_invalid", "selfOrchestration must be a boolean");
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
{
|
|
325
|
+
const denial = selfOrchestrationDenial({ selfOrchestrationEnabled: deps.config.selfOrchestrationEnabled, requirePrincipal: deps.config.requirePrincipal }, centerEntitlementSourceWired(deps.config));
|
|
326
|
+
const wantsSelfOrchestration = body.selfOrchestration === true
|
|
327
|
+
|| parseTaskSettings(body.settings).settings?.ultracode === true;
|
|
328
|
+
if (denial !== null && wantsSelfOrchestration) {
|
|
329
|
+
sendError(res, 501, "capability.self_orchestration_required", "self-orchestration is enabled on this worker (SELF_ORCHESTRATION_ENABLED) but it is multi-tenant (REQUIRE_PRINCIPAL) and wires NO per-principal entitlement source, so the engine's allowWorkflows gate — which is tighten-only, meaning an absent resolver leaves it UNGATED — is fail-closed for every principal: no request can ever get the workflow engine here (GET /v1/capabilities reports workflows:false with workflowsGate.denial \"entitlement_resolver_absent\"). Wire a config center (SEMA_REGISTRY_URL + token, not dry-run) to gate it per principal, or set SELF_ORCHESTRATION_ENABLED=false — the honest posture for a worker that cannot grant it. To submit this task without the workflow engine, drop selfOrchestration / settings.ultracode; ultracode's other half (the max thinking tier) is available as reasoningEffort: \"max\"");
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (body.resilience !== undefined) {
|
|
334
|
+
const r = body.resilience;
|
|
335
|
+
if (typeof r !== "object" || r === null || Array.isArray(r)) {
|
|
336
|
+
sendError(res, 400, "request.field_invalid", "resilience must be an object { allowDegrade?, allowFailover?, bypassBreaker? } (booleans)");
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
for (const k of ["allowDegrade", "allowFailover", "bypassBreaker"]) {
|
|
340
|
+
const v = r[k];
|
|
341
|
+
if (v !== undefined && typeof v !== "boolean") {
|
|
342
|
+
sendError(res, 400, "request.field_invalid", `resilience.${k} must be a boolean`);
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (body.finalVerification !== undefined && typeof body.finalVerification !== "boolean") {
|
|
348
|
+
sendError(res, 400, "request.field_invalid", "finalVerification must be a boolean");
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
if (body.limits !== undefined) {
|
|
352
|
+
const l = body.limits;
|
|
353
|
+
if (typeof l !== "object" || l === null || Array.isArray(l)) {
|
|
354
|
+
sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (l.timeoutSec !== undefined) {
|
|
358
|
+
sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (l.deadlineNudge !== undefined) {
|
|
362
|
+
sendError(res, 400, "request.field_invalid", "limits.deadlineNudge was retired in core 5.8.0 — use limits.approachNotice (default on at 80%/95%; false turns it off, {at: [r1, r2]} tunes thresholds)");
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
for (const k of ["callCapByDeadline", "gracefulFinalize"]) {
|
|
366
|
+
if (l[k] !== undefined) {
|
|
367
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} was retired in core 5.8.0 (the deadline-pacing mechanism family is gone; there is no replacement key — remove it)`);
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
for (const k of ["maxCostUsd", "maxTokens"]) {
|
|
372
|
+
if (l[k] !== undefined) {
|
|
373
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} is not read from limits here — send it at the request body top level (${k}); it is capped by the operator ceiling ${k === "maxCostUsd" ? "MAX_TASK_COST_USD" : "MAX_TASK_TOKENS"}`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (l.degrade !== undefined) {
|
|
378
|
+
sendError(res, 400, "request.field_invalid", "limits.degrade is operator-controlled (env MODEL_DEGRADE_TO / MODEL_DEGRADE_AT_COST_FRACTION), not caller-settable — remove it");
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
if (l.budgetStreamCancel !== undefined) {
|
|
382
|
+
sendError(res, 400, "request.field_invalid", "limits.budgetStreamCancel is not caller-settable here — the engine default applies (armed whenever a cost budget is present); remove it");
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
for (const k of ["maxWalltimeMs", "maxOutputTokens", "maxTurns"]) {
|
|
386
|
+
const v = l[k];
|
|
387
|
+
if (v !== undefined && (typeof v !== "number" || !Number.isInteger(v) || v < 1)) {
|
|
388
|
+
sendError(res, 400, "request.field_invalid", `limits.${k} must be a positive integer${k === "maxWalltimeMs" ? " (milliseconds)" : ""}`);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
{
|
|
393
|
+
const v = l.approachNotice;
|
|
394
|
+
if (v !== undefined && normalizeApproachNotice(v) === undefined) {
|
|
395
|
+
sendError(res, 400, "request.field_invalid", "limits.approachNotice accepts false (turn off) or {at?: [r1, r2]} with fractions in (0, 1], r1 <= r2 (default on at 80%/95%)");
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (body.agents !== undefined) {
|
|
401
|
+
const agentsErr = validateTaskAgents(body.agents);
|
|
402
|
+
if (agentsErr) {
|
|
403
|
+
sendError(res, 400, "request.field_invalid", agentsErr);
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const agentsBare = deps.config.models ?? {};
|
|
407
|
+
const agentsCatalog = expandTiers(agentsBare, deps.config.tiers ?? {}) ?? agentsBare;
|
|
408
|
+
const um = unknownTaskAgentModelRef(body.agents, agentsCatalog);
|
|
409
|
+
if (um !== undefined) {
|
|
410
|
+
sendError(res, 400, "request.unknown_reference", `unknown agents[${um.index}].model "${um.ref.slice(0, 120)}" — not in the configured catalog (a catalog name or tier word)`, { available: mentionableNames(deps.config, agentsCatalog) });
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (body.interactiveTools !== undefined && typeof body.interactiveTools !== "boolean") {
|
|
415
|
+
sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
|
|
419
|
+
sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
if (body.retainBackgroundProcesses !== undefined && typeof body.retainBackgroundProcesses !== "boolean") {
|
|
423
|
+
sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
for (const key of ["excludeTools", "deferTools"]) {
|
|
427
|
+
const v = body[key];
|
|
428
|
+
if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
|
|
429
|
+
sendError(res, 400, "request.field_invalid", `${key} must be an array of non-empty tool-name strings`);
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
{
|
|
434
|
+
const v = body.promptProfile;
|
|
435
|
+
if (v !== undefined && v !== "simple" && v !== "classic") {
|
|
436
|
+
sendError(res, 400, "request.field_invalid", 'promptProfile must be "simple" or "classic"');
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
{
|
|
441
|
+
const v = body.toolMaterializeStrategy;
|
|
442
|
+
if (v !== undefined && v !== "static" && v !== "swap") {
|
|
443
|
+
sendError(res, 400, "request.field_invalid", 'toolMaterializeStrategy must be "static" or "swap"');
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
{
|
|
448
|
+
const v = body.permissionMode;
|
|
449
|
+
if (v !== undefined && v !== "default" && v !== "acceptEdits" && v !== "plan" && v !== "bypassPermissions" && v !== "auto") {
|
|
450
|
+
sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
{
|
|
455
|
+
const s = body.settings;
|
|
456
|
+
if (s !== null && typeof s === "object" && !Array.isArray(s)) {
|
|
457
|
+
const p = s.permissions;
|
|
458
|
+
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
459
|
+
const m = p.defaultMode;
|
|
460
|
+
if (m !== undefined && m !== "default" && m !== "acceptEdits" && m !== "plan" && m !== "bypassPermissions" && m !== "auto") {
|
|
461
|
+
sendError(res, 400, "request.field_invalid", 'settings.permissions.defaultMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const d = p.disableAutoMode;
|
|
465
|
+
if (d !== undefined && d !== "disable" && d !== true && d !== false) {
|
|
466
|
+
sendError(res, 400, "request.field_invalid", 'settings.permissions.disableAutoMode must be the literal "disable" or a boolean when present (omit it to leave auto mode available)');
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (body.model !== undefined) {
|
|
473
|
+
if (typeof body.model !== "string" || body.model.length === 0) {
|
|
474
|
+
sendError(res, 400, "request.field_invalid", "model must be a non-empty string (a configured catalog name, tier word, or model id)");
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
const bare = deps.config.models ?? {};
|
|
478
|
+
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
479
|
+
if (matchCatalogModel(body.model, catalog) === undefined) {
|
|
480
|
+
sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (body.compactionModel !== undefined) {
|
|
485
|
+
if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0) {
|
|
486
|
+
sendError(res, 400, "request.field_invalid", "compactionModel must be a non-empty string (a configured catalog name, tier word, or model id)");
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const bare = deps.config.models ?? {};
|
|
490
|
+
const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
|
|
491
|
+
if (matchCatalogModel(body.compactionModel, catalog) === undefined) {
|
|
492
|
+
sendError(res, 400, "request.unknown_reference", `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (body.attachments !== undefined) {
|
|
497
|
+
const a = body.attachments;
|
|
498
|
+
if (typeof a !== "object" || a === null || Array.isArray(a)) {
|
|
499
|
+
sendError(res, 400, "request.field_invalid", "attachments must be an object { todoReminder?, todoReminderMode?, changedFiles?, planModeReminder?, budgetUsd?, backgroundTasks?, toolsDelta?, agentListing?, skillsListing?, mcpInstructions? }");
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
const rec = a;
|
|
503
|
+
for (const k of ["todoReminder", "planModeReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "agentListing", "skillsListing", "mcpInstructions"]) {
|
|
504
|
+
if (rec[k] !== undefined && typeof rec[k] !== "boolean") {
|
|
505
|
+
sendError(res, 400, "request.field_invalid", `attachments.${k} must be a boolean`);
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
if (rec.todoReminderMode !== undefined && rec.todoReminderMode !== "baseline" && rec.todoReminderMode !== "off") {
|
|
510
|
+
sendError(res, 400, "request.field_invalid", 'attachments.todoReminderMode must be "baseline" | "off"');
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const cf = rec.changedFiles;
|
|
514
|
+
if (cf !== undefined && typeof cf !== "boolean") {
|
|
515
|
+
if (typeof cf !== "object" || cf === null || Array.isArray(cf)) {
|
|
516
|
+
sendError(res, 400, "request.field_invalid", "attachments.changedFiles must be a boolean or { maxFiles?: number }");
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const mf = cf.maxFiles;
|
|
520
|
+
if (mf !== undefined && (typeof mf !== "number" || !Number.isFinite(mf) || mf < 1)) {
|
|
521
|
+
sendError(res, 400, "request.field_invalid", "attachments.changedFiles.maxFiles must be a number ≥ 1");
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const verify = verifyRoundsFromBody(body);
|
|
527
|
+
const cascade = body.cascade === true;
|
|
528
|
+
if (cascade && verify) {
|
|
529
|
+
sendError(res, 400, "request.field_conflict", "verify and cascade are mutually exclusive (verify = adversarial gate; cascade = cheap→strong ladder)");
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
if (body.suggestNextPrompts && (verify || cascade)) {
|
|
533
|
+
sendError(res, 400, "request.field_conflict", "suggestNextPrompts is not supported with verify or cascade (those return a result, not a streamed run)");
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
if (cascade && deps.config.cascadeLadder.length === 0) {
|
|
537
|
+
sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
542
|
+
const engineNotices = [];
|
|
543
|
+
const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
|
|
544
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body, ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
if (err instanceof HttpError) {
|
|
548
|
+
if (onTypedFailure) {
|
|
549
|
+
onTypedFailure(err);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
throw err;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { prepareSpec };
|
|
559
|
+
}
|
|
560
|
+
//# sourceMappingURL=admission.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审批扫腿(S-147 切片:`http/server.ts` 5196–5488 逐字搬家,正文一字未改)——两条不经 HTTP 路由的赎回口:
|
|
3
|
+
* ①`denyExpiredApprovals`([ref] D-D,SLA 到期的 deny-sweep,main.ts 的 reaper 按拍调);
|
|
4
|
+
* ②`redeemParkedAsk`([ref],live 回决腿的 PARKED 赎回席,装配层接进协调器的晚绑座)。
|
|
5
|
+
* 两者都走 resume 家族的 `resumeCheckpoint`,所以它们的属主是「谁在无 HTTP 请求的情况下替人做决定」这件事,
|
|
6
|
+
* 不是请求流水线。`createWallClockJumpGuard` 建的 `denySweepClockGuard` 是 ① 的私有件,随它同居。
|
|
7
|
+
* ctx 字段 3 只(`deps` / `resumeCheckpoint` / `mayDiscloseRowError`),机器算(closure-free-vars)与手抄一致。
|
|
8
|
+
*/
|
|
9
|
+
import { type ParkedAskRedeemRequest } from "../tool-approval.js";
|
|
10
|
+
import type { createResumeLegs } from "./resume-legs.js";
|
|
11
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
12
|
+
export declare function createApprovalSweeps(ctx: {
|
|
13
|
+
deps: FlatServiceDeps;
|
|
14
|
+
} & Pick<ReturnType<typeof createResumeLegs>, "resumeCheckpoint" | "mayDiscloseRowError">): {
|
|
15
|
+
denyExpiredApprovals: (now: number) => Promise<void>;
|
|
16
|
+
redeemParkedAsk: (req: ParkedAskRedeemRequest) => Promise<{
|
|
17
|
+
status: number;
|
|
18
|
+
body: Record<string, unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=approval-sweeps.d.ts.map
|