@sema-agent/server 7.78.2 → 7.79.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/auto-mode-face.d.ts +23 -11
- package/dist/auto-mode-face.js +5 -2
- package/dist/boot/deferred-sandbox-path-env.d.ts +47 -0
- package/dist/boot/deferred-sandbox-path-env.js +1 -5
- package/dist/boot/resolve-spec.js +21 -2
- package/dist/capabilities/code-review-council.js +2 -1
- package/dist/config.js +4 -3
- package/dist/env-facts.d.ts +61 -0
- package/dist/env-facts.js +32 -6
- package/dist/execution-lane-caps.d.ts +12 -0
- package/dist/execution-lane-caps.js +8 -0
- 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 +96 -0
- package/dist/http/dispatch.js +499 -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/server.d.ts +19 -104
- package/dist/http/server.js +23 -2589
- package/dist/main.js +6 -3
- package/dist/model-select.d.ts +4 -0
- package/dist/model-select.js +2 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/plugins/file-history-store-sql.js +12 -11
- package/dist/plugins/remote-env-adb.d.ts +11 -0
- package/dist/plugins/remote-env-adb.js +1 -1
- package/dist/plugins/remote-env-device.d.ts +11 -0
- package/dist/plugins/remote-env-device.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +11 -0
- package/dist/plugins/remote-env-host.js +1 -1
- package/dist/plugins/remote-env-k8s.d.ts +11 -0
- package/dist/plugins/remote-env-k8s.js +1 -1
- package/dist/plugins/remote-env-ssh.d.ts +11 -0
- package/dist/plugins/remote-env-ssh.js +1 -1
- package/dist/run-local.js +2 -1
- package/dist/task-cwd.d.ts +24 -0
- package/dist/task-cwd.js +9 -0
- package/package.json +2 -2
package/dist/auto-mode-face.d.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type RunnerDeps, type RuntimeCaps } from "@sema-agent/core";
|
|
2
2
|
import type { SettingsPermissionMode } from "./task-settings.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* 分类器席的模型 —— **调 core 的单一解析器** `classifierSeatModel({deps, main})`(core 7.20.0 / [ref],
|
|
5
|
+
* 本仓 [ref] 的跨仓诉求兑现;契约句 `classifier.seat.one_resolver` 逐字:引擎的武装点与**显示这个席**
|
|
6
|
+
* 的宿主读面调同一只导出函数,而不是各自从目录/角色/档位表再推一遍 —— 第二份推导就是第二个答案)。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **S-350:本仓的自算分支已删**。修前这里调的是 `resolveTaskModel({modelRole:"classifier"})`,那只
|
|
9
|
+
* 回答的是「classifier **角色**解到谁」;引擎真正挑席时在角色没点名模型的情况下还要走 **tierBelowMain**
|
|
10
|
+
* (主模型档位**严格之下**、路由不同的最近一档)。两者在「档位表在场 + 没有 classifier 角色」这一形上
|
|
11
|
+
* 给出**不同的模型**(车FL U-3 坐实),读面于是对着 `sema doctor permissions` 报另一只模型。规则变少的
|
|
12
|
+
* 修法只有一条:把这个问题整个交回给上游的那一只解析器。
|
|
13
|
+
*
|
|
14
|
+
* 递进去的 `deps` 就是**交给 `Runner` 的那一只**(main.ts 递 `runnerDeps`):`models`/`roles`/`tiers` 三席
|
|
15
|
+
* 与引擎看到的是同一批**活引用**(center 原地热更新 ⇒ 读面按请求现算)。解析器对**原始**与**已展开**
|
|
16
|
+
* 两形 deps 同答(它自己 `normalizeSeatDeps` 一次 `expandTiers`),且**永不抛**。
|
|
17
|
+
*
|
|
18
|
+
* 唯一还会抛的是 `main` —— **这条 run 的主模型**,也就是本仓给一条没选模型的 task 铸的那个 ref
|
|
19
|
+
* ({@link DEFAULT_MODEL_REF};`boot/resolve-spec.ts` 每条 spec 都盖 `spec.model`,所以引擎眼里的 main
|
|
20
|
+
* 恒是它解出来的那一只,**不是** core 的 `default` **角色**链)。席是相对主模型定义的(tierBelowMain =
|
|
21
|
+
* 「主模型档位之下」),主模型解不出就没有席可言 ⇒ `{id:undefined, fault}`(core 的拒句当数据交出,读面
|
|
22
|
+
* 不写 model 键):本面是纯披露、不是执法——读面 500 掉整张能力表比少一个 model 键更伤。`main` 这一步
|
|
23
|
+
* 喂**展开**目录,与引擎构造期 `expandTiers(models, tiers)` 之后再解逐字同源(邻居惯例:
|
|
24
|
+
* `routes/capabilities.ts` 的 `workflowModels` / `/v1/models` 两处同法)。
|
|
10
25
|
*/
|
|
11
|
-
export declare function classifierRouteFor(deps:
|
|
12
|
-
models?: Record<string, Model>;
|
|
13
|
-
roles?: ModelRoles;
|
|
14
|
-
}): ClassifierRoute;
|
|
26
|
+
export declare function classifierRouteFor(deps: Pick<RunnerDeps, "models" | "roles" | "tiers">): ClassifierRoute;
|
|
15
27
|
/** `classifierRouteFor` 的答:解出 ⇒ `{id}`;解不出 ⇒ `{id:undefined, fault}`(core 的拒句原文,不吞)。 */
|
|
16
28
|
export type ClassifierRoute = {
|
|
17
29
|
readonly id: string;
|
package/dist/auto-mode-face.js
CHANGED
|
@@ -2,10 +2,13 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { resolveTaskModel } from "@sema-agent/core";
|
|
5
|
+
import { classifierSeatModel, expandTiers, resolveTaskModel } from "@sema-agent/core";
|
|
6
|
+
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
6
7
|
export function classifierRouteFor(deps) {
|
|
7
8
|
try {
|
|
8
|
-
|
|
9
|
+
const expanded = expandTiers(deps.models, deps.tiers ?? {}) ?? deps.models;
|
|
10
|
+
const main = resolveTaskModel({ model: DEFAULT_MODEL_REF }, { ...deps, ...(expanded !== undefined ? { models: expanded } : {}) }).model;
|
|
11
|
+
return { id: classifierSeatModel({ deps, main }).model.id };
|
|
9
12
|
}
|
|
10
13
|
catch (err) {
|
|
11
14
|
return { id: undefined, fault: err instanceof Error ? err.message : String(err) };
|
|
@@ -1,3 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [ref] —— 沙箱 lane(e2b/k8s/ssh/adb/local-docker)写门的**真 env 裁决**接法。
|
|
3
|
+
*
|
|
4
|
+
* ## 缺口与它的形状
|
|
5
|
+
*
|
|
6
|
+
* core 的 `createFsWriteGatePolicy` / `createSensitivePathPolicy` 把每一个写目标交给
|
|
7
|
+
* `canonicalizeTarget(env, …)` 去问 fs 要真身(`absolutePath` → `exists` → `canonicalPath` →
|
|
8
|
+
* `fileInfo`/`readLink` 逐跳,dist 亲读)。host lane 上那个 env 就是 hand 工具真正写的那块盘。
|
|
9
|
+
* 沙箱 lane 不是:per-task 的沙箱 env 由 core 的 `executionEnvFactory` 在 spec **之后**才铸,
|
|
10
|
+
* 而写门 wiring 必须在 spec 期就交出去。
|
|
11
|
+
*
|
|
12
|
+
* 出路不是把裁决搬去 hook 层(那要在本仓复刻 `canonicalizeTarget` 的逐级 exists / symlink 解析 /
|
|
13
|
+
* 段匹配 / 折叠序——复制 core 的裁决逻辑=同源谎),而是利用一个部署事实:**那个 per-task 沙箱 env
|
|
14
|
+
* 本来就是本仓铸的**。于是本模块给写门一个 ExecutionEnv **代理**:它在**调用时刻**(runToolGate 裁决,
|
|
15
|
+
* 必在 prepare 之后)去 slot 里取该 task 的真 env 并转发 fs 读原语。call-time 转发消除了时序问题,
|
|
16
|
+
* 与「从调用上下文读 env」是同一性质。
|
|
17
|
+
*
|
|
18
|
+
* ## 三条不可动的裁定
|
|
19
|
+
*
|
|
20
|
+
* 1. **`absolutePath` 一面不转发**,沿用词法规则(POSIX 绝对形归一 / NUL 拒 / 非绝对形 err)。
|
|
21
|
+
* 真 env 的 `absolutePath` 会按 **env 自己的 cwd** 解析相对路径,而工具真写处按 engine 跟踪的
|
|
22
|
+
* cwd 解析——两个基准不同,转发它等于拿错基准铸 canonical key,而**错误的 allow 比没有门更糟**。
|
|
23
|
+
* 相对形 / `~` / 盘符形因此照旧 err ⇒ `canonicalizeTarget` 失败 ⇒ 写门 `ask`(fail-closed)。
|
|
24
|
+
* 「不猜沙箱工作目录」这条公理不随真 env 到场而松。
|
|
25
|
+
* 2. **slot 空 = fail-closed 且响亮**:转发面返回错误 ⇒ core 把 `exists` 报错读成
|
|
26
|
+
* `unresolvedSymlink` ⇒ `createSensitivePathPolicy` 判 deny、写门判 ask,折叠后 deny 恒赢。
|
|
27
|
+
* 保护型缺席的失败方向必须与能力语义同向。留痕走结构化 `warn`(每实例一次)而**不**走
|
|
28
|
+
* `observability/fail-open.ts`:那个词表按其模块头是 `F`(体验/缓存回退)与 `P-DEBT`(明知方向
|
|
29
|
+
* 不对的保护型欠账)两类的准入面,本臂方向是**对的**(fail-closed),登记进去会把「已收口的
|
|
30
|
+
* 保护臂」混进「待还的债」计数里。
|
|
31
|
+
* 3. **slot 只认「当前活腿」**:同一 session 同时只有一条活腿(HTTP `conflict.session_active_run` +
|
|
32
|
+
* core session acquire 双重串行化;委派子代另用一个 sessionId),所以同键改铸即顶替旧登记——
|
|
33
|
+
* 那些旧登记来自 park-only 之类「不 destroy 也不 suspend」的终局,留着只会让 resume 腿失去真身裁决。
|
|
34
|
+
*
|
|
35
|
+
* ## slot 的键 = sessionId
|
|
36
|
+
*
|
|
37
|
+
* 工厂 ctx 只有 `{sessionId, taskId?, isolation?, parentCwd?}`(core `ExecutionEnvFactoryContext`),
|
|
38
|
+
* 而 `/v1/runs` 会在 resolveSpec **之后**铸自己的持久 taskId 覆盖 `spec.taskId`,所以 taskId 在主路径
|
|
39
|
+
* 上对不上——sessionId 是两侧都稳的那一个(per-task 镜像登记簿是同一个先例)。
|
|
40
|
+
* 委派子任务(Agent / workflow 子代)拿的是**另一个** sessionId,所以父的 slot 不会被子代覆盖(「不许
|
|
41
|
+
* 最后写赢」由此成立)。**残余面**:子代的写仍按**父**的 env 裁决——子代的 policy 就是父的那一个实例
|
|
42
|
+
* (core `inheritedGateForChildren` 把它当 parentConstraint 继承),而 policy 收到的只有
|
|
43
|
+
* `{toolName, args, toolCallId}`,读侧根本没有腿身份可键控。方向上它逼近真相而非远离(过渡形是「谁的盘
|
|
44
|
+
* 都不看」),但有一条**新的松面**:父盘上一个名叫守卫段、真身良性的软链,会让子代在自己盘上指向真守卫段
|
|
45
|
+
* 的同名路径拿到父盘的宽松答案。收口需要一条「按当前工具调用的 env 裁决」的引擎缝,属设计件;
|
|
46
|
+
* 该面由 test/task-settings.test.ts 的「真身胜过名字」特征化钉机器可见。
|
|
47
|
+
*/
|
|
1
48
|
import { FileError, StubExecutionEnv, type ExecutionEnv, type ExecutionEnvFactory, type FileInfo, type Result } from "@sema-agent/core";
|
|
2
49
|
/** 哪些 lane 用本模块的代理裁决写门 —— `REMOTE_EXEC` 未设(进程内 host)与显式 `host` 之外的全部。
|
|
3
50
|
* 与 resolve-spec 的 `hostSemanticsLane` 是同一判别式的两面,取值处**只此一个**(两处各写一份正是漂移的成因)。 */
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { posix } from "node:path";
|
|
2
1
|
import { FileError, StubExecutionEnv, err, ok } from "@sema-agent/core";
|
|
2
|
+
import { normalizeAbsolutePathLexically } from "../task-cwd.js";
|
|
3
3
|
export function isSandboxPathAdjudicationLane(provider) {
|
|
4
4
|
return provider !== undefined && provider !== "host";
|
|
5
5
|
}
|
|
6
|
-
function normalizeAbsolutePathLexically(path) {
|
|
7
|
-
const collapsed = posix.normalize(path.replace(/^\/+/, "/"));
|
|
8
|
-
return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
|
|
9
|
-
}
|
|
10
6
|
const MAX_BOUND_SESSIONS = 4096;
|
|
11
7
|
export class SandboxPathEnvSlots {
|
|
12
8
|
bySession = new Map();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, deriveAskEffective, expandTiers, resolveAskSeamForm, tightenTaskSpec } from "@sema-agent/core";
|
|
3
|
+
import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, WRITE_EXEMPT_DIR_COVERS_ROOT_CODE, deriveAskEffective, expandTiers, resolveAskSeamForm, tightenTaskSpec } from "@sema-agent/core";
|
|
4
4
|
import { cappedCeiling } from "../budget.js";
|
|
5
5
|
import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
|
|
6
6
|
import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
|
|
@@ -10,7 +10,7 @@ import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.j
|
|
|
10
10
|
import { shouldDiscloseNoPersistentMemory, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
|
|
11
11
|
import { applyLongtailDefer } from "../capabilities/tool-defer.js";
|
|
12
12
|
import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
|
|
13
|
-
import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane } from "../env-facts.js";
|
|
13
|
+
import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane, scratchpadCoveringTaskRoot } from "../env-facts.js";
|
|
14
14
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
15
15
|
import { composeHooks, createTaskHooks } from "../hooks/hook-runner.js";
|
|
16
16
|
import { headerStr } from "../http/principal-gate.js";
|
|
@@ -46,6 +46,19 @@ export function createResolveSpec(ctx) {
|
|
|
46
46
|
assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
|
|
47
47
|
if (opts?.leg === "fresh")
|
|
48
48
|
assertRequestMcpContentOrigin(body.mcpServers);
|
|
49
|
+
if (opts?.leg === "fresh") {
|
|
50
|
+
const conflict = scratchpadCoveringTaskRoot({
|
|
51
|
+
cwd: body.cwd,
|
|
52
|
+
shellScratchpadDir: body.scratchpadDir,
|
|
53
|
+
localDataRoot: config.localDataRoot ?? localRoot,
|
|
54
|
+
sessionId: auth?.sessionId,
|
|
55
|
+
requirePrincipal: config.requirePrincipal,
|
|
56
|
+
cwdHonored: cwdHonored(config),
|
|
57
|
+
});
|
|
58
|
+
if (conflict) {
|
|
59
|
+
throw new HttpError(400, `cwd "${conflict.cwd.asSpelled}" is inside this session's scratchpad "${conflict.scratchpadDir.asSpelled}" — the scratchpad is the fs-write gate's EXEMPT directory, so a task rooted there would auto-approve every write under its own root and the engine refuses to build the gate at all (config.write_exempt_dir_covers_root). Point cwd at the project directory instead (or send no cwd and let the session keep the one it has): this session registers the cwd you send, so a task that happens not to mount the write gate would still leave every LATER task on this session rooted in the exemption. A workflow that really is meant to write without prompts says so with a knob that means it (a permission mode, an open write face) and still keeps its root outside the scratchpad — those knobs do not lift this refusal, because the conflict is between two fields of this request, not a permission level.`, { code: "request.field_conflict", extra: { cwd: conflict.cwd, scratchpadDir: conflict.scratchpadDir } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
49
62
|
const rawMemoryCapture = body.memoryCapture;
|
|
50
63
|
if (rawMemoryCapture !== undefined && rawMemoryCapture !== "off") {
|
|
51
64
|
throw new HttpError(400, "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)", { code: "request.field_invalid" });
|
|
@@ -507,6 +520,12 @@ export function createResolveSpec(ctx) {
|
|
|
507
520
|
governed = applyTaskSettings(governedBase, effectiveSettings, fsWriteGate, workflowGate);
|
|
508
521
|
}
|
|
509
522
|
catch (e) {
|
|
523
|
+
if (e.code === WRITE_EXEMPT_DIR_COVERS_ROOT_CODE) {
|
|
524
|
+
throw new HttpError(400, e.message, {
|
|
525
|
+
code: "request.field_conflict",
|
|
526
|
+
...(e.detail !== undefined ? { extra: { writeExemptDirCoversRoot: e.detail } } : {}),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
510
529
|
throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
|
|
511
530
|
}
|
|
512
531
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { DEFAULT_MODEL_REF } from "../model-select.js";
|
|
2
3
|
import { runTeamDiscussion } from "@sema-agent/core";
|
|
3
4
|
import { REVIEW_LENSES, GROUNDING, LENS_SYSTEM_TPL, DEBATE_MEMBER_TPL, DEBATE_TOPIC_TPL, ARBITER_SYSTEM, ARBITER_FALLBACK_HEADER } from "./prompts/code-review.js";
|
|
4
5
|
const LENSES = REVIEW_LENSES;
|
|
@@ -79,7 +80,7 @@ async function runConvergence(subRunner, repoTools, objective, findings, opts, s
|
|
|
79
80
|
if (opts.debate) {
|
|
80
81
|
const team = await (opts.teamFn ?? runTeamDiscussion)({
|
|
81
82
|
runner: subRunner,
|
|
82
|
-
model:
|
|
83
|
+
model: DEFAULT_MODEL_REF,
|
|
83
84
|
members: lenses.map((l) => ({ role: l.name, systemPrompt: debateMemberSystem(l) })),
|
|
84
85
|
topic: debateTopic(objective, findings),
|
|
85
86
|
rounds: opts.rounds ?? 1,
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -872,14 +873,14 @@ function parseModelDomain() {
|
|
|
872
873
|
const models = { default: model, [model.id]: model };
|
|
873
874
|
if (cheapModel)
|
|
874
875
|
models[cheapModel.id] = cheapModel;
|
|
875
|
-
const cheapRef = cheapModel ? cheapModel.id :
|
|
876
|
+
const cheapRef = cheapModel ? cheapModel.id : DEFAULT_MODEL_REF;
|
|
876
877
|
const verifierEnv = process.env.MODEL_VERIFIER;
|
|
877
|
-
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv :
|
|
878
|
+
const verifierRef = verifierEnv && models[verifierEnv] ? verifierEnv : DEFAULT_MODEL_REF;
|
|
878
879
|
const roles = {
|
|
879
880
|
verifier: verifierRef,
|
|
880
881
|
};
|
|
881
882
|
for (const r of ROSTER_PRIMARY_ROLES)
|
|
882
|
-
roles[r] =
|
|
883
|
+
roles[r] = DEFAULT_MODEL_REF;
|
|
883
884
|
for (const r of ROSTER_CHEAP_ROLES)
|
|
884
885
|
roles[r] = cheapRef;
|
|
885
886
|
for (const r of csv("MODEL_CODE_ROLES")) {
|
package/dist/env-facts.d.ts
CHANGED
|
@@ -96,6 +96,67 @@ export declare function acceptShellScratchpadDir(raw: unknown, opts: {
|
|
|
96
96
|
hostSemanticsLane: boolean;
|
|
97
97
|
warn: (msg: string, meta?: object) => void;
|
|
98
98
|
}): Promise<string | undefined>;
|
|
99
|
+
/**
|
|
100
|
+
* {@link acceptShellScratchpadDir} 的**词法半场**(S-351 拆出):零 I/O、可在请求的任何一点问。
|
|
101
|
+
*
|
|
102
|
+
* 为什么拆:S-351 的前判要在**任何副作用之前**回答「这条请求会把哪个目录当写门豁免域」,而验收门的
|
|
103
|
+
* 后半场是 `mkdir`(副作用 + I/O)。把判据抄一份到前判处 = 两份验收规则,而两份规则漂移的那天,前判
|
|
104
|
+
* 会替一条**根本不会被接受**的壳供路径拒掉请求(或反过来放过一条会被接受的)。所以规则只有这一份,
|
|
105
|
+
* 两个调用点各取所需:验收门 = 本函数 + mkdir;前判 = 本函数。
|
|
106
|
+
*
|
|
107
|
+
* 返回的是**词法归一后**(`resolve`)的拼法 —— 与验收门交给写门的那一个逐字相同。
|
|
108
|
+
*/
|
|
109
|
+
export declare function screenShellScratchpadDir(raw: unknown, opts: {
|
|
110
|
+
requirePrincipal: boolean;
|
|
111
|
+
hostSemanticsLane: boolean;
|
|
112
|
+
warn: (msg: string, meta?: object) => void;
|
|
113
|
+
}): string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* S-351(core 7.20.0 / [ref])—— **这条请求会不会把任务根放进写门的豁免域里**。
|
|
116
|
+
*
|
|
117
|
+
* ## 病形(引擎侧的硬拒,不是判决期的宽严)
|
|
118
|
+
* 写门的豁免域就是 scratchpad(`exemptDirs: [scratchpadDir]`,`task-settings.ts` 的
|
|
119
|
+
* `createFsWriteGatePolicy` 那一句),而门的根是这条 task 的 cwd。core 7.20.0 起,**豁免域等于根或是
|
|
120
|
+
* 根的祖先 ⇒ 构造期抛**(亲读安装树 `dist/core/fs-write-gate-policy.js:11-28`,
|
|
121
|
+
* `code = "config.write_exempt_dir_covers_root"`)——理由逐字:一条盖住根的豁免把根下**每一次写**都自动
|
|
122
|
+
* 批准,那就是整个工作区,门宁可拒绝被建出来,也不肯带着一扇静默关掉的写治理去跑。
|
|
123
|
+
*
|
|
124
|
+
* 本仓真正够得着这一形的只有**一条边**:调用方自设 `body.cwd` 进 scratchpad 树(server 自己**从不**
|
|
125
|
+
* 把 cwd 缺省到 scratchpad —— scratchpad 与 task root 是两棵子树)。它今天的下场是 spec 折叠期抛出一条
|
|
126
|
+
* 没人分诊过的错,而调用方看到的是一条与病因无关的 422。所以前判把它提前成一条**读得懂的请求拒绝**。
|
|
127
|
+
*
|
|
128
|
+
* ## 判据只有词法(不承诺 canonical)
|
|
129
|
+
* 引擎那一侧的构造期**没有 I/O**(同上坐标:`lexicalViewOf` 纯字符串),所以这里也只做词法 —— 承诺
|
|
130
|
+
* canonical 就是承诺一件此刻做不到的事(软链形的真身要到裁决时刻才问 fs,那是写门自己的第二道)。
|
|
131
|
+
*
|
|
132
|
+
* ## 射程(每一条都是「不这样写就会误拒」)
|
|
133
|
+
* · **只在 cwd 真会被尊重时判**:`cwdHonored` 为假的部署(多租户 / 非 host 车道)把 cwd 直接忽略,
|
|
134
|
+
* 它永远不会变成门的根 —— 在那里拒等于凭空多一条 400;
|
|
135
|
+
* · **豁免域取这条会话可能挂上的每一只**(不是「这一次挂哪只」):server 自算那条恒是候选(有 session
|
|
136
|
+
* 就算得出),壳供 `body.scratchpadDir` **过了词法验收**才追加为候选(过不了的那条永远挂不上,拿它拒
|
|
137
|
+
* 就是凭空多一条 400)。只判其一会留一条**跨请求**的洞:一条请求供一只合法壳 scratchpad 把判据引开、
|
|
138
|
+
* 同时把 cwd 指进自算那只,放行后 cwd 坐进会话表,下一条不带路径字段的请求回落自算形、死在引擎构造期,
|
|
139
|
+
* 而它根本没送过 cwd(codex r2 [medium] 实证);
|
|
140
|
+
* · **`in-process` 车道(provider 未设)不在射程**:那条腿的门根是**进程自己的 cwd**,与请求字段无关 ——
|
|
141
|
+
* 对着一个请求字段拒一件不是它造成的事,是把诊断指向错的地方(如实登记,不假装覆盖)。
|
|
142
|
+
*/
|
|
143
|
+
export declare function scratchpadCoveringTaskRoot(args: {
|
|
144
|
+
cwd: unknown;
|
|
145
|
+
shellScratchpadDir: unknown;
|
|
146
|
+
localDataRoot: string;
|
|
147
|
+
sessionId: string | undefined;
|
|
148
|
+
requirePrincipal: boolean;
|
|
149
|
+
cwdHonored: boolean;
|
|
150
|
+
}): {
|
|
151
|
+
cwd: {
|
|
152
|
+
asSpelled: string;
|
|
153
|
+
lexical: string;
|
|
154
|
+
};
|
|
155
|
+
scratchpadDir: {
|
|
156
|
+
asSpelled: string;
|
|
157
|
+
lexical: string;
|
|
158
|
+
};
|
|
159
|
+
} | undefined;
|
|
99
160
|
/** 删除一个 session 的 scratchpad 目录(rm -rf,幂等:目录不存在=no-op)。失败吞 warn 不抛——
|
|
100
161
|
* 清理绝不 fault 调用它的 session-delete 路径;下一轮 {@link sweepStaleScratchpads} 仍会兜到它。 */
|
|
101
162
|
export declare function purgeScratchpadDir(localDataRoot: string, sessionId: string): Promise<void>;
|
package/dist/env-facts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
2
2
|
import { mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { dirCoversPath, isValidCwd, normalizeAbsolutePathLexically } from "./task-cwd.js";
|
|
4
5
|
const MAX_STR = 64;
|
|
5
6
|
const MAX_CAPS = 16;
|
|
6
7
|
const MAX_PATH = 160;
|
|
@@ -61,6 +62,19 @@ export async function ensureScratchpadDir(localDataRoot, sessionId) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
export async function acceptShellScratchpadDir(raw, opts) {
|
|
65
|
+
const screened = screenShellScratchpadDir(raw, opts);
|
|
66
|
+
if (screened === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
await mkdir(screened, { recursive: true });
|
|
70
|
+
return screened;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
opts.warn("shell_scratchpad_ignored", { reason: "mkdir_failed", error: err instanceof Error ? err.message : String(err) });
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function screenShellScratchpadDir(raw, opts) {
|
|
64
78
|
if (typeof raw !== "string" || raw.length === 0)
|
|
65
79
|
return undefined;
|
|
66
80
|
if (opts.requirePrincipal) {
|
|
@@ -85,14 +99,26 @@ export async function acceptShellScratchpadDir(raw, opts) {
|
|
|
85
99
|
opts.warn("shell_scratchpad_ignored", { reason: "too_shallow", canonical });
|
|
86
100
|
return undefined;
|
|
87
101
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
catch (err) {
|
|
93
|
-
opts.warn("shell_scratchpad_ignored", { reason: "mkdir_failed", error: err instanceof Error ? err.message : String(err) });
|
|
102
|
+
return canonical;
|
|
103
|
+
}
|
|
104
|
+
export function scratchpadCoveringTaskRoot(args) {
|
|
105
|
+
if (!args.cwdHonored || !isValidCwd(args.cwd))
|
|
94
106
|
return undefined;
|
|
107
|
+
const candidates = [];
|
|
108
|
+
const shell = screenShellScratchpadDir(args.shellScratchpadDir, { requirePrincipal: args.requirePrincipal, hostSemanticsLane: true, warn: () => { } });
|
|
109
|
+
if (shell !== undefined)
|
|
110
|
+
candidates.push({ asSpelled: typeof args.shellScratchpadDir === "string" ? args.shellScratchpadDir : shell, dir: shell });
|
|
111
|
+
if (args.sessionId !== undefined) {
|
|
112
|
+
const own = scratchpadDirFor(args.localDataRoot, args.sessionId);
|
|
113
|
+
candidates.push({ asSpelled: own, dir: own });
|
|
95
114
|
}
|
|
115
|
+
const hit = candidates.find((c) => dirCoversPath(c.dir, args.cwd));
|
|
116
|
+
if (hit === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
return {
|
|
119
|
+
cwd: { asSpelled: args.cwd, lexical: normalizeAbsolutePathLexically(args.cwd) },
|
|
120
|
+
scratchpadDir: { asSpelled: hit.asSpelled, lexical: normalizeAbsolutePathLexically(hit.dir) },
|
|
121
|
+
};
|
|
96
122
|
}
|
|
97
123
|
export async function purgeScratchpadDir(localDataRoot, sessionId) {
|
|
98
124
|
const dir = scratchpadDirFor(localDataRoot, sessionId);
|
|
@@ -52,6 +52,18 @@ export interface ExecutionLaneCaps {
|
|
|
52
52
|
/** core 的 `isRemoteExecutionEnv` 对本车道的判决(鸭子类型;判别位 = `config.remoteExec` 在不在场,
|
|
53
53
|
* **含 host**)。5.26.0 起它决定 `# Memory` 写指令撤不撤。 */
|
|
54
54
|
readonly coreRemoteExecutionEnv: LaneCapability;
|
|
55
|
+
/**
|
|
56
|
+
* S-354 / core 7.20.0([ref]):这条车道的适配器**声明**了 `capabilities.canonicalPathAuthoritative`
|
|
57
|
+
* 没有 —— 即「我的 `canonicalPath` 是路径落点的权威,整链答不出来是解析器出了事,不是一个我看不见的
|
|
58
|
+
* 命名空间」。声明 ⇒ 读边界的执行期复核在解析器沉默时**拒**;缺席 ⇒ 今天每个适配器的读法(词法判决
|
|
59
|
+
* 成立、命令照跑)—— **缺席不是「声明为 false」**,所以这一位的 `unsupported` 读作「本车道没表态」。
|
|
60
|
+
*
|
|
61
|
+
* 判别式:各 adapter 的 `capabilities` 字面量(`plugins/remote-env-{host,ssh,adb,k8s,device,e2b,local-docker}.ts`),
|
|
62
|
+
* 由 core 的严格读器 `canonicalPathIsAuthoritative(env)` 读;对账测试逐 lane 拿**真 adapter 实例**问它。
|
|
63
|
+
* 本位存在的理由 = 让「新车道/新适配器不表态」变成**编译红**(穷举 switch 少一行即 tsc 红),而不是
|
|
64
|
+
* 静默继承一个没人表过态的缺席。
|
|
65
|
+
*/
|
|
66
|
+
readonly canonicalPathAuthority: LaneCapability;
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* per-lane 能力表。**穷举 switch,禁 default 臂**(hands-lane.ts:39-46 先例)。
|
|
@@ -19,6 +19,7 @@ export function executionLaneCaps(lane) {
|
|
|
19
19
|
memoryPersistenceCapable: NO,
|
|
20
20
|
hostFilePlane: YES,
|
|
21
21
|
coreRemoteExecutionEnv: NO,
|
|
22
|
+
canonicalPathAuthority: NO,
|
|
22
23
|
};
|
|
23
24
|
case "host":
|
|
24
25
|
return {
|
|
@@ -35,6 +36,7 @@ export function executionLaneCaps(lane) {
|
|
|
35
36
|
memoryPersistenceCapable: YES,
|
|
36
37
|
hostFilePlane: YES,
|
|
37
38
|
coreRemoteExecutionEnv: YES,
|
|
39
|
+
canonicalPathAuthority: YES,
|
|
38
40
|
};
|
|
39
41
|
case "e2b":
|
|
40
42
|
return {
|
|
@@ -51,6 +53,7 @@ export function executionLaneCaps(lane) {
|
|
|
51
53
|
memoryPersistenceCapable: NO,
|
|
52
54
|
hostFilePlane: NO,
|
|
53
55
|
coreRemoteExecutionEnv: YES,
|
|
56
|
+
canonicalPathAuthority: NO,
|
|
54
57
|
};
|
|
55
58
|
case "k8s":
|
|
56
59
|
return {
|
|
@@ -67,6 +70,7 @@ export function executionLaneCaps(lane) {
|
|
|
67
70
|
memoryPersistenceCapable: NO,
|
|
68
71
|
hostFilePlane: NO,
|
|
69
72
|
coreRemoteExecutionEnv: YES,
|
|
73
|
+
canonicalPathAuthority: YES,
|
|
70
74
|
};
|
|
71
75
|
case "ssh":
|
|
72
76
|
return {
|
|
@@ -83,6 +87,7 @@ export function executionLaneCaps(lane) {
|
|
|
83
87
|
memoryPersistenceCapable: NO,
|
|
84
88
|
hostFilePlane: NO,
|
|
85
89
|
coreRemoteExecutionEnv: YES,
|
|
90
|
+
canonicalPathAuthority: YES,
|
|
86
91
|
};
|
|
87
92
|
case "adb":
|
|
88
93
|
return {
|
|
@@ -99,6 +104,7 @@ export function executionLaneCaps(lane) {
|
|
|
99
104
|
memoryPersistenceCapable: NO,
|
|
100
105
|
hostFilePlane: NO,
|
|
101
106
|
coreRemoteExecutionEnv: YES,
|
|
107
|
+
canonicalPathAuthority: YES,
|
|
102
108
|
};
|
|
103
109
|
case "local-docker":
|
|
104
110
|
return {
|
|
@@ -115,6 +121,7 @@ export function executionLaneCaps(lane) {
|
|
|
115
121
|
memoryPersistenceCapable: NO,
|
|
116
122
|
hostFilePlane: NO,
|
|
117
123
|
coreRemoteExecutionEnv: YES,
|
|
124
|
+
canonicalPathAuthority: NO,
|
|
118
125
|
};
|
|
119
126
|
case "device":
|
|
120
127
|
return {
|
|
@@ -131,6 +138,7 @@ export function executionLaneCaps(lane) {
|
|
|
131
138
|
memoryPersistenceCapable: NO,
|
|
132
139
|
hostFilePlane: NO,
|
|
133
140
|
coreRemoteExecutionEnv: YES,
|
|
141
|
+
canonicalPathAuthority: YES,
|
|
134
142
|
};
|
|
135
143
|
}
|
|
136
144
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-157④ —— SQL file-history 店的 **typed 永久失败**披露座:散文一个铸点 + 机器面一只 sink。
|
|
3
|
+
*
|
|
4
|
+
* ## 病(基线 v7.79.0 逐行亲核 + 亲跑)
|
|
5
|
+
* `plugins/blob-backend.ts:55` 的 `BlobTooLargeError` 带 typed 判别码 `blob_too_large_for_sql`,头注
|
|
6
|
+
* 原话「绝不能与 transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)」。可它一旦被
|
|
7
|
+
* `plugins/file-history-store-sql.ts` 的任何一条 `catch` 折进 `FileHistoryResult.error.message`,判别码
|
|
8
|
+
* 就只剩散文,**机器一位都读不到**。两条腿各自的后果:
|
|
9
|
+
*
|
|
10
|
+
* · **snapshot(边界捕获)腿**:引擎把失败包成一句散文交给 `RunnerDeps.onError({phase:"rewind"})`
|
|
11
|
+
* (core 7.20.0 `dist/core/runner/prepare-file-history.js:269`/`:275`),服务那条臂从文本里抠**第一个**
|
|
12
|
+
* 括号词当 metric label —— 而第一个括号词恒是引擎自己的闭集码。实测:「这个文件永远存不进这口店」
|
|
13
|
+
* 与「连接被重置」在 `rewind_snapshot_failed_total` 上**同形** `code="snapshot_failed"`。
|
|
14
|
+
* · **trackEdit(首触备份)腿**:本服务用引擎默认的 `onTrackFailure:"refuse"`(`boot/runner-deps.ts` 亲读),
|
|
15
|
+
* 那条路 **完全不走 onError** —— 引擎把 `(track_failed: <本店 message>)` 念给模型,末句是
|
|
16
|
+
* 「Retry when the file-history store is healthy.」(`prepare-file-history.js:154`)。对这枚码那是永远
|
|
17
|
+
* 不会到来的一天,而**运维面一个计数都没有**:这条腿今天在遥测上完全不可见。
|
|
18
|
+
*
|
|
19
|
+
* ## 修法(源头留痕,不走文本通道)
|
|
20
|
+
* · **散文面**({@link discloseCaughtFailure} 的返回值):把 caught error 的 typed `code` 写成一个括号词
|
|
21
|
+
* 前置到它自己的 message 前 —— **不 `instanceof` 单挑任何一个类**(身上有字符串 `code` 就折),该店
|
|
22
|
+
* 11 处折点全部改走这一只函数。人与模型因此看得到「这是哪一类失败」。
|
|
23
|
+
* · **机器面**(同一只函数的副作用):码取自 typed 错**对象**、对闭集词表判定,**绝不从散文里抠**。
|
|
24
|
+
* 🔴 这一条是 codex R1-[high] 验真后的定形:文本通道**天然可伪造** —— 一个名叫
|
|
25
|
+
* `(blob_too_large_for_sql).txt` 的文件读不出来时,它的路径就在 `snapshot_failed` 的散文里,任何
|
|
26
|
+
* 「从散文里取在册括号词」的读法都会把它当成一枚永久码(继而抑制掉真故障的告警)。闭集只约束基数,
|
|
27
|
+
* 不能证明来源。所以机读判定搬回**唯一知道真相的那一层**=铸错的地方,`rewind` 臂的读法一字不动。
|
|
28
|
+
* · 留痕三件套与 `observability/fail-open.ts` / `observability/secret-env-scrub.ts` 同律:**计数逐次**
|
|
29
|
+
* (`file_history_permanent_failure_total{code}`)、**每进程每码一条 warn**(第二条起零新增内容,而
|
|
30
|
+
* 20G 树的用户每 turn 撞一次)、装配前累计的次数在 install 时补进 metrics。
|
|
31
|
+
*/
|
|
32
|
+
import type { Logger } from "./observability/logger.js";
|
|
33
|
+
import type { Metrics } from "./observability/metrics.js";
|
|
34
|
+
interface Sink {
|
|
35
|
+
logger?: Logger;
|
|
36
|
+
metrics?: Metrics;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 把一个 caught error 折进店侧散文,**并**在它是一枚 typed 永久失败时留痕。
|
|
40
|
+
*
|
|
41
|
+
* 返回值 = 散文形:带 typed `code` 的前缀一个括号词(`(blob_too_large_for_sql) blob of … exceeds …`),
|
|
42
|
+
* 不带的**逐字**是原 message(transient 失败的文案因此与本批之前一字不差)。
|
|
43
|
+
* 副作用 = 机器形:码取自错误对象,逐次计数 + 每进程每码一条 warn。观测绝不抛。
|
|
44
|
+
*/
|
|
45
|
+
export declare function discloseCaughtFailure(err: unknown): string;
|
|
46
|
+
/** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。 */
|
|
47
|
+
export declare function installFileHistoryFailureRecorder(deps: Sink): void;
|
|
48
|
+
/** 本进程逐码累计(测试/诊断读面)。 */
|
|
49
|
+
export declare function fileHistoryPermanentFailureCounts(): ReadonlyMap<string, number>;
|
|
50
|
+
/** 测试用重置(进程级状态在同一个 vitest worker 里跨用例存活)。 */
|
|
51
|
+
export declare function resetFileHistoryFailureRecorderForTest(deps?: Sink): void;
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=file-history-disclosure.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const PERMANENT_STORE_FAILURE_CODES = {
|
|
2
|
+
blob_too_large_for_sql: {
|
|
3
|
+
note: "a blob is larger than this deployment's SQL snapshot-blob cap — the same file will fail every turn until the deployment changes",
|
|
4
|
+
knobs: "MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY (object storage) or SNAPSHOT_BLOB_SQL_MAX_BYTES (raise the cap)",
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
const PERMANENT_FAILURE_COUNTER = "file_history_permanent_failure_total";
|
|
8
|
+
const PERMANENT_FAILURE_HELP = "file-history store failures whose typed code means retrying can NEVER succeed (the deployment must change), by code. " +
|
|
9
|
+
"Covers BOTH legs — the rewind boundary capture AND the first-touch trackEdit refusal, which the engine answers to the model " +
|
|
10
|
+
"without ever calling onError, so it appears in no other metric. Non-zero = go read the paired one-shot warn for the knobs.";
|
|
11
|
+
let sink = {};
|
|
12
|
+
let counts = new Map();
|
|
13
|
+
let pushed = new Map();
|
|
14
|
+
let warned = new Set();
|
|
15
|
+
let pendingWarn = new Map();
|
|
16
|
+
function isPermanentCode(code) {
|
|
17
|
+
return Object.prototype.hasOwnProperty.call(PERMANENT_STORE_FAILURE_CODES, code);
|
|
18
|
+
}
|
|
19
|
+
function drainToMetrics() {
|
|
20
|
+
const m = sink.metrics;
|
|
21
|
+
if (m === undefined)
|
|
22
|
+
return;
|
|
23
|
+
for (const [code, total] of counts) {
|
|
24
|
+
const already = pushed.get(code) ?? 0;
|
|
25
|
+
if (total > already) {
|
|
26
|
+
m.inc(PERMANENT_FAILURE_COUNTER, { code }, total - already);
|
|
27
|
+
pushed.set(code, total);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function discloseCaughtFailure(err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
const code = err?.code;
|
|
34
|
+
if (typeof code !== "string" || code === "")
|
|
35
|
+
return message;
|
|
36
|
+
if (isPermanentCode(code))
|
|
37
|
+
recordPermanent(code, message);
|
|
38
|
+
return `(${code}) ${message}`;
|
|
39
|
+
}
|
|
40
|
+
function safely(record) {
|
|
41
|
+
try {
|
|
42
|
+
record();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function recordPermanent(code, message) {
|
|
48
|
+
safely(() => {
|
|
49
|
+
counts.set(code, (counts.get(code) ?? 0) + 1);
|
|
50
|
+
drainToMetrics();
|
|
51
|
+
if (!warned.has(code))
|
|
52
|
+
emitFirstWarn(code, message);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function emitFirstWarn(code, message) {
|
|
56
|
+
if (!pendingWarn.has(code))
|
|
57
|
+
pendingWarn.set(code, message);
|
|
58
|
+
const logger = sink.logger;
|
|
59
|
+
if (logger === undefined)
|
|
60
|
+
return;
|
|
61
|
+
logger.warn("file_history_permanent_failure", {
|
|
62
|
+
code,
|
|
63
|
+
note: PERMANENT_STORE_FAILURE_CODES[code].note,
|
|
64
|
+
knobs: PERMANENT_STORE_FAILURE_CODES[code].knobs,
|
|
65
|
+
info: pendingWarn.get(code) ?? message,
|
|
66
|
+
once: "每进程每码一条;计数逐次,读 " + PERMANENT_FAILURE_COUNTER,
|
|
67
|
+
});
|
|
68
|
+
warned.add(code);
|
|
69
|
+
pendingWarn.delete(code);
|
|
70
|
+
}
|
|
71
|
+
export function installFileHistoryFailureRecorder(deps) {
|
|
72
|
+
sink = { ...deps };
|
|
73
|
+
deps.metrics?.counter(PERMANENT_FAILURE_COUNTER, PERMANENT_FAILURE_HELP);
|
|
74
|
+
drainToMetrics();
|
|
75
|
+
for (const [code, message] of [...pendingWarn])
|
|
76
|
+
safely(() => emitFirstWarn(code, message));
|
|
77
|
+
}
|
|
78
|
+
export function fileHistoryPermanentFailureCounts() {
|
|
79
|
+
return new Map(counts);
|
|
80
|
+
}
|
|
81
|
+
export function resetFileHistoryFailureRecorderForTest(deps = {}) {
|
|
82
|
+
counts = new Map();
|
|
83
|
+
pushed = new Map();
|
|
84
|
+
warned = new Set();
|
|
85
|
+
pendingWarn = new Map();
|
|
86
|
+
sink = { ...deps };
|
|
87
|
+
deps.metrics?.counter(PERMANENT_FAILURE_COUNTER, PERMANENT_FAILURE_HELP);
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=file-history-disclosure.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 提交准入(S-147 切片:`http/server.ts` 的 `prepareSpec` 与它的模块级校验词表逐字搬家,正文一字未改)。
|
|
3
|
+
* 读体 → 验型 → 授权 → 建 spec 的**唯一**一条链;tasks / runs / A2A 三个提交面共用它(抄一份 = 抄一份会漂的 400)。
|
|
4
|
+
* ctx 字段 3 只(`deps` / `readJson` / `runnerFor`),机器算(closure-free-vars)与手抄一致。
|
|
5
|
+
*/
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
|
+
import type { TaskRequestBody } from "./wire-types.js";
|
|
8
|
+
import { HttpError } from "../security.js";
|
|
9
|
+
import type { PreparedTaskSubmission, RouteHelpers } from "./route-ctx.js";
|
|
10
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
11
|
+
/** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
|
|
12
|
+
* dist/core/runner/synthetic-tools.js:84,未从 core 包根导出,故钉本地孪生+本出处注记)。修7 曾对齐旧
|
|
13
|
+
* 20k 截断门([ref] 飞轮考据:20k 是压缩保留区口径,invoke 时刻用错),core 1.293 撤门后本门跟随回收
|
|
14
|
+
* ([ref]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。
|
|
15
|
+
*
|
|
16
|
+
* 🔴 **这是技能面**唯一**剩下的尺寸门,而且它不是超集帽**:引擎在装配期对超 1 MiB 的技能本就整拒
|
|
17
|
+
* (config 阶段错误),门上先答只是把同一个判决搬到调用方看得懂的地方。[ref] 退役的三条帽
|
|
18
|
+
* (条数 / description / name 长度)都没有这个性质——它们是本仓自造的、CC 与引擎都没有的门。 */
|
|
19
|
+
export declare const MAX_SKILL_CONTENT_CHARS = 1048576;
|
|
20
|
+
export declare const MAX_SYSTEM_PROMPT_CHARS = 16384;
|
|
21
|
+
/** Cap on a request's `outputSchema` (JSON-serialized) — an uncapped schema bloats the prompt + the per-turn
|
|
22
|
+
* validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
|
|
23
|
+
export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
24
|
+
/**
|
|
25
|
+
* Validate `body.skills` —— **形**校验 + 一条引擎孪生门。返回错误串(→ 400)或 null。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 **[ref](7.77.0,硬 BREAKING;零别名零双读)**:此前这里有三条本仓自造的拒绝臂,全部退役 ——
|
|
28
|
+
* · `at most 10 skills per request`(条数帽);
|
|
29
|
+
* · `skill.description must be a string of at most 1024 characters`(描述长度帽);
|
|
30
|
+
* · `skill.name must be a non-empty string of at most 64 characters` 里的 **长度**那一半。
|
|
31
|
+
*
|
|
32
|
+
* 退役的判据是**上游两侧都没有同形门**:CC 2.1.259 的技能清单渲染(`cli259.js:23195` 的预算算法)
|
|
33
|
+
* 在超预算时把**描述裁短**、再不够就退成 name-only,**从不**因为条数多或描述长而拒掉一条技能;
|
|
34
|
+
* 它的 `Ir()` 名字校验也只判非空 / 无配对代理 / 无首尾空白 / 无括号逗号控制字符,没有长度帽。
|
|
35
|
+
* 引擎侧同理:唯一整拒的是 1 MiB 的单技能加载门。于是那三条帽是「装了 11 个技能就 400」这种
|
|
36
|
+
* 纯本仓面的伤——收益是零(预算由渲染侧按字节兜),代价是用户装不进自己的技能库。
|
|
37
|
+
*
|
|
38
|
+
* **补偿**(三问的第三问):`settings.skillListingBudgetFraction` / `settings.skillListingMaxDescChars`
|
|
39
|
+
* 两个旋钮直达引擎的渲染预算(per-turn 成本的真属主)。
|
|
40
|
+
* ⚠️ **fraction 是**单向**的**:引擎的预算 = `min(窗口 × fraction, 8 000 字节)`,8 000 是**投递车道的结构
|
|
41
|
+
* 上限**(清单帧与同 turn 的其它附件共用一份完整送达额度)⇒ **调低生效、调高被夹**;调高唯一的用处是让
|
|
42
|
+
* 一个声明窗口小于 200 000 token 的模型把清单预算拿回大窗口的水平。别把这句读成「调用方可以随便加大」。
|
|
43
|
+
* 体积面照旧由**非技能专属**的 8 MiB 请求体上限兜住(`MAX_BODY`),那是一条一直都在的门。
|
|
44
|
+
*
|
|
45
|
+
* 🔴 **server 侧禁裁列表 / 禁截描述**:渲染归引擎(单一属主)。本函数只判「这一条技能的形能不能用」。
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateUserSkills(skills: unknown): string | null;
|
|
48
|
+
export declare function createAdmission(ctx: {
|
|
49
|
+
deps: FlatServiceDeps;
|
|
50
|
+
runnerFor: NonNullable<FlatServiceDeps["runnerFor"]>;
|
|
51
|
+
} & Pick<RouteHelpers, "readJson">): {
|
|
52
|
+
prepareSpec: (req: IncomingMessage, res: ServerResponse, bodyIn?: TaskRequestBody, onTypedFailure?: (err: HttpError) => void, legFacts?: {
|
|
53
|
+
liveStream?: boolean;
|
|
54
|
+
}) => Promise<PreparedTaskSubmission | null>;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=admission.d.ts.map
|