@sema-agent/server 7.78.0 → 7.78.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-card.d.ts +24 -1
- package/dist/approval-card.js +13 -0
- package/dist/boot/resolve-spec.js +3 -0
- package/dist/http/routes/approvals-assistant.d.ts +22 -0
- package/dist/http/routes/approvals-assistant.js +4 -1
- package/dist/http/routes/tasks.js +4 -9
- package/dist/http/server.d.ts +28 -4
- package/dist/http/server.js +38 -12
- package/dist/request-key-closure.d.ts +30 -0
- package/dist/request-key-closure.js +56 -0
- package/dist/runs.js +3 -1
- package/dist/runtime-governance.d.ts +18 -4
- package/dist/runtime-governance.js +2 -2
- package/dist/spec-fields.d.ts +3 -1
- package/dist/spec-fields.js +4 -5
- package/dist/task-settings.d.ts +18 -16
- package/dist/task-settings.js +17 -29
- package/dist/tool-approval.d.ts +35 -3
- package/dist/tool-approval.js +41 -19
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/ledger-sink.js +12 -5
- package/dist/trace/model-segment.d.ts +32 -2
- package/dist/trace/model-segment.js +15 -5
- package/dist/trace/project.d.ts +37 -3
- package/dist/trace/project.js +14 -10
- package/package.json +3 -3
package/dist/approval-card.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* 没有方法、没有捕获的行为。
|
|
22
22
|
*/
|
|
23
23
|
import { z } from "zod";
|
|
24
|
-
import { type AskEvidenceAbsence, type RuleOffer } from "@sema-agent/core";
|
|
24
|
+
import { type AskEvidenceAbsence, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
|
|
25
25
|
import type { AskRow } from "./plugins/approval-ask-store-sql.js";
|
|
26
26
|
/** 模型自由文本(`sourceAgentName` / `delegation.agentName`)的限长(设计稿 §6.2)。设计 §3.1 把
|
|
27
27
|
* 「限长 + 脱敏」写成 **server 新增责任**(引擎无此层):spawning model 挑的名字是自由文本,
|
|
@@ -269,6 +269,29 @@ export type ProbeCauseProjection = z.infer<typeof ProbeCauseSchema>;
|
|
|
269
269
|
* (截出来的是另一个机器码);`shown` 超限 ⇒ **截条目**,`total` 逐字保真。
|
|
270
270
|
*/
|
|
271
271
|
export declare function readProbeCause(req: unknown): ProbeCauseProjection | undefined;
|
|
272
|
+
/**
|
|
273
|
+
* [ref](core 7.19.0)—— `readRootCandidate.dir` 的**上限**。core 自己的同族界是
|
|
274
|
+
* `READ_ROOT_CANDIDATE_DIR_MAX = 1024`(`src/core/read-root-candidate.ts`),**未从包根导出**,故钉本地
|
|
275
|
+
* 孪生 + 本出处注记(与 `MAX_SKILL_CONTENT_CHARS` 同款处置)。
|
|
276
|
+
*
|
|
277
|
+
* 🔴 超限的方向是**整只丢,不截** —— core 契约 `shell.read_boundary.grant_candidate_is_the_grant`:卡上
|
|
278
|
+
* 显示的串就是人要加进读目录的那个串,截过的目录是**另一个**目录,加进去也清不掉这只 ask。与
|
|
279
|
+
* {@link MAX_PROBE_CAUSE_CODE} 的 `code` 超限整只丢逐字同判据。
|
|
280
|
+
*/
|
|
281
|
+
export declare const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
|
|
282
|
+
/** 窄读的输出形 —— **就是 core 的那只型**,不在本仓再声明一份同形结构(重声明 = 上游改形那天本仓静默不红)。 */
|
|
283
|
+
export type ReadRootCandidateProjection = ReadRootGrantCandidate;
|
|
284
|
+
/**
|
|
285
|
+
* [ref] —— `AskRequest.readRootCandidate` 的边界窄读:**把这个目录加进本会话的读目录,这只 ask 就不会
|
|
286
|
+
* 再出现**。活卡帧的**唯一**铸造点。
|
|
287
|
+
*
|
|
288
|
+
* 🔴 **零判读**:server 既不从命令文本重推目录、也不校验它是不是真的能清掉这只 ask —— 那是 core 已经
|
|
289
|
+
* 做过的判断(它把提议的根放回读边界又走了一遍),在下游再判一次就是同一个事实的第二个判官。
|
|
290
|
+
* 🔴 形不合 ⇒ **按缺席处置**;超限 ⇒ **整只丢**(见 {@link MAX_READ_ROOT_CANDIDATE_DIR})。
|
|
291
|
+
* 🔴 **缺席不是断言**:它同时覆盖「这一族 ask 没有任何目录能清掉」(敏感路径 deny 行、读边界压根读不懂
|
|
292
|
+
* 的命令、递归遍历、非读边界 ask……)与「老引擎」两形 —— 消费端只读在场,永远不读缺席。
|
|
293
|
+
*/
|
|
294
|
+
export declare function readReadRootCandidate(req: unknown): ReadRootCandidateProjection | undefined;
|
|
272
295
|
/**
|
|
273
296
|
* [ref] 件 G2 —— `AskRequest.ruleEvidence` 的形(帧 / `card_json` / 读面共用;窄读入口是
|
|
274
297
|
* {@link readRuleEvidence})。三对成员各 = 值**或**五词命名缺席(`AskEvidenceAbsence`),core 引擎
|
package/dist/approval-card.js
CHANGED
|
@@ -160,6 +160,19 @@ export function readProbeCause(req) {
|
|
|
160
160
|
...(raw.further !== undefined ? { further: family(raw.further) } : {}),
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
+
export const MAX_READ_ROOT_CANDIDATE_DIR = 1024;
|
|
164
|
+
const ReadRootCandidateEnvelopeSchema = z.object({
|
|
165
|
+
readRootCandidate: z.object({ dir: z.string().min(1), clearsThisAsk: z.literal(true) }).optional(),
|
|
166
|
+
});
|
|
167
|
+
export function readReadRootCandidate(req) {
|
|
168
|
+
const parsed = ReadRootCandidateEnvelopeSchema.safeParse(req);
|
|
169
|
+
const raw = parsed.success ? parsed.data.readRootCandidate : undefined;
|
|
170
|
+
if (raw === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (raw.dir.length > MAX_READ_ROOT_CANDIDATE_DIR)
|
|
173
|
+
return undefined;
|
|
174
|
+
return { dir: raw.dir, clearsThisAsk: true };
|
|
175
|
+
}
|
|
163
176
|
const MAX_EVIDENCE_DOT_ACTOR = 256;
|
|
164
177
|
const MAX_EVIDENCE_DOTS = 16;
|
|
165
178
|
const AskEvidenceAbsenceSchema = z.custom((v) => typeof v === "string" && ASK_EVIDENCE_ABSENCE_VALUES.includes(v));
|
|
@@ -279,6 +279,7 @@ export function createResolveSpec(ctx) {
|
|
|
279
279
|
additionalReadDirectories,
|
|
280
280
|
enablePlanMode: config.planModeEnabled ? true : undefined,
|
|
281
281
|
...(effMode ? { shellGate: shellGateForMode(effMode) } : {}),
|
|
282
|
+
...(effMode === "bypassPermissions" ? { writeFace: "open" } : {}),
|
|
282
283
|
...(effMode === "auto" ? { autoModeRequested: true } : {}),
|
|
283
284
|
selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
|
|
284
285
|
forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
|
|
@@ -367,6 +368,8 @@ export function createResolveSpec(ctx) {
|
|
|
367
368
|
return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
|
|
368
369
|
})(cap.tools),
|
|
369
370
|
skills: mergeUserSkills(cap.skills, body.skills, logger),
|
|
371
|
+
...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
|
|
372
|
+
...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
|
|
370
373
|
mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
|
|
371
374
|
a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
|
|
372
375
|
promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
|
|
@@ -6,6 +6,28 @@ import { type RouteCtx } from "../route-ctx.js";
|
|
|
6
6
|
export declare const ASSISTANT_PREEMPT_RE: RegExp;
|
|
7
7
|
export declare const ASSISTANT_RESUME_RE: RegExp;
|
|
8
8
|
export declare const ASSISTANT_PLAN_REVIEW_RE: RegExp;
|
|
9
|
+
/**
|
|
10
|
+
* S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
|
|
11
|
+
*
|
|
12
|
+
* ## 三个域,三个属主(这才是归一:每个域的读法只写一次,而不是三处各挑一种写法)
|
|
13
|
+
* `body.reason` 在这条路由上会被读三次,而它们的属主**不是同一个**:
|
|
14
|
+
* · **签名域**(直连门的 HMAC 信封第五位)—— 属主是**跨仓字节契约**(§6.2 / `approvalHmacMessage`,
|
|
15
|
+
* 签名器在 cli / SDK)。那份契约明写空串与 `null` 是两个不同的被签值。⇒ **逐字线值,永不折叠**;
|
|
16
|
+
* 在这里折一刀,一份合法签发的空理由证明就永远 401(合并复审 R1 [high] 真复现过)。
|
|
17
|
+
* · **上送域**(`resumePlanReview` / `resumeCheckpoint` 的 `reason` 尾参)—— 属主是**引擎的单铸点**
|
|
18
|
+
* (7.19.0 / [ref] 把「人的结算」收进一个铸点,空文本在那里折成缺席,live 与 durable decide 两条路径同形)。
|
|
19
|
+
* ⇒ **原样透传**;跨仓宪法「源头修复禁下游旁路」:在下游再折一次就是同一语义的第二个写者,
|
|
20
|
+
* 今天两者答案相同,上游哪天改法就变成一条谁都查不出来的分歧。
|
|
21
|
+
* · **本仓自铸域**(幂等回放的**身份**)—— 属主就是本仓:它回答「这次重试与首决是不是同一个决议」。
|
|
22
|
+
* ⇒ **本函数**。此前那里是一个裸真值判(`body.reason ? …`),与上面两处混在一起看就是三种拼法;
|
|
23
|
+
* 收进一只具名读法之后,「什么算一个理由」在本仓只有这一个答案,而另外两个域各自点名了自己的属主。
|
|
24
|
+
*
|
|
25
|
+
* 值域前置门(非串 400 / 超长 413)在两条路由里各自先跑,本函数只做「空串 ⇒ 缺席」这一件事。
|
|
26
|
+
* 纯空白**仍是**一条理由(上游把这一形逐字钉成 current behaviour,本仓跟着它、不自作主张多折一层)。
|
|
27
|
+
*/
|
|
28
|
+
export declare function decisionReasonOf(body: {
|
|
29
|
+
reason?: unknown;
|
|
30
|
+
}): string | undefined;
|
|
9
31
|
/** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
|
|
10
32
|
* load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
|
|
11
33
|
* so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
|
|
@@ -20,6 +20,9 @@ import { sendResumeOutcome } from "../route-ctx.js";
|
|
|
20
20
|
export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
|
|
21
21
|
export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
|
|
22
22
|
export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
|
|
23
|
+
export function decisionReasonOf(body) {
|
|
24
|
+
return typeof body.reason === "string" && body.reason !== "" ? body.reason : undefined;
|
|
25
|
+
}
|
|
23
26
|
export function isQuestionAnswer(v) {
|
|
24
27
|
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
25
28
|
return false;
|
|
@@ -564,7 +567,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
564
567
|
payload: {
|
|
565
568
|
...(decision === "approve" && binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
|
|
566
569
|
...(answer !== undefined ? { answer } : {}),
|
|
567
|
-
...(
|
|
570
|
+
...((r) => (r !== undefined ? { reason: r } : {}))(decisionReasonOf(body)),
|
|
568
571
|
},
|
|
569
572
|
deciderPrincipal,
|
|
570
573
|
explicitOperator,
|
|
@@ -13,7 +13,7 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
|
|
|
13
13
|
import { mintCancelledResult } from "../../run-cancel-context.js";
|
|
14
14
|
import { recordResourceWindow } from "../../resource-window.js";
|
|
15
15
|
import { redactSecrets } from "../../trace/redact.js";
|
|
16
|
-
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData } from "../../trace/project.js";
|
|
16
|
+
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
|
|
17
17
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
18
18
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
19
19
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
@@ -502,12 +502,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
502
502
|
}
|
|
503
503
|
else if (ev.type === "text_delta" || ev.type === "text_end" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
504
504
|
const e = ev;
|
|
505
|
-
const ident =
|
|
506
|
-
...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
|
|
507
|
-
...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}),
|
|
508
|
-
...(e.sourceTaskId !== undefined ? { sourceTaskId: e.sourceTaskId } : {}),
|
|
509
|
-
...(e.bgAgentId !== undefined ? { bgAgentId: e.bgAgentId } : {}),
|
|
510
|
-
};
|
|
505
|
+
const ident = liveIdentityFields(e);
|
|
511
506
|
const textEnd = ev.type === "text_end" ? textEndEventData(e) : undefined;
|
|
512
507
|
const arm = ev.type === "text_delta"
|
|
513
508
|
? { type: "text_delta", delta: e.delta, ...ident }
|
|
@@ -523,7 +518,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
523
518
|
sseData(res, arm);
|
|
524
519
|
}
|
|
525
520
|
else {
|
|
526
|
-
sseData(res, ev);
|
|
521
|
+
sseData(res, { ...ev });
|
|
527
522
|
}
|
|
528
523
|
}
|
|
529
524
|
});
|
|
@@ -616,7 +611,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
616
611
|
if (deps.toolApproval) {
|
|
617
612
|
const toolApproval = deps.toolApproval;
|
|
618
613
|
prepared.spec.onAsk = windowZero
|
|
619
|
-
? () => Promise.resolve(toolApproval.unattendedAskOutcome())
|
|
614
|
+
? (askReq) => Promise.resolve(toolApproval.unattendedAskOutcome({ taskId: askTaskId, owner: askOwner, toolName: askReq.toolName }))
|
|
620
615
|
: toolApproval.boundAsk({
|
|
621
616
|
owner: askOwner,
|
|
622
617
|
taskId: askTaskId,
|
package/dist/http/server.d.ts
CHANGED
|
@@ -730,18 +730,42 @@ export type FlatServiceDeps = ServiceCoreDeps & Partial<ServiceStoreDeps & Servi
|
|
|
730
730
|
export type ServiceDeps = FlatServiceDeps & ServiceDepGroups;
|
|
731
731
|
/** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
|
|
732
732
|
export declare function flattenServiceDeps(deps: ServiceDeps): FlatServiceDeps;
|
|
733
|
-
/** Personalization caps: server-pinned hard limits — a UI may be stricter, never looser. */
|
|
734
|
-
export declare const MAX_USER_SKILLS = 10;
|
|
735
733
|
/** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
|
|
736
734
|
* dist/core/runner/synthetic-tools.js:84,未从 core 包根导出,故钉本地孪生+本出处注记)。修7 曾对齐旧
|
|
737
735
|
* 20k 截断门([ref] 飞轮考据:20k 是压缩保留区口径,invoke 时刻用错),core 1.293 撤门后本门跟随回收
|
|
738
|
-
* ([ref]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。
|
|
736
|
+
* ([ref]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。
|
|
737
|
+
*
|
|
738
|
+
* 🔴 **这是技能面**唯一**剩下的尺寸门,而且它不是超集帽**:引擎在装配期对超 1 MiB 的技能本就整拒
|
|
739
|
+
* (config 阶段错误),门上先答只是把同一个判决搬到调用方看得懂的地方。[ref] 退役的三条帽
|
|
740
|
+
* (条数 / description / name 长度)都没有这个性质——它们是本仓自造的、CC 与引擎都没有的门。 */
|
|
739
741
|
export declare const MAX_SKILL_CONTENT_CHARS = 1048576;
|
|
740
742
|
export declare const MAX_SYSTEM_PROMPT_CHARS = 16384;
|
|
741
743
|
/** Cap on a request's `outputSchema` (JSON-serialized) — an uncapped schema bloats the prompt + the per-turn
|
|
742
744
|
* validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
|
|
743
745
|
export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
744
|
-
/**
|
|
746
|
+
/**
|
|
747
|
+
* Validate `body.skills` —— **形**校验 + 一条引擎孪生门。返回错误串(→ 400)或 null。
|
|
748
|
+
*
|
|
749
|
+
* 🔴 **[ref](7.77.0,硬 BREAKING;零别名零双读)**:此前这里有三条本仓自造的拒绝臂,全部退役 ——
|
|
750
|
+
* · `at most 10 skills per request`(条数帽);
|
|
751
|
+
* · `skill.description must be a string of at most 1024 characters`(描述长度帽);
|
|
752
|
+
* · `skill.name must be a non-empty string of at most 64 characters` 里的 **长度**那一半。
|
|
753
|
+
*
|
|
754
|
+
* 退役的判据是**上游两侧都没有同形门**:CC 2.1.259 的技能清单渲染(`cli259.js:23195` 的预算算法)
|
|
755
|
+
* 在超预算时把**描述裁短**、再不够就退成 name-only,**从不**因为条数多或描述长而拒掉一条技能;
|
|
756
|
+
* 它的 `Ir()` 名字校验也只判非空 / 无配对代理 / 无首尾空白 / 无括号逗号控制字符,没有长度帽。
|
|
757
|
+
* 引擎侧同理:唯一整拒的是 1 MiB 的单技能加载门。于是那三条帽是「装了 11 个技能就 400」这种
|
|
758
|
+
* 纯本仓面的伤——收益是零(预算由渲染侧按字节兜),代价是用户装不进自己的技能库。
|
|
759
|
+
*
|
|
760
|
+
* **补偿**(三问的第三问):`settings.skillListingBudgetFraction` / `settings.skillListingMaxDescChars`
|
|
761
|
+
* 两个旋钮直达引擎的渲染预算(per-turn 成本的真属主)。
|
|
762
|
+
* ⚠️ **fraction 是**单向**的**:引擎的预算 = `min(窗口 × fraction, 8 000 字节)`,8 000 是**投递车道的结构
|
|
763
|
+
* 上限**(清单帧与同 turn 的其它附件共用一份完整送达额度)⇒ **调低生效、调高被夹**;调高唯一的用处是让
|
|
764
|
+
* 一个声明窗口小于 200 000 token 的模型把清单预算拿回大窗口的水平。别把这句读成「调用方可以随便加大」。
|
|
765
|
+
* 体积面照旧由**非技能专属**的 8 MiB 请求体上限兜住(`MAX_BODY`),那是一条一直都在的门。
|
|
766
|
+
*
|
|
767
|
+
* 🔴 **server 侧禁裁列表 / 禁截描述**:渲染归引擎(单一属主)。本函数只判「这一条技能的形能不能用」。
|
|
768
|
+
*/
|
|
745
769
|
export declare function validateUserSkills(skills: unknown): string | null;
|
|
746
770
|
/**
|
|
747
771
|
* Managed-Agents-style HTTP/SSE service surface.
|
package/dist/http/server.js
CHANGED
|
@@ -10,6 +10,7 @@ import { HttpError, principalFrom, verifiedPrincipal, setSsoPrincipal, ssoVerifi
|
|
|
10
10
|
import { verifyDirectDoorProof } from "../principal-jwt.js";
|
|
11
11
|
import {} from "../memory-sync.js";
|
|
12
12
|
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, taskSettingsKeyIssue } from "../task-settings.js";
|
|
13
|
+
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
13
14
|
import { selfOrchestrationDenial } from "../task-workflow.js";
|
|
14
15
|
import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
|
|
15
16
|
import { parseHooksConfig } from "../hooks/hook-runner.js";
|
|
@@ -37,6 +38,7 @@ import { modelTextEventData, turnEndEventData, contextUsageEventData, toolStartE
|
|
|
37
38
|
import { redactSecrets } from "../trace/redact.js";
|
|
38
39
|
import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
|
|
39
40
|
import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
|
|
41
|
+
import { reasoningDeltaBytes, reasoningSegmentEventId } from "../trace/model-segment.js";
|
|
40
42
|
import { errorText } from "../observability/err-text.js";
|
|
41
43
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
42
44
|
import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
|
|
@@ -95,7 +97,6 @@ export function flattenServiceDeps(deps) {
|
|
|
95
97
|
return deps;
|
|
96
98
|
return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
|
|
97
99
|
}
|
|
98
|
-
export const MAX_USER_SKILLS = 10;
|
|
99
100
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
100
101
|
export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
|
|
101
102
|
export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
|
|
@@ -104,15 +105,13 @@ export function validateUserSkills(skills) {
|
|
|
104
105
|
return null;
|
|
105
106
|
if (!Array.isArray(skills))
|
|
106
107
|
return "skills must be an array of { name, description, content }";
|
|
107
|
-
if (skills.length > MAX_USER_SKILLS)
|
|
108
|
-
return `at most ${MAX_USER_SKILLS} skills per request`;
|
|
109
108
|
for (const s of skills) {
|
|
110
109
|
if (s === null || typeof s !== "object")
|
|
111
110
|
return "each skill must be an object { name, description, content }";
|
|
112
|
-
if (typeof s["name"] !== "string" || s["name"].length === 0
|
|
113
|
-
return "skill.name must be a non-empty string
|
|
114
|
-
if (typeof s["description"] !== "string"
|
|
115
|
-
return "skill.description must be a string
|
|
111
|
+
if (typeof s["name"] !== "string" || s["name"].length === 0)
|
|
112
|
+
return "skill.name must be a non-empty string";
|
|
113
|
+
if (typeof s["description"] !== "string")
|
|
114
|
+
return "skill.description must be a string";
|
|
116
115
|
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
117
116
|
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)`;
|
|
118
117
|
}
|
|
@@ -525,6 +524,13 @@ export function createHttpServer(rawDeps) {
|
|
|
525
524
|
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)");
|
|
526
525
|
return null;
|
|
527
526
|
}
|
|
527
|
+
{
|
|
528
|
+
const bodyKeyIssue = taskBodyKeyIssue(body);
|
|
529
|
+
if (bodyKeyIssue) {
|
|
530
|
+
sendError(res, 400, "request.body_shape", bodyKeyIssue.message, { unknownKeys: bodyKeyIssue.unknownKeys, unsupportedKeys: bodyKeyIssue.unsupportedKeys });
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
528
534
|
if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
|
|
529
535
|
sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
|
|
530
536
|
return null;
|
|
@@ -644,6 +650,20 @@ export function createHttpServer(rawDeps) {
|
|
|
644
650
|
sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
|
|
645
651
|
return null;
|
|
646
652
|
}
|
|
653
|
+
if (st.skillListingBudgetFraction !== undefined) {
|
|
654
|
+
const v = st.skillListingBudgetFraction;
|
|
655
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0 || v > 1) {
|
|
656
|
+
sendError(res, 400, "request.field_invalid", "settings.skillListingBudgetFraction must be a number greater than 0 and at most 1");
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (st.skillListingMaxDescChars !== undefined) {
|
|
661
|
+
const v = st.skillListingMaxDescChars;
|
|
662
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) {
|
|
663
|
+
sendError(res, 400, "request.field_invalid", "settings.skillListingMaxDescChars must be a positive integer");
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
647
667
|
if (st.hooks !== undefined && st.hooks !== null) {
|
|
648
668
|
const hooksParsed = parseHooksConfig(st.hooks);
|
|
649
669
|
if (!hooksParsed.config) {
|
|
@@ -1355,7 +1375,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1355
1375
|
if (deps.toolApproval && taskId) {
|
|
1356
1376
|
const toolApproval = deps.toolApproval;
|
|
1357
1377
|
resumeTaskConfig.onAsk = approvalLeg.windowZero
|
|
1358
|
-
? () => Promise.resolve(toolApproval.unattendedAskOutcome())
|
|
1378
|
+
? (askReq) => Promise.resolve(toolApproval.unattendedAskOutcome({ taskId, owner: principal ?? null, toolName: askReq.toolName }))
|
|
1359
1379
|
: toolApproval.boundAsk({
|
|
1360
1380
|
owner: principal ?? null,
|
|
1361
1381
|
taskId,
|
|
@@ -1532,6 +1552,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1532
1552
|
const startSeq = rs && taskId ? await rs.maxSeq(taskId) : 0;
|
|
1533
1553
|
let text = "";
|
|
1534
1554
|
let reasoning = "";
|
|
1555
|
+
let reasoningEventId;
|
|
1535
1556
|
const persistThinking = deps.config.traceThinking;
|
|
1536
1557
|
const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
|
|
1537
1558
|
const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
|
|
@@ -1555,8 +1576,9 @@ export function createHttpServer(rawDeps) {
|
|
|
1555
1576
|
const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
|
|
1556
1577
|
const flush = async () => {
|
|
1557
1578
|
if (reasoning) {
|
|
1558
|
-
await append("reasoning", modelTextEventData({ text: reasoning }));
|
|
1579
|
+
await append("reasoning", modelTextEventData({ text: reasoning, eventId: reasoningEventId }));
|
|
1559
1580
|
reasoning = "";
|
|
1581
|
+
reasoningEventId = undefined;
|
|
1560
1582
|
}
|
|
1561
1583
|
if (text) {
|
|
1562
1584
|
await append("text", modelTextEventData({ text, eventId: anchor.firstTextEventId }));
|
|
@@ -1632,10 +1654,14 @@ export function createHttpServer(rawDeps) {
|
|
|
1632
1654
|
text += ev.delta;
|
|
1633
1655
|
anchor.onText(ev.eventId);
|
|
1634
1656
|
break;
|
|
1635
|
-
case "reasoning_delta":
|
|
1636
|
-
|
|
1637
|
-
|
|
1657
|
+
case "reasoning_delta": {
|
|
1658
|
+
const rb = reasoningDeltaBytes(ev);
|
|
1659
|
+
if (persistThinking && rb !== undefined) {
|
|
1660
|
+
reasoning += rb;
|
|
1661
|
+
reasoningEventId ??= reasoningSegmentEventId(ev.eventId);
|
|
1662
|
+
}
|
|
1638
1663
|
break;
|
|
1664
|
+
}
|
|
1639
1665
|
case "tool_start":
|
|
1640
1666
|
anchor.onTool();
|
|
1641
1667
|
await flush();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** 拒体里逐字回显的键名条数上限。键名原样回显(不猜语义),但**条数有界** —— 一个塞满键的体不许把 400 响应
|
|
2
|
+
* 放大成流量面。超出部分只报数。 */
|
|
3
|
+
export declare const MAX_KEY_ECHO = 20;
|
|
4
|
+
/** 键名回显的清洗:剥控制/双向字符(键名进的是 JSON 错误体与运维日志,一个 RLO 能把两行字读反),
|
|
5
|
+
* 再按 {@link MAX_KEY_ECHO_CHARS} 截断。清洗之外**逐字**,不做大小写/近似猜测。 */
|
|
6
|
+
export declare function echoKeyName(prefix: string, key: string): string;
|
|
7
|
+
/** 一次键闭集裁决的产物:一句 wire 文案 + 两张分类键名表(消费端可机读,不必解析文案)。 */
|
|
8
|
+
export interface KeyClosureIssue {
|
|
9
|
+
message: string;
|
|
10
|
+
/** 词表外的键(拼错/未来键),全路径形,已清洗+截断+排序+封顶。 */
|
|
11
|
+
unknownKeys: string[];
|
|
12
|
+
/** 上游合约(CC / SDK / 引擎)里合法、但本 service 不在这一面接线的键,同形。 */
|
|
13
|
+
unsupportedKeys: string[];
|
|
14
|
+
}
|
|
15
|
+
/** 逐键分拣:受理集内跳过,`tolerated` 命中进 `unsupported`,其余进 `unknown`。两桶都拒,标签只影响文案。 */
|
|
16
|
+
export declare function scanKeyClosure(bag: object, prefix: string, accepted: ReadonlySet<string>, tolerated: ReadonlySet<string>, into: {
|
|
17
|
+
unknown: string[];
|
|
18
|
+
unsupported: string[];
|
|
19
|
+
}): void;
|
|
20
|
+
/** 一张分类表的文案片段(封顶后只报数)。 */
|
|
21
|
+
export declare function keyClosureClause(names: string[], label: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* FRESH 提交面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
|
|
24
|
+
* 全部受理 ⇒ `null`。
|
|
25
|
+
*
|
|
26
|
+
* 判据边界(与 settings 那只同源,有意窄):只判**键名**;体本身非对象 ⇒ 本门不判(留给既有的
|
|
27
|
+
* 「missing 'objective' string」那道门,免得同一个体被两句话抢答)。
|
|
28
|
+
*/
|
|
29
|
+
export declare function taskBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
|
|
30
|
+
//# sourceMappingURL=request-key-closure.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
2
|
+
export const MAX_KEY_ECHO = 20;
|
|
3
|
+
const MAX_KEY_ECHO_CHARS = 64;
|
|
4
|
+
export function echoKeyName(prefix, key) {
|
|
5
|
+
const clean = key.replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
6
|
+
return `${prefix}${clean.length > MAX_KEY_ECHO_CHARS ? `${clean.slice(0, MAX_KEY_ECHO_CHARS)}…` : clean}`;
|
|
7
|
+
}
|
|
8
|
+
export function scanKeyClosure(bag, prefix, accepted, tolerated, into) {
|
|
9
|
+
for (const k of Object.keys(bag)) {
|
|
10
|
+
if (accepted.has(k))
|
|
11
|
+
continue;
|
|
12
|
+
(tolerated.has(k) ? into.unsupported : into.unknown).push(echoKeyName(prefix, k));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function keyClosureClause(names, label) {
|
|
16
|
+
const shown = names.slice(0, MAX_KEY_ECHO);
|
|
17
|
+
const more = names.length - shown.length;
|
|
18
|
+
return `${label}: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
19
|
+
}
|
|
20
|
+
const TASK_BODY_DECLARED_KEYS = [
|
|
21
|
+
"a2aPeers", "acceptPartialRestore", "additionalDirectories", "additionalReadDirectories", "agents",
|
|
22
|
+
"appendSystemPrompt", "attachmentIds", "attachments", "capabilitiesNeeded", "cascade", "clientContext",
|
|
23
|
+
"compaction", "compactionModel", "council", "cwd", "debate", "deferTools", "enableFork", "excludeTools",
|
|
24
|
+
"finalVerification", "forwardSubagentEvents", "images", "interactiveTools", "jobId", "limits", "maxCostUsd",
|
|
25
|
+
"maxTokens", "mcpServers", "memoryCapture", "memoryWrite", "model", "objective", "oneShot", "outputRetries",
|
|
26
|
+
"outputSchema", "permissionMode", "projectId", "promptProfile", "reasoningEffort", "requireExistingSession",
|
|
27
|
+
"resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
|
|
28
|
+
"rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
|
|
29
|
+
"sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
|
|
30
|
+
"verify", "verifyRounds",
|
|
31
|
+
];
|
|
32
|
+
const TASK_BODY_SCENARIO_KEYS = ["businessContext", "lenses", "repo", "rounds", "team"];
|
|
33
|
+
const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
|
|
34
|
+
const UNSUPPORTED_BODY = new Set(["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "writeFace"]);
|
|
35
|
+
export function taskBodyKeyIssue(raw) {
|
|
36
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
|
|
37
|
+
return null;
|
|
38
|
+
const buckets = { unknown: [], unsupported: [] };
|
|
39
|
+
scanKeyClosure(raw, "", ACCEPTED_BODY, UNSUPPORTED_BODY, buckets);
|
|
40
|
+
if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
buckets.unknown.sort();
|
|
43
|
+
buckets.unsupported.sort();
|
|
44
|
+
const parts = [
|
|
45
|
+
...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a task-request key — check the spelling)")] : []),
|
|
46
|
+
...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (an engine seat this service derives, never reads from the body)")] : []),
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
|
|
50
|
+
`${parts.join("; ")}. ` +
|
|
51
|
+
`Accepted: ${[...ACCEPTED_BODY].sort().join(",")}`,
|
|
52
|
+
unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
|
|
53
|
+
unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=request-key-closure.js.map
|
package/dist/runs.js
CHANGED
|
@@ -95,6 +95,8 @@ export function resumeAtHttpStatus(result) {
|
|
|
95
95
|
return 400;
|
|
96
96
|
if (code === "config.invalid_checkpoint_store")
|
|
97
97
|
return 400;
|
|
98
|
+
if (code === "config.skill_listing_budget_invalid")
|
|
99
|
+
return 400;
|
|
98
100
|
if (code === "config.memory_capture_unsupported")
|
|
99
101
|
return 409;
|
|
100
102
|
if (!code.startsWith("resume_at."))
|
|
@@ -196,7 +198,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
196
198
|
if (approval) {
|
|
197
199
|
const coordinator = approval.coordinator;
|
|
198
200
|
spec.onAsk = approvalLeg.windowZero
|
|
199
|
-
? () => Promise.resolve(coordinator.unattendedAskOutcome())
|
|
201
|
+
? (askReq) => Promise.resolve(coordinator.unattendedAskOutcome({ taskId, owner: elicitOwner ?? owner, toolName: askReq.toolName }))
|
|
200
202
|
: coordinator.boundAsk({
|
|
201
203
|
owner: elicitOwner ?? owner,
|
|
202
204
|
taskId,
|
|
@@ -84,13 +84,27 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
|
|
|
84
84
|
*/
|
|
85
85
|
export declare function compileCommandPolicy(rules: CommandRule[] | undefined): ToolPolicy | undefined;
|
|
86
86
|
/**
|
|
87
|
-
* Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the
|
|
88
|
-
* - `read-only` / `plan` → `handsReadOnly: true` (read + propose, never mutate; the
|
|
89
|
-
* boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan
|
|
90
|
-
* is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands.
|
|
87
|
+
* Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
|
|
88
|
+
* - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
|
|
89
|
+
* §6 verifier read-only boundary). `plan` shares the SAME safety boundary as `read-only`; the "present a plan
|
|
90
|
+
* for approval" intent is higher up (UX), not a distinct TaskSpec safety field, so both map to read-only hands.
|
|
91
91
|
* - `ask` → `shellGate: "always"` (every `Bash` command tightens to an `irreversible_ask` durable suspend —
|
|
92
92
|
* the fail-closed default for an unattended deployment with no parsed classifier).
|
|
93
93
|
* - `auto` / `undefined` → `{}` (no extra tightening; still subject to the deployment baseline + commandPolicy).
|
|
94
|
+
*
|
|
95
|
+
* 🔴 **为什么只读那两档同时说 `writeFace:"roots"`**([ref] 随批;不是补丁,是把这一档的表态说全):
|
|
96
|
+
* core 7.18.0 的 `resolveContainmentFace` 第 1 行把「只读(verifier)挂载 × 任务层 `writeFace:"open"`」
|
|
97
|
+
* 判成**真实矛盾**并**抛** `config.write_face_readonly_conflict`(理由逐字:那种挂载根本不带写面,
|
|
98
|
+
* 声明一个开放写面 = 为一次调用没有的工具表态)。而 `permissionMode:"bypassPermissions"` 自 [ref] 起
|
|
99
|
+
* 会在阶段④铸出 `writeFace:"open"` —— 于是「`AUTONOMY=read-only` 的部署 × 一个 bypass 请求」这一格
|
|
100
|
+
* 不修就是**整条 run 当场配置错误失败**。
|
|
101
|
+
*
|
|
102
|
+
* 修在这里而不是在写点加一条「若 handsReadOnly 则别写」的判据:两个字段**在这一档里是同一件事**
|
|
103
|
+
* (「这条 run 没有写面」),而 `tightenTaskSpec` 本来就把 `writeFace` 当 tighten-only 安全字段收
|
|
104
|
+
* (`base:"open"` × `override:"roots"` ⇒ `"roots"`,反向 throw)—— 用既有的合并表表达它,规则数不变;
|
|
105
|
+
* 在写点加判据则是让阶段④去读一个它不拥有的治理事实,那才是第二个判官。
|
|
106
|
+
* 与 `ask → shellGate:"always"` 同姿势:这一档**自己就是**一次「围栏在」的表态,所以无表态的请求上
|
|
107
|
+
* 它照样把键写出来(不是「只在有 open 时才压」——那又是一条判据)。
|
|
94
108
|
*/
|
|
95
109
|
export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial<TaskSpec>;
|
|
96
110
|
/**
|
|
@@ -85,7 +85,7 @@ export function autonomyOverrides(autonomy) {
|
|
|
85
85
|
switch (autonomy) {
|
|
86
86
|
case "read-only":
|
|
87
87
|
case "plan":
|
|
88
|
-
return { handsReadOnly: true };
|
|
88
|
+
return { handsReadOnly: true, writeFace: "roots" };
|
|
89
89
|
case "ask":
|
|
90
90
|
return { shellGate: "always" };
|
|
91
91
|
case "auto":
|
|
@@ -163,7 +163,7 @@ export function applyRuntimeGovernance(base, governance) {
|
|
|
163
163
|
const shellObserver = createGovernanceShellGateMarkPolicy(marksOf);
|
|
164
164
|
overrides.toolPolicy = governancePolicy === undefined ? shellObserver : combinePolicies(governancePolicy, shellObserver);
|
|
165
165
|
}
|
|
166
|
-
if (
|
|
166
|
+
if (Object.keys(overrides).length === 0) {
|
|
167
167
|
return base;
|
|
168
168
|
}
|
|
169
169
|
return tightenTaskSpec(base, overrides);
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -38,7 +38,9 @@ export declare function normalizeAttachments(v: unknown): TaskSpec["attachments"
|
|
|
38
38
|
export declare function normalizeResumeAtMode(v: unknown, resumeAtResolved: boolean): "at" | "before" | undefined;
|
|
39
39
|
/** [ref] 防御上限:一份请求最多带多少个 agent 定义。core 侧无上限;这里封一个防御值 —— 每个定义都进
|
|
40
40
|
* 系统提示(whenToUse/skills/systemPrompt 全是 prompt 载荷),不设界就是 per-turn token 成本洞(与
|
|
41
|
-
*
|
|
41
|
+
* 上限)。32 足够覆盖 CC 式 `.claude/agents/` 全家桶还有余量。⚠️ 这条帽守的是 **agent 定义**的条数
|
|
42
|
+
* (每个定义的 whenToUse/systemPrompt 都是无条件进系统提示的 prompt 载荷),与 [ref] 退役的
|
|
43
|
+
* **技能**条数帽不是同一轴:技能清单由引擎按预算渲染(超了裁描述),agent 定义没有那个预算面。 */
|
|
42
44
|
export declare const MAX_TASK_AGENTS = 32;
|
|
43
45
|
/** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
|
|
44
46
|
* 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
|
package/dist/spec-fields.js
CHANGED
|
@@ -58,7 +58,6 @@ const MAX_AGENT_TEXT_CHARS = 4_096;
|
|
|
58
58
|
const MAX_AGENT_SYSTEM_PROMPT_CHARS = 65_536;
|
|
59
59
|
const MAX_AGENT_TOOLS = 64;
|
|
60
60
|
const MAX_AGENT_TOOL_NAME_CHARS = 128;
|
|
61
|
-
const MAX_AGENT_SKILLS = 10;
|
|
62
61
|
const MAX_AGENT_SKILL_CONTENT_CHARS = 1_048_576;
|
|
63
62
|
const MAX_AGENT_MAX_TURNS = 1_000;
|
|
64
63
|
const boundedString = (max) => (v) => typeof v === "string" && v.length <= max;
|
|
@@ -73,9 +72,9 @@ const skillOk = (s) => {
|
|
|
73
72
|
if (k !== "name" && k !== "description" && k !== "content" && k !== "manifest" && k !== "files")
|
|
74
73
|
return false;
|
|
75
74
|
}
|
|
76
|
-
if (typeof s.name !== "string" || s.name.length === 0
|
|
75
|
+
if (typeof s.name !== "string" || s.name.length === 0)
|
|
77
76
|
return false;
|
|
78
|
-
if (typeof s.description !== "string"
|
|
77
|
+
if (typeof s.description !== "string")
|
|
79
78
|
return false;
|
|
80
79
|
if (typeof s.content !== "string" || s.content.length === 0 || s.content.length > MAX_AGENT_SKILL_CONTENT_CHARS)
|
|
81
80
|
return false;
|
|
@@ -135,8 +134,8 @@ const TASK_AGENT_FIELD_SHAPES = {
|
|
|
135
134
|
allowTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
|
|
136
135
|
denyTools: { desc: `an array of at most ${MAX_AGENT_TOOLS} non-empty strings of at most ${MAX_AGENT_TOOL_NAME_CHARS} characters each`, ok: toolList },
|
|
137
136
|
skills: {
|
|
138
|
-
desc:
|
|
139
|
-
ok: (v) => Array.isArray(v) && v.
|
|
137
|
+
desc: "an array of { name, description, content } skill objects (optional manifest/files; files entries need { path, content }) — no count cap; the engine renders the listing within its own byte budget",
|
|
138
|
+
ok: (v) => Array.isArray(v) && v.every(skillOk),
|
|
140
139
|
},
|
|
141
140
|
background: { desc: "a boolean", ok: (v) => typeof v === "boolean" },
|
|
142
141
|
isolation: { desc: '"worktree"', ok: (v) => v === "worktree" },
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider } from "@sema-agent/core";
|
|
39
39
|
import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
|
|
40
|
+
import { type KeyClosureIssue } from "./request-key-closure.js";
|
|
40
41
|
/** The permission MODE a client may request (SDK `SettingsPermissions.defaultMode`), the [ref]/[ref] five-word
|
|
41
42
|
* table. ⚠️ This is a DELIBERATE deployment set, NOT a copy of any CC set ([ref] P2-1 corpus check): CC's own
|
|
42
43
|
* full enum is SIX words (= ours + `dontAsk`), CC's managed/remote form accepts FOUR (= ours − `bypassPermissions`,
|
|
@@ -160,6 +161,13 @@ export interface ParsedTaskSettings {
|
|
|
160
161
|
* 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
|
|
161
162
|
* core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
|
|
162
163
|
hooks?: HooksConfig;
|
|
164
|
+
/** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
|
|
165
|
+
* 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
|
|
166
|
+
* 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。
|
|
167
|
+
* server 零加工透传到 `TaskSpec`,默认值属主是引擎(缺席 ⇒ 整键不写)。 */
|
|
168
|
+
skillListingBudgetFraction?: number;
|
|
169
|
+
/** 同族第二键:每条技能描述在清单里的字符帽(正整数;CC 默认 1536)。超出由引擎**截断**,不是拒。 */
|
|
170
|
+
skillListingMaxDescChars?: number;
|
|
163
171
|
}
|
|
164
172
|
/** The result of parsing an untrusted `body.settings`: the applied subset + the names of present-but-deferred
|
|
165
173
|
* fields (so the caller can log/advertise them — no silent drop). `null` settings ⇒ `{ settings: undefined }`. */
|
|
@@ -180,10 +188,15 @@ export declare const MAX_SETTINGS_ENV_VARS = 256;
|
|
|
180
188
|
export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768;
|
|
181
189
|
export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
|
|
182
190
|
/** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
|
|
183
|
-
* `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode`
|
|
184
|
-
*
|
|
185
|
-
* 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。
|
|
186
|
-
|
|
191
|
+
* `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在
|
|
192
|
+
* {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(`webSearchConfigFromSettings`,单用户闸)。
|
|
193
|
+
* 加键必须同时加消费点,否则就是往闭集里塞一个新的静默丢。
|
|
194
|
+
*
|
|
195
|
+
* 🔴 两个 `skillListing*`([ref],7.77.0)= **CC settings 同名键**,值域抄 CC 的 schema 声明
|
|
196
|
+
* (`skillListingBudgetFraction` 是 `gt(0).lte(1)`、`skillListingMaxDescChars` 是 `int().positive()`);
|
|
197
|
+
* 消费点 = 本文件的 parse + `boot/resolve-spec.ts` 的 `TaskSpec` 透传。语义(预算怎么花、描述怎么裁、
|
|
198
|
+
* 溢出怎么退成 name-only)整个归引擎,本仓一个字节都不渲染,也**不复刻默认值**——缺席即整键不写。 */
|
|
199
|
+
export declare const TASK_SETTINGS_KEYS: readonly ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
|
|
187
200
|
/** 本 service 消费的 `settings.permissions` 子键 —— 闭集(值形校验另有既有门,本门只判键名)。 */
|
|
188
201
|
export declare const TASK_SETTINGS_PERMISSION_KEYS: readonly ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
|
|
189
202
|
/** CC/SDK 合约里合法、本 service **不接线**的顶层键。词表外的 CC 键落 `unknown` —— 两列都是 400,标签只影响
|
|
@@ -202,17 +215,6 @@ export declare const TASK_SETTINGS_UNSUPPORTED_KEYS: readonly ["apiKeyHelper", "
|
|
|
202
215
|
/** 同上,`settings.permissions` 子层(出处 = SDK `SettingsPermissions` 的三个声明键)。
|
|
203
216
|
* `additionalDirectories` 有**同请求替代面**:顶层 `body.additionalDirectories`([ref],resolve-spec 真消费)。 */
|
|
204
217
|
export declare const TASK_SETTINGS_UNSUPPORTED_PERMISSION_KEYS: readonly ["additionalDirectories", "disableBypassPermissionsMode"];
|
|
205
|
-
/** 拒体里逐字回显的键名条数上限。键名原样回显(不猜语义),但**条数有界** —— 一个塞满键的体不许把 400 响应
|
|
206
|
-
* 放大成流量面。超出部分只报数。 */
|
|
207
|
-
export declare const MAX_SETTINGS_KEY_ECHO = 20;
|
|
208
|
-
/** {@link taskSettingsKeyIssue} 的拒体:一句 wire 文案 + 两张分类键名表(消费端可机读,不必解析文案)。 */
|
|
209
|
-
export interface TaskSettingsKeyIssue {
|
|
210
|
-
message: string;
|
|
211
|
-
/** 词表外的键(拼错/未来键),`settings.x` / `settings.permissions.x` 全路径形,已清洗+截断+排序+封顶。 */
|
|
212
|
-
unknownKeys: string[];
|
|
213
|
-
/** CC/SDK 合约合法但本 service 不接线的键,同形。 */
|
|
214
|
-
unsupportedKeys: string[];
|
|
215
|
-
}
|
|
216
218
|
/**
|
|
217
219
|
* FRESH 提交面的 `settings` 键闭集裁决:顶层或 `permissions` 子层出现受理集之外的键 ⇒ 返回拒体(调用方 400
|
|
218
220
|
* `request.body_shape`);全部受理 ⇒ `null`。
|
|
@@ -224,7 +226,7 @@ export interface TaskSettingsKeyIssue {
|
|
|
224
226
|
* 「settings must be an object」/「settings.permissions must be an object」两道门,免得同一个体被两句话抢答。
|
|
225
227
|
* · `permissions: null` 是既有钉住的「容器缺席」形,照旧不咬。
|
|
226
228
|
*/
|
|
227
|
-
export declare function taskSettingsKeyIssue(raw: unknown):
|
|
229
|
+
export declare function taskSettingsKeyIssue(raw: unknown): KeyClosureIssue | null;
|
|
228
230
|
/**
|
|
229
231
|
* Validate an untrusted `body.settings` (raw `unknown` off the wire) into the service-trusted {@link
|
|
230
232
|
* ParsedTaskSettings}. NEVER trusts the wire type — every field is shape-checked. `defaultMode` accepts the FIVE
|