@sema-agent/server 7.131.0 → 7.132.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +309 -0
- package/README.md +5 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +163 -54
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +19 -47
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +6 -7
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/scenarios.d.ts +34 -40
- package/dist/capabilities/scenarios.js +16 -33
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/config-catalog.js +6 -6
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +12 -4
- package/dist/config-types.d.ts +7 -9
- package/dist/config.js +17 -4
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +78 -35
- package/dist/hooks/hook-runner.d.ts +232 -80
- package/dist/hooks/hook-runner.js +218 -312
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +101 -57
- package/dist/http/approval-sweeps.js +57 -16
- package/dist/http/checkpoint-ctx.d.ts +25 -8
- package/dist/http/checkpoint-ctx.js +11 -7
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +5 -9
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +43 -22
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +19 -9
- package/dist/http/send.d.ts +9 -7
- package/dist/http/send.js +7 -2
- package/dist/http/server.d.ts +21 -11
- package/dist/http/server.js +28 -2
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +1 -9
- package/dist/mcp-assertion-header.js +0 -6
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-route-endpoint.js +5 -5
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +42 -6
- package/dist/observability/fail-open.js +42 -6
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +137 -90
- package/dist/plugins/one-time-migrations.js +356 -207
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +2 -4
- package/dist/rule-entry-list.d.ts +12 -9
- package/dist/rule-entry-list.js +37 -21
- package/dist/run-local.js +19 -19
- package/dist/runs.d.ts +6 -0
- package/dist/runs.js +16 -0
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +28 -16
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-tool-axes-m19.d.ts +19 -4
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +46 -58
- package/dist/task-mcp.js +99 -65
- package/dist/task-settings.d.ts +7 -21
- package/dist/task-settings.js +7 -36
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.js +4 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +19 -6
- package/dist/trace/project.js +57 -22
- package/dist/trace/wire-projection-faces.d.ts +79 -1
- package/dist/trace/wire-projection-faces.js +22 -0
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +2 -3
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +34 -1
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
package/dist/http/admission.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
7
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
8
8
|
import { HttpError } from "../security.js";
|
|
9
|
+
import { type LimitFacts } from "../limit-refusal.js";
|
|
9
10
|
import type { PreparedTaskSubmission, RouteHelpers } from "./route-ctx.js";
|
|
10
11
|
import type { FlatServiceDeps } from "./server.js";
|
|
11
12
|
/** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
|
|
@@ -22,7 +23,7 @@ export declare const MAX_SYSTEM_PROMPT_CHARS = 16384;
|
|
|
22
23
|
* validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
|
|
23
24
|
export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
24
25
|
/**
|
|
25
|
-
* Validate `body.skills` ——
|
|
26
|
+
* Validate `body.skills` —— 逐条:**形**校验 + 一条引擎孪生门,再判两只清单的条数帽(与全部工具名清单同一只判)。返回拒({@link UserSkillsRefusal}:句子 + 撞帽时的机读位 → 400)或 null。
|
|
26
27
|
*
|
|
27
28
|
* 🔴 **[ref](7.77.0,硬 BREAKING;零别名零双读)**:此前这里有三条本仓自造的拒绝臂,全部退役 ——
|
|
28
29
|
* · `at most 10 skills per request`(条数帽);
|
|
@@ -51,7 +52,12 @@ export declare const MAX_OUTPUT_SCHEMA_CHARS = 32768;
|
|
|
51
52
|
*
|
|
52
53
|
* 🔴 **server 侧禁裁列表 / 禁截描述**:渲染归引擎(单一属主)。本函数只判「这一条技能的形能不能用」。
|
|
53
54
|
*/
|
|
54
|
-
export declare function validateUserSkills(skills: unknown):
|
|
55
|
+
export declare function validateUserSkills(skills: unknown): UserSkillsRefusal | null;
|
|
56
|
+
/** `body.skills` 的拒:句子 + 撞帽时的机读位(`field` / `reason` / `count` / `cap`;形错只有句子)。与子代定义那道门(`spec-fields.ts` 的 `TaskAgentsRefusal`)同形。 */
|
|
57
|
+
export interface UserSkillsRefusal {
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly facts?: LimitFacts;
|
|
60
|
+
}
|
|
55
61
|
/**
|
|
56
62
|
* core 7.41.0 #1303 —— 顶层技能带 `manifest` 的拒因句(单铸点)。出路只点本服务上**真存在**的旋钮:技能的两只清单
|
|
57
63
|
* (`allowedTools` / `disallowedTools`)、圈工具 = deny 规则(请求 `settings.permissions.deny` / 运维 `APPROVAL_DENY`)或子代定义的
|
package/dist/http/admission.js
CHANGED
|
@@ -8,15 +8,20 @@ import { autonomyModeFloor } from "../runtime-governance.js";
|
|
|
8
8
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
9
9
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
10
10
|
import * as facts from "../capability-facts.js";
|
|
11
|
-
import {
|
|
11
|
+
import { buildHooksRefusal, parseHooksConfig, hooksMatcherIssue } from "../hooks/hook-runner.js";
|
|
12
12
|
import { judgeRequestWebSearch } from "../plugins/web-search.js";
|
|
13
|
-
import { normalizeApproachNotice, skillToolListOk,
|
|
13
|
+
import { normalizeApproachNotice, skillToolListOk, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
|
|
14
|
+
import { skillToolListsOverCap } from "../rule-entry-list.js";
|
|
15
|
+
import { limitRefusal } from "../limit-refusal.js";
|
|
16
|
+
import { quotedCallerText } from "../caller-text-echo.js";
|
|
14
17
|
import { MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
|
|
15
18
|
import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
16
19
|
import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
|
|
17
20
|
import { resolveDevicePlacementIntent } from "./device-placement.js";
|
|
18
21
|
import { createRunAcceptance } from "../run-acceptance.js";
|
|
19
22
|
import { sendError, sendHttpError } from "./send.js";
|
|
23
|
+
import { refuseSubmitBody } from "./request-guards.js";
|
|
24
|
+
import { sqlWallsOf } from "../plugins/sql-driver.js";
|
|
20
25
|
import { ABSOLUTE_HOST_PATH_MAX, isAbsoluteHostPath, WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
21
26
|
const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
|
|
22
27
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
@@ -26,27 +31,34 @@ export function validateUserSkills(skills) {
|
|
|
26
31
|
if (skills === undefined)
|
|
27
32
|
return null;
|
|
28
33
|
if (!Array.isArray(skills))
|
|
29
|
-
return "skills must be an array of { name, description, content }";
|
|
30
|
-
for (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
return { message: "skills must be an array of { name, description, content }" };
|
|
35
|
+
for (let k = 0; k < skills.length; k++) {
|
|
36
|
+
const shape = userSkillShapeIssue(skills[k]);
|
|
37
|
+
if (shape !== null)
|
|
38
|
+
return { message: shape };
|
|
39
|
+
const over = skillToolListsOverCap(skills[k], `skills[${k}]`, "top");
|
|
40
|
+
if (over !== undefined)
|
|
41
|
+
return { message: over.message, ...(over.extra !== undefined ? { facts: over.extra } : {}) };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function userSkillShapeIssue(item) {
|
|
46
|
+
if (item === null || typeof item !== "object")
|
|
47
|
+
return "each skill must be an object { name, description, content }";
|
|
48
|
+
const s = item;
|
|
49
|
+
if (typeof s["name"] !== "string" || s["name"].length === 0)
|
|
50
|
+
return "skill.name must be a non-empty string";
|
|
51
|
+
if (typeof s["description"] !== "string")
|
|
52
|
+
return "skill.description must be a string";
|
|
53
|
+
if (typeof s["content"] !== "string" || s["content"].length === 0 || s["content"].length > MAX_SKILL_CONTENT_CHARS)
|
|
54
|
+
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)`;
|
|
55
|
+
if (s["baseDir"] !== undefined && !isAbsoluteHostPath(s["baseDir"]))
|
|
56
|
+
return `skill.baseDir must be an absolute path string of at most ${ABSOLUTE_HOST_PATH_MAX} characters with no ".." segment and no NUL byte (it is not resolved or read here — the engine only substitutes it into the skill's own text)`;
|
|
57
|
+
if (skillCarriesRetiredManifest(s))
|
|
58
|
+
return SKILL_MANIFEST_RETIRED;
|
|
59
|
+
for (const key of ["allowedTools", "disallowedTools"]) {
|
|
60
|
+
if (s[key] !== undefined && !skillToolListOk(s[key]))
|
|
61
|
+
return `skill.${key} must be an array of strings (permission-rule spellings: a tool name, mcp__<server> / mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**))`;
|
|
50
62
|
}
|
|
51
63
|
return null;
|
|
52
64
|
}
|
|
@@ -64,11 +76,64 @@ function refuseUnavailableModel(res, config, field, ref) {
|
|
|
64
76
|
sendError(res, 400, "request.model_unavailable", message);
|
|
65
77
|
return true;
|
|
66
78
|
}
|
|
79
|
+
const kindOf = (v) => (v === null ? "null" : Array.isArray(v) ? "an array" : typeof v === "object" ? "an object" : `a ${typeof v}`);
|
|
80
|
+
function hostPathListRefusal(field, raw) {
|
|
81
|
+
if (!Array.isArray(raw))
|
|
82
|
+
return { message: `${field} must be an array of absolute host paths` };
|
|
83
|
+
if (raw.length > MAX_ADDITIONAL_DIRS)
|
|
84
|
+
return limitRefusal(field, "entries", raw.length, MAX_ADDITIONAL_DIRS);
|
|
85
|
+
const index = raw.findIndex((d) => !isAbsoluteHostPath(d));
|
|
86
|
+
if (index < 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const bad = raw[index];
|
|
89
|
+
return { message: `${field}[${index}] must be a non-empty absolute host path (no '..' segments) — got ${typeof bad === "string" ? quotedCallerText(bad) : kindOf(bad)}`, facts: { field, reason: "bad_entry", index } };
|
|
90
|
+
}
|
|
67
91
|
const modelIdOf = (model) => (typeof model === "string" ? model : model?.id);
|
|
68
92
|
export function createAdmission(ctx) {
|
|
69
93
|
const { deps, readJson, runnerFor } = ctx;
|
|
70
94
|
async function prepareSpec(req, res, bodyIn, onTypedFailure, legFacts) {
|
|
71
95
|
const body = bodyIn ?? (await readJson(req));
|
|
96
|
+
{
|
|
97
|
+
const refusal = refuseSubmitBody(sqlWallsOf(deps.sqlEngine), body);
|
|
98
|
+
if (refusal !== null) {
|
|
99
|
+
const facts = { wallSource: refusal.wallSource };
|
|
100
|
+
switch (refusal.wallSource) {
|
|
101
|
+
case "reported":
|
|
102
|
+
switch (refusal.by) {
|
|
103
|
+
case "row":
|
|
104
|
+
sendError(res, 413, "request.payload_too_large", `task body is ${refusal.bytes} bytes, over this deployment's ${refusal.limitBytes}-byte limit: the database stores each submitted task in one row and holds at most ${refusal.wallBytes} bytes per row — shrink the inline images / userAttachments, or ask the operator to raise the database's single-row limit (TiDB performance.txn-entry-size-limit)`, facts);
|
|
105
|
+
break;
|
|
106
|
+
case "packet":
|
|
107
|
+
sendError(res, 413, "request.payload_too_large", `task body is ${refusal.bytes} bytes as sent to the database (JSON text with SQL string escaping), over this deployment's ${refusal.limitBytes}-byte limit: the database takes each submitted task in one statement and accepts at most ${refusal.wallBytes} bytes per statement — shrink the inline images / userAttachments, or ask the operator to raise the database's max_allowed_packet`, facts);
|
|
108
|
+
break;
|
|
109
|
+
default: {
|
|
110
|
+
const unhandled = refusal.by;
|
|
111
|
+
throw new Error(`submit body refusal without an HTTP answer: ${String(unhandled)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case "assumed":
|
|
116
|
+
switch (refusal.by) {
|
|
117
|
+
case "row":
|
|
118
|
+
sendError(res, 413, "request.payload_too_large", `task body is ${refusal.bytes} bytes, over this deployment's ${refusal.limitBytes}-byte limit: the database's single-row limit was not read (engine facts not read yet, or the limit could not be read); refusing conservatively at ${refusal.wallBytes} bytes per row (the smallest documented single-row limit among the supported engines) — retry shortly; if it persists, ask the operator to let the service's database account read the engine's row-size limit`, facts);
|
|
119
|
+
break;
|
|
120
|
+
case "packet":
|
|
121
|
+
sendError(res, 413, "request.payload_too_large", `task body is ${refusal.bytes} bytes as sent to the database (JSON text with SQL string escaping), over this deployment's ${refusal.limitBytes}-byte limit: the database's statement limit was not read (engine facts not read yet, or max_allowed_packet could not be read); refusing conservatively at ${refusal.wallBytes} bytes per statement (smallest documented max_allowed_packet default) — retry shortly; if it persists, ask the operator to check that the service's database account can read max_allowed_packet`, facts);
|
|
122
|
+
break;
|
|
123
|
+
default: {
|
|
124
|
+
const unhandled = refusal.by;
|
|
125
|
+
throw new Error(`submit body refusal without an HTTP answer: ${String(unhandled)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
default: {
|
|
130
|
+
const unhandled = refusal.wallSource;
|
|
131
|
+
throw new Error(`submit body refusal without an HTTP answer: ${String(unhandled)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
72
137
|
if (!body || typeof body.objective !== "string") {
|
|
73
138
|
sendError(res, 400, "request.field_invalid", "missing 'objective' string");
|
|
74
139
|
return null;
|
|
@@ -113,35 +178,13 @@ export function createAdmission(ctx) {
|
|
|
113
178
|
sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
|
|
114
179
|
return null;
|
|
115
180
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
sendError(res, 400, "request.field_invalid", `additionalDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
const bad = addDirsRaw.find((d) => !isAbsoluteHostPath(d));
|
|
127
|
-
if (bad !== undefined) {
|
|
128
|
-
sendError(res, 400, "request.field_invalid", "each additionalDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const addReadDirsRaw = body.additionalReadDirectories;
|
|
133
|
-
if (addReadDirsRaw !== undefined) {
|
|
134
|
-
if (!Array.isArray(addReadDirsRaw)) {
|
|
135
|
-
sendError(res, 400, "request.field_invalid", "additionalReadDirectories must be an array of absolute host paths");
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (addReadDirsRaw.length > MAX_ADDITIONAL_DIRS) {
|
|
139
|
-
sendError(res, 400, "request.field_invalid", `additionalReadDirectories must have at most ${MAX_ADDITIONAL_DIRS} entries`);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const badRead = addReadDirsRaw.find((d) => !isAbsoluteHostPath(d));
|
|
143
|
-
if (badRead !== undefined) {
|
|
144
|
-
sendError(res, 400, "request.field_invalid", "each additionalReadDirectories entry must be a non-empty absolute host path (no '..' segments)");
|
|
181
|
+
for (const field of ["additionalDirectories", "additionalReadDirectories"]) {
|
|
182
|
+
const raw = body[field];
|
|
183
|
+
if (raw === undefined)
|
|
184
|
+
continue;
|
|
185
|
+
const refusal = hostPathListRefusal(field, raw);
|
|
186
|
+
if (refusal !== undefined) {
|
|
187
|
+
sendError(res, 400, "request.field_invalid", refusal.message, refusal.facts);
|
|
145
188
|
return null;
|
|
146
189
|
}
|
|
147
190
|
}
|
|
@@ -215,12 +258,13 @@ export function createAdmission(ctx) {
|
|
|
215
258
|
if (st.hooks !== undefined && st.hooks !== null) {
|
|
216
259
|
const hooksParsed = parseHooksConfig(st.hooks);
|
|
217
260
|
if (!hooksParsed.config) {
|
|
218
|
-
|
|
261
|
+
const refusal = buildHooksRefusal(hooksParsed.issue);
|
|
262
|
+
sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
|
|
219
263
|
return null;
|
|
220
264
|
}
|
|
221
|
-
const toolMatcher =
|
|
265
|
+
const toolMatcher = hooksMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
|
|
222
266
|
if (toolMatcher !== undefined) {
|
|
223
|
-
const refusal =
|
|
267
|
+
const refusal = buildHooksRefusal(toolMatcher);
|
|
224
268
|
sendError(res, 400, "request.field_invalid", refusal.message, { field: refusal.field, detail: refusal.detail });
|
|
225
269
|
return null;
|
|
226
270
|
}
|
|
@@ -235,7 +279,7 @@ export function createAdmission(ctx) {
|
|
|
235
279
|
}
|
|
236
280
|
const skillsErr = validateUserSkills(body.skills);
|
|
237
281
|
if (skillsErr) {
|
|
238
|
-
sendError(res, 400, "request.field_invalid", skillsErr);
|
|
282
|
+
sendError(res, 400, "request.field_invalid", skillsErr.message, skillsErr.facts);
|
|
239
283
|
return null;
|
|
240
284
|
}
|
|
241
285
|
if (Array.isArray(body.images)) {
|
|
@@ -461,7 +505,7 @@ export function createAdmission(ctx) {
|
|
|
461
505
|
if (body.agents !== undefined) {
|
|
462
506
|
const agentsErr = validateTaskAgents(body.agents);
|
|
463
507
|
if (agentsErr) {
|
|
464
|
-
sendError(res, 400, "request.field_invalid", agentsErr);
|
|
508
|
+
sendError(res, 400, "request.field_invalid", agentsErr.message, agentsErr.facts);
|
|
465
509
|
return null;
|
|
466
510
|
}
|
|
467
511
|
const agentsBare = deps.config.models ?? {};
|
|
@@ -4,8 +4,7 @@ import { decodeCheckpointScope } from "../security.js";
|
|
|
4
4
|
import { hostDecisionFacts } from "../host-decision.js";
|
|
5
5
|
import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
|
-
import { hasServiceAuth } from "./
|
|
8
|
-
import { gatedPrincipal } from "./principal-gate.js";
|
|
7
|
+
import { gatedPrincipal, hasServiceAuth } from "./principal-gate.js";
|
|
9
8
|
import { costQuotaRefusalOf } from "./request-guards.js";
|
|
10
9
|
import { admissionRefusalOf, readinessOf } from "./readiness.js";
|
|
11
10
|
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
@@ -22,8 +21,27 @@ function blockedScalars(body) {
|
|
|
22
21
|
}
|
|
23
22
|
return out;
|
|
24
23
|
}
|
|
24
|
+
const BLOCKED_WARNED_MAX = 1024;
|
|
25
25
|
export function createApprovalSweeps(ctx) {
|
|
26
26
|
const { deps, resumeCheckpoint, mayDiscloseRowError } = ctx;
|
|
27
|
+
const blockedWarned = new Map();
|
|
28
|
+
const noteResumeBlocked = (cardToken, sessionId, status, body) => {
|
|
29
|
+
deps.metrics?.inc("deny_sweep_resume_blocked_total");
|
|
30
|
+
const fields = { status, ...blockedScalars(body) };
|
|
31
|
+
if (cardToken !== undefined) {
|
|
32
|
+
const fingerprint = JSON.stringify(fields);
|
|
33
|
+
if (blockedWarned.get(cardToken) === fingerprint)
|
|
34
|
+
return;
|
|
35
|
+
blockedWarned.delete(cardToken);
|
|
36
|
+
if (blockedWarned.size >= BLOCKED_WARNED_MAX) {
|
|
37
|
+
const oldest = blockedWarned.keys().next();
|
|
38
|
+
if (oldest.done !== true)
|
|
39
|
+
blockedWarned.delete(oldest.value);
|
|
40
|
+
}
|
|
41
|
+
blockedWarned.set(cardToken, fingerprint);
|
|
42
|
+
}
|
|
43
|
+
deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, ...fields });
|
|
44
|
+
};
|
|
27
45
|
const denySweepClockGuard = createWallClockJumpGuard({
|
|
28
46
|
slaMs: deps.config.approvalTimeoutSec > 0 ? deps.config.approvalTimeoutSec * 1000 : undefined,
|
|
29
47
|
onJump: ({ jumpMs, graceMs }) => deps.logger?.warn?.("deny_sweep_wall_clock_jump", {
|
|
@@ -45,26 +63,38 @@ export function createApprovalSweeps(ctx) {
|
|
|
45
63
|
return;
|
|
46
64
|
if (!denySweepClockGuard.admit(now))
|
|
47
65
|
return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
let expired;
|
|
67
|
+
try {
|
|
68
|
+
expired = await cs.listExpiredApprovalGates(now);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
deps.metrics?.inc("deny_sweep_list_failed_total");
|
|
72
|
+
deps.logger?.warn?.("deny_sweep_list_failed", {
|
|
73
|
+
err: redactHead(e instanceof Error ? e.message : String(e), 2000),
|
|
74
|
+
note: "the SLA deny-sweep could not read the expired approvals this tick, so nothing was denied and nothing was skipped; the rows stay suspended and are read again next tick (the terminal backstop still reaps them)",
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const parkSources = deps.backgroundAgentStore !== undefined || deps.workflowAgentSessionIndex !== undefined;
|
|
79
|
+
const cardOf = async (sessionId) => {
|
|
80
|
+
let token;
|
|
52
81
|
try {
|
|
53
|
-
|
|
54
|
-
if (!token)
|
|
55
|
-
return false;
|
|
82
|
+
token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: APPROVAL_GATE_KINDS })) ?? undefined;
|
|
83
|
+
if (!parkSources || !token)
|
|
84
|
+
return { token, parked: false };
|
|
56
85
|
const cp = await cs.get(token);
|
|
57
86
|
if (!cp)
|
|
58
|
-
return false;
|
|
59
|
-
|
|
87
|
+
return { token, parked: false };
|
|
88
|
+
const origin = await findParkOrigin({
|
|
60
89
|
...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
|
|
61
90
|
...(deps.workflowAgentSessionIndex !== undefined && deps.workflowRunStore !== undefined
|
|
62
91
|
? { workflowIndex: deps.workflowAgentSessionIndex, workflowRunStore: deps.workflowRunStore }
|
|
63
92
|
: {}),
|
|
64
|
-
}, { token, scope: cp.scope, sessionId: cp.sessionId })
|
|
93
|
+
}, { token, scope: cp.scope, sessionId: cp.sessionId });
|
|
94
|
+
return { token, parked: origin !== undefined };
|
|
65
95
|
}
|
|
66
96
|
catch {
|
|
67
|
-
return false;
|
|
97
|
+
return { token, parked: false };
|
|
68
98
|
}
|
|
69
99
|
};
|
|
70
100
|
const storedBodyUnservable = async (sessionId) => {
|
|
@@ -77,8 +107,12 @@ export function createApprovalSweeps(ctx) {
|
|
|
77
107
|
};
|
|
78
108
|
let parkedSkipped = 0;
|
|
79
109
|
const replaySkipped = [];
|
|
110
|
+
const seenCards = new Set();
|
|
80
111
|
for (const { sessionId } of expired) {
|
|
81
|
-
|
|
112
|
+
const { token: cardToken, parked } = await cardOf(sessionId);
|
|
113
|
+
if (cardToken !== undefined)
|
|
114
|
+
seenCards.add(cardToken);
|
|
115
|
+
if (parked) {
|
|
82
116
|
parkedSkipped += 1;
|
|
83
117
|
continue;
|
|
84
118
|
}
|
|
@@ -87,9 +121,16 @@ export function createApprovalSweeps(ctx) {
|
|
|
87
121
|
replaySkipped.push({ sessionId, kind: unservable.kind });
|
|
88
122
|
continue;
|
|
89
123
|
}
|
|
90
|
-
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => {
|
|
91
|
-
|
|
124
|
+
await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => {
|
|
125
|
+
if (r.status >= 400)
|
|
126
|
+
noteResumeBlocked(cardToken, sessionId, r.status, r.body);
|
|
127
|
+
else if (cardToken !== undefined)
|
|
128
|
+
blockedWarned.delete(cardToken);
|
|
129
|
+
}, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) }));
|
|
92
130
|
}
|
|
131
|
+
for (const token of [...blockedWarned.keys()])
|
|
132
|
+
if (!seenCards.has(token))
|
|
133
|
+
blockedWarned.delete(token);
|
|
93
134
|
if (parkedSkipped > 0)
|
|
94
135
|
deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
|
|
95
136
|
if (replaySkipped.length > 0) {
|
|
@@ -48,6 +48,7 @@ import type { PermissionMode } from "@sema-agent/core";
|
|
|
48
48
|
import type { CheckpointCtx } from "./route-ctx.js";
|
|
49
49
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
50
50
|
import type { InertToolListEntry } from "../rule-entry-list.js";
|
|
51
|
+
import { type Refusal } from "./refusal-body.js";
|
|
51
52
|
/** 读法记号的闭集(一词 = 当前读法):这一行体里的 `settings.permissions.allow` 是按「交给引擎的 allow 层」写下、也该按它读的
|
|
52
53
|
* (与能力位 `settings.permissions.allowLayer` 同一个说法)。 */
|
|
53
54
|
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["allow_layer"];
|
|
@@ -98,7 +99,7 @@ export declare function withPermissionModeRewritten(ctx: CheckpointCtx, body: Ta
|
|
|
98
99
|
* 记号缺席或认不得 ⇒ 不是「按当前读法写下」的证据,与 allow 记号同一条纪律。 */
|
|
99
100
|
export declare function permissionModeReadingStated(ctx: CheckpointCtx): boolean;
|
|
100
101
|
/**
|
|
101
|
-
* 第三问的拒答复 —— 码与落点同 allow 记号那一问(`checkpoint.unsupported_version`,409
|
|
102
|
+
* 第三问的拒答复 —— 码与落点同 allow 记号那一问(`checkpoint.unsupported_version`,409;卡仍 pending 时体上的 `retriable:true` 由 {@link resumeRefusalWithCard} 追加,本铸点不带)。
|
|
102
103
|
* 句子说原因(更早的版本在每次续跑重钳运维的 AUTONOMY、没把它记进任务;本版只在提交时记一次 ⇒ 续跑会按任务提交时的原词跑)与两条出路。
|
|
103
104
|
* 🔴 形是给冻结门看的:`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
|
|
104
105
|
*/
|
|
@@ -107,7 +108,6 @@ export declare function permissionModeReadingRefusal(): {
|
|
|
107
108
|
body: {
|
|
108
109
|
error: string;
|
|
109
110
|
errorCode: string;
|
|
110
|
-
retriable: true;
|
|
111
111
|
};
|
|
112
112
|
};
|
|
113
113
|
/**
|
|
@@ -118,7 +118,7 @@ export declare function allowListReadingUnstated(ctx: CheckpointCtx): boolean;
|
|
|
118
118
|
/** 被拒的行是哪一代写的(只在 {@link allowListReadingUnstated} 为真时有意义):记号是 `"statement"` ⇒ 表态时代;其余(缺席 / 认不得)⇒ 名单时代。 */
|
|
119
119
|
export declare function allowListReadingEraOf(ctx: CheckpointCtx): AllowListReadingEra;
|
|
120
120
|
/**
|
|
121
|
-
* 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409
|
|
121
|
+
* 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409;卡仍 pending 时的 `retriable:true` 由 {@link resumeRefusalWithCard} 追加,本铸点不带)。
|
|
122
122
|
* 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子按这一行是哪一代写的分两句(闭集穷举,加一代 ⇒ 编译红):说原因(这条任务停在哪种读法上、
|
|
123
123
|
* 照当前读法续跑会放宽什么)与出路(作为新任务重提,仍停驻的先取消)。
|
|
124
124
|
*
|
|
@@ -129,7 +129,6 @@ export declare function allowListReadingRefusal(era: AllowListReadingEra): {
|
|
|
129
129
|
body: {
|
|
130
130
|
error: string;
|
|
131
131
|
errorCode: string;
|
|
132
|
-
retriable: true;
|
|
133
132
|
};
|
|
134
133
|
};
|
|
135
134
|
/**
|
|
@@ -168,11 +167,24 @@ export interface ResumeRefusalAnswer {
|
|
|
168
167
|
} & Record<string, unknown>;
|
|
169
168
|
}
|
|
170
169
|
/**
|
|
171
|
-
* 「续跑被一条准入策略挡住」的**唯一**铸点:`resolveSpec` 在重放存量体时抛出的 `HttpError`(400 族)折成 409 `resume_blocked_by_policy
|
|
172
|
-
* `
|
|
170
|
+
* 「续跑被一条准入策略挡住」的**唯一**铸点:`resolveSpec` 在重放存量体时抛出的 `HttpError`(400 族)折成 409 `resume_blocked_by_policy`。
|
|
171
|
+
* 入参是内层那只拒的 {@link Refusal}(`HttpError` 经 `http/send.ts` 的 `httpErrorRefusal` 读 —— 拆 `HttpError` 的唯一一处,码的属主 `httpErrorCode`):
|
|
172
|
+
* `blockedBy` = 内层的码,句子 = 前缀 + 内层的句子,内层的机读事实原样随体。{@link storedBodyReplayAnswer} 的工具名清单那一臂与
|
|
173
173
|
* `resume-legs.ts` 的折叠口都调它 —— 句子与码只在这里写一次(冻结门 `api-error-text-freeze` 的模板骨架表锚这一处)。
|
|
174
|
+
*
|
|
175
|
+
* 本铸点**不带** `retriable`:那是调用点的事实(有没有一张待批卡仍在等),由 {@link resumeRefusalWithCard} 一处追加 —— 续跑入口(卡仍 pending、run 行留 `suspended`)
|
|
176
|
+
* 带 `retriable:true`,终态 run 的 steer 在铸唤醒卡之前拒(没有卡)不带。键集 = `errorCode` · `error` · `blockedBy` · (`field` · `reason` · 其余 `extra`,有才在)
|
|
177
|
+
* · (`retriable`,卡在才在)。
|
|
178
|
+
*/
|
|
179
|
+
export declare function resumeBlockedByPolicyAnswer(inner: Refusal): ResumeRefusalAnswer;
|
|
180
|
+
/**
|
|
181
|
+
* 🔴 **三类续跑拒体上 `retriable` 的唯一写点**(本服务重放存量体前的拒、准入策略折叠、引擎拒的续跑):卡仍 pending(`cardPending`)⇒ 体尾追加 `retriable: true`;
|
|
182
|
+
* 否则体原样(键缺席)。这三类拒上规则一条:`retriable` 在场 ⇔ 有一张待批卡仍在等、run 行留 `suspended`(**不是**「原样重试会成功」—— 契约附录 A.1 注那一个意思)。
|
|
183
|
+
* 门型对不上的早拒(`gate_not_resumable` 一族)不经这里、不带它。三个调用点各交自己的事实:
|
|
184
|
+
* {@link storedBodyReplayAnswer}(调用方的 `cardPending`)、`resume-legs.ts` 的准入策略折叠口(续跑入口,恒有卡)与 `checkpointRefusalAnswer`(引擎拒的处置读口答 `retriable`)。
|
|
185
|
+
* 排在体尾:拒绝带来的附带键不得抹掉它。纯函数。
|
|
174
186
|
*/
|
|
175
|
-
export declare function
|
|
187
|
+
export declare function resumeRefusalWithCard(answer: ResumeRefusalAnswer, cardPending: boolean): ResumeRefusalAnswer;
|
|
176
188
|
/**
|
|
177
189
|
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的**日志字段**(三处问点同一形,各自只再加坐标键 `sessionId` / `taskId`):`kind` 判别,allow 臂带 `era`、技能臂带 `layer`,`note` 一句出路。
|
|
178
190
|
* 事件名三个(问点各一:`resume_refused_stored_body` / `steer_wake_park_refused_stored_body` / `deny_sweep_stored_body_skipped`),字段形只有这一只。
|
|
@@ -189,6 +201,11 @@ export declare function storedBodyReplayLogFields(refusal: StoredBodyReplayRefus
|
|
|
189
201
|
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的答复:每条规则一只既有铸点(allow 记号 ⇒ {@link allowListReadingRefusal};模式词记号 ⇒ {@link permissionModeReadingRefusal};
|
|
190
202
|
* 这两只答 `checkpoint.unsupported_version`,体按那一码的键律补两版本键,`readerVersion` = 调用方传的本进程服务版本;工具名清单 ⇒ {@link resumeBlockedByPolicyAnswer},与提交腿的 400
|
|
191
203
|
* (该层的拒码:`request.field_invalid` / `config.agent.unknown_tool` / `config.tool_face_control`)同码同句 —— 体上只有这一个问题时;存量体同时还踩中 `resolveSpec` 更早的拒〔三桶超帽 / 托管姿态〕时,提交侧答更早那条、续跑侧答本句,两边都是拒,指路不同)。
|
|
204
|
+
*
|
|
205
|
+
* 🔴 `cardPending` —— **调用方交出的事实**:这一次拒的时候,有没有一张待批卡仍在等。`retriable:true` 说的恰是「卡仍 pending」,所以它只在卡在时在场(经
|
|
206
|
+
* {@link resumeRefusalWithCard} 一处写):续跑入口(`/decide` · `/resume` · `/wake` · plan_review · workflow 决定的新一跑,五个问点里除下面这一个之外全部)传 `true`;
|
|
207
|
+
* 终态 run 的 `POST /v1/runs/:id/steer` 在**铸唤醒卡之前**就拒,没有卡也没铸卡 ⇒ 传 `false`,体上不带 `retriable`(三条臂同一条律;其余键两种处境逐字相同 —— 同判同答)。
|
|
208
|
+
* 没有默认值:每个调用点必须说出这件事实。
|
|
192
209
|
*/
|
|
193
|
-
export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal, readerVersion: string): ResumeRefusalAnswer;
|
|
210
|
+
export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal, readerVersion: string, cardPending: boolean): ResumeRefusalAnswer;
|
|
194
211
|
//# sourceMappingURL=checkpoint-ctx.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refusalBody } from "./refusal-body.js";
|
|
1
2
|
export const PERMISSIONS_ALLOW_READINGS = ["allow_layer"];
|
|
2
3
|
export const PERMISSION_MODE_READINGS = ["entry_word"];
|
|
3
4
|
export const ALLOW_LIST_READING_ERAS = ["list", "statement"];
|
|
@@ -34,7 +35,6 @@ export function permissionModeReadingRefusal() {
|
|
|
34
35
|
body: {
|
|
35
36
|
error: "this task was suspended by an earlier server version that re-applied this deployment's AUTONOMY on every resume instead of recording it in the task; this version records the operator's plan floor once, when a task is submitted, so continuing would run this task under the permission mode it was submitted with rather than the one it ran under. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel); to let it finish with read-only hands instead, set AUTONOMY=read-only until it does.",
|
|
36
37
|
errorCode: "checkpoint.unsupported_version",
|
|
37
|
-
retriable: true,
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
40
|
}
|
|
@@ -56,7 +56,6 @@ export function allowListReadingRefusal(era) {
|
|
|
56
56
|
body: {
|
|
57
57
|
error: "this task was suspended by an earlier server version that did not record how it read settings.permissions.allow (7.114.0 and earlier read it as a whitelist: every tool not on the list was refused); this version hands that list to the engine's allow layer, where each entry is a standing yes for the calls it covers, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
|
|
58
58
|
errorCode: "checkpoint.unsupported_version",
|
|
59
|
-
retriable: true,
|
|
60
59
|
},
|
|
61
60
|
};
|
|
62
61
|
case "statement":
|
|
@@ -65,7 +64,6 @@ export function allowListReadingRefusal(era) {
|
|
|
65
64
|
body: {
|
|
66
65
|
error: "this task was suspended by an earlier server version (7.115.0 to 7.116.x) that read settings.permissions.allow as a statement that the named tools are allowed, which answered only the question nobody else had answered and never cleared an ordinary question such as a file write or a shell command; this version hands that list to the engine's allow layer, where an entry also clears those ordinary questions, so continuing would run calls without asking that the task was submitted to ask about. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel).",
|
|
67
66
|
errorCode: "checkpoint.unsupported_version",
|
|
68
|
-
retriable: true,
|
|
69
67
|
},
|
|
70
68
|
};
|
|
71
69
|
default: {
|
|
@@ -77,12 +75,15 @@ export function allowListReadingRefusal(era) {
|
|
|
77
75
|
export function buildCheckpointVersionKeys(writerVersion, readerVersion) {
|
|
78
76
|
return writerVersion !== undefined ? { writerVersion, readerVersion } : {};
|
|
79
77
|
}
|
|
80
|
-
export function resumeBlockedByPolicyAnswer(
|
|
78
|
+
export function resumeBlockedByPolicyAnswer(inner) {
|
|
81
79
|
return {
|
|
82
80
|
status: 409,
|
|
83
|
-
body: {
|
|
81
|
+
body: refusalBody({ errorCode: "resume_blocked_by_policy", message: `resume blocked by an admission policy change: ${inner.message}`, facts: { blockedBy: inner.errorCode, ...inner.facts } }),
|
|
84
82
|
};
|
|
85
83
|
}
|
|
84
|
+
export function resumeRefusalWithCard(answer, cardPending) {
|
|
85
|
+
return cardPending ? { status: answer.status, body: { ...answer.body, retriable: true } } : answer;
|
|
86
|
+
}
|
|
86
87
|
export function storedBodyReplayLogFields(refusal) {
|
|
87
88
|
switch (refusal.kind) {
|
|
88
89
|
case "allow_list_reading":
|
|
@@ -97,14 +98,17 @@ export function storedBodyReplayLogFields(refusal) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
export function storedBodyReplayAnswer(refusal, readerVersion) {
|
|
101
|
+
export function storedBodyReplayAnswer(refusal, readerVersion, cardPending) {
|
|
102
|
+
return resumeRefusalWithCard(storedBodyReplayArmAnswer(refusal, readerVersion), cardPending);
|
|
103
|
+
}
|
|
104
|
+
function storedBodyReplayArmAnswer(refusal, readerVersion) {
|
|
101
105
|
switch (refusal.kind) {
|
|
102
106
|
case "allow_list_reading": {
|
|
103
107
|
const answer = allowListReadingRefusal(refusal.era);
|
|
104
108
|
return { status: answer.status, body: { ...answer.body, ...buildCheckpointVersionKeys(refusal.writerVersion, readerVersion) } };
|
|
105
109
|
}
|
|
106
110
|
case "inert_tool_list":
|
|
107
|
-
return resumeBlockedByPolicyAnswer(refusal.entry.
|
|
111
|
+
return resumeBlockedByPolicyAnswer(refusal.entry.extra === undefined ? { errorCode: refusal.entry.code, message: refusal.entry.message } : { errorCode: refusal.entry.code, message: refusal.entry.message, facts: refusal.entry.extra });
|
|
108
112
|
case "permission_mode_reading": {
|
|
109
113
|
const answer = permissionModeReadingRefusal();
|
|
110
114
|
return { status: answer.status, body: { ...answer.body, ...buildCheckpointVersionKeys(refusal.writerVersion, readerVersion) } };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一条提交腿**刚认领**的 run 在起跑之前就收场 —— 撤掉本请求建的东西的**唯一**写法。两个调用点:device 落点拒
|
|
3
|
+
* (`device-placement.ts` `commitPlacement`)与续跑输入写不进(`submit-ctx.ts` `persistSubmitCtx`)。
|
|
4
|
+
*
|
|
5
|
+
* 撤两步,**各自独立**(一步失败不拦另一步,残余最小):
|
|
6
|
+
* · 终结这条 run 行(`setTerminal` 是按 taskId 的 CAS,首写者赢 ⇒ 重复调用是 no-op)—— 随之释放它的 `task_active` 认领;
|
|
7
|
+
* · 放开它占的提交幂等键(拒绝答复邀请重试;同键重试必须真跑,不是回放这次拒绝 —— `RunStore.releaseIdemKey`)。
|
|
8
|
+
* 撤不掉不致命、但要响:每一步的失败各交给调用方按自己的名字留痕(`onFailure`)。终结没落下 ⇒ 此刻心跳还没起来,`reapStale`
|
|
9
|
+
* 在 `REAP_RUN_STALE_SEC` 之后把这条行翻 failed 并释放会话;幂等键没放开 ⇒ 同键重提回放这条行的回执(两形都是修前每一次的样子)。
|
|
10
|
+
*/
|
|
11
|
+
import type { RunStore } from "../plugins/store-backend.js";
|
|
12
|
+
export declare function releaseClaimedRun(runStore: Pick<RunStore, "setTerminal" | "releaseIdemKey">, taskId: string, why: string, onFailure: (err: unknown, step: "terminal" | "idem_key") => void): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=claimed-run.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function releaseClaimedRun(runStore, taskId, why, onFailure) {
|
|
2
|
+
try {
|
|
3
|
+
await runStore.setTerminal(taskId, "failed", null, why);
|
|
4
|
+
}
|
|
5
|
+
catch (err) {
|
|
6
|
+
onFailure(err, "terminal");
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
await runStore.releaseIdemKey(taskId);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
onFailure(err, "idem_key");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=claimed-run.js.map
|
|
@@ -2,6 +2,7 @@ import { createCodedHttpError, ssoVerifiedScope } from "../security.js";
|
|
|
2
2
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
3
3
|
import { DEVICE_ERROR_CODES, assertDeviceIdShape, } from "../device-store.js";
|
|
4
4
|
import { buildDeviceRejection } from "../device-enrollment.js";
|
|
5
|
+
import { releaseClaimedRun } from "./claimed-run.js";
|
|
5
6
|
export function deviceOwnerDomainOf(req, config) {
|
|
6
7
|
const subject = gatedPrincipal(req, config);
|
|
7
8
|
if (subject === undefined)
|
|
@@ -89,16 +90,10 @@ export async function commitPlacement(ctx, runStore, taskId, intent) {
|
|
|
89
90
|
if (intent === undefined)
|
|
90
91
|
return null;
|
|
91
92
|
const enrollment = ctx.deviceEnrollment;
|
|
92
|
-
const releaseRun =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
ctx.logger?.warn("device_placement_run_release_failed", { taskId, err: err instanceof Error ? err.message : String(err) });
|
|
99
|
-
ctx.metrics?.inc("device_placement_run_release_failed_total");
|
|
100
|
-
}
|
|
101
|
-
};
|
|
93
|
+
const releaseRun = (why) => releaseClaimedRun(runStore, taskId, why, (err, step) => {
|
|
94
|
+
ctx.logger?.warn("device_placement_run_release_failed", { taskId, step, err: err instanceof Error ? err.message : String(err) });
|
|
95
|
+
ctx.metrics?.inc("device_placement_run_release_failed_total");
|
|
96
|
+
});
|
|
102
97
|
if (!enrollment) {
|
|
103
98
|
await releaseRun("device placement could not be committed: the device lane's admission chain is not wired");
|
|
104
99
|
throw new Error("commitPlacement: a device placement intent was minted without a DeviceEnrollment — assembly bug (both are read from the same ServiceDeps)");
|
package/dist/http/dispatch.d.ts
CHANGED
|
@@ -18,10 +18,6 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
18
18
|
import type { ServiceConfig } from "../config-types.js";
|
|
19
19
|
import type { RouteCtxBase, RunRegistry } from "./route-ctx.js";
|
|
20
20
|
import type { FlatServiceDeps } from "./server.js";
|
|
21
|
-
/** 「这台部署配了任何一把 service 凭证吗」—— fail-closed 写门族的真值源。**单一属主**:`handle()` 的
|
|
22
|
-
* 凭证门、[ref] 的 parked 赎回席、以及 [ref] 的 consolidation 阀门启动期告警读同一句(手抄迟早
|
|
23
|
-
* 只改一处,而这条判据决定的是「无凭证部署上写口开不开」)。导出仅为最后一个消费点(main.ts)。 */
|
|
24
|
-
export declare function hasServiceAuth(config: ServiceConfig): boolean;
|
|
25
21
|
/**
|
|
26
22
|
* 🔴 S-672(test T-72 [ref];主会话裁 ①,分诊 [ref])—— 全局服务凭据门**之前**那几族只读路
|
|
27
23
|
* (`route-table.ts` 里 `position: "read-credential"` 的行)的**唯一**鉴权谓词:「服务凭据 ∨ 已验 principal」。
|