@sema-agent/server 7.120.0 → 7.122.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.
Files changed (149) hide show
  1. package/MIGRATION.md +173 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +97 -29
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  6. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  7. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  8. package/dist/approval-reconciler.d.ts +8 -0
  9. package/dist/approval-reconciler.js +72 -52
  10. package/dist/auto-mode-face.d.ts +9 -2
  11. package/dist/auto-mode-face.js +5 -3
  12. package/dist/bench/s1/live-deps.js +1 -0
  13. package/dist/boot/config-center.d.ts +5 -7
  14. package/dist/boot/config-center.js +33 -9
  15. package/dist/boot/engine-lease.d.ts +1 -1
  16. package/dist/boot/leader.js +1 -0
  17. package/dist/boot/reapers.d.ts +3 -0
  18. package/dist/boot/reapers.js +2 -1
  19. package/dist/boot/resolve-spec.js +2 -4
  20. package/dist/boot/shutdown.d.ts +9 -10
  21. package/dist/boot/shutdown.js +59 -16
  22. package/dist/boot/stage-08-reapers.js +2 -2
  23. package/dist/boot/stage-09-leader.d.ts +2 -9
  24. package/dist/boot/stage-10-http-server.d.ts +1 -9
  25. package/dist/boot/stage-10-http-server.js +31 -8
  26. package/dist/capabilities/composite-members.d.ts +1 -1
  27. package/dist/capabilities/composite-members.js +1 -1
  28. package/dist/capabilities/memory-notice.d.ts +1 -1
  29. package/dist/config-catalog.js +12 -6
  30. package/dist/config-center/center-request.d.ts +13 -1
  31. package/dist/config-center/center-request.js +25 -10
  32. package/dist/config-types.d.ts +44 -5
  33. package/dist/config-types.js +6 -0
  34. package/dist/config.d.ts +35 -6
  35. package/dist/config.js +113 -33
  36. package/dist/fleet/drain-entry.d.ts +112 -0
  37. package/dist/fleet/drain-entry.js +38 -0
  38. package/dist/fleet/engine-signal.d.ts +144 -0
  39. package/dist/fleet/engine-signal.js +60 -0
  40. package/dist/fleet/serving-lease.d.ts +47 -0
  41. package/dist/fleet/serving-lease.js +233 -0
  42. package/dist/fleet-client.d.ts +35 -19
  43. package/dist/fleet-client.js +49 -25
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +5 -7
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +33 -16
  50. package/dist/http/health-self-report.d.ts +60 -0
  51. package/dist/http/health-self-report.js +53 -0
  52. package/dist/http/principal-gate.d.ts +8 -1
  53. package/dist/http/principal-gate.js +1 -0
  54. package/dist/http/readiness.d.ts +25 -5
  55. package/dist/http/readiness.js +31 -6
  56. package/dist/http/resume-legs.js +9 -9
  57. package/dist/http/route-ctx.d.ts +25 -7
  58. package/dist/http/route-table.d.ts +6 -0
  59. package/dist/http/route-table.js +8 -1
  60. package/dist/http/routes/a2a-serve.d.ts +1 -1
  61. package/dist/http/routes/a2a-serve.js +5 -5
  62. package/dist/http/routes/admin-drain.d.ts +15 -31
  63. package/dist/http/routes/admin-drain.js +28 -10
  64. package/dist/http/routes/approvals-assistant.d.ts +4 -3
  65. package/dist/http/routes/approvals-assistant.js +9 -9
  66. package/dist/http/routes/capabilities.js +7 -5
  67. package/dist/http/routes/devices.js +3 -2
  68. package/dist/http/routes/fleet.d.ts +1 -0
  69. package/dist/http/routes/fleet.js +1 -1
  70. package/dist/http/routes/images.d.ts +1 -0
  71. package/dist/http/routes/images.js +1 -1
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +7 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +4 -3
  79. package/dist/http/routes/runs.d.ts +4 -2
  80. package/dist/http/routes/runs.js +7 -6
  81. package/dist/http/routes/sessions.d.ts +1 -0
  82. package/dist/http/routes/sessions.js +1 -1
  83. package/dist/http/routes/side-query.d.ts +1 -1
  84. package/dist/http/routes/side-query.js +1 -1
  85. package/dist/http/routes/tasks.d.ts +2 -2
  86. package/dist/http/routes/tasks.js +3 -3
  87. package/dist/http/routes/trace-usage.d.ts +1 -0
  88. package/dist/http/routes/trace-usage.js +1 -1
  89. package/dist/http/routes/workflows.d.ts +1 -0
  90. package/dist/http/routes/workflows.js +1 -1
  91. package/dist/http/send.d.ts +13 -0
  92. package/dist/http/send.js +9 -0
  93. package/dist/http/server.d.ts +10 -21
  94. package/dist/http/server.js +20 -1
  95. package/dist/http/stored-body-replay.d.ts +27 -0
  96. package/dist/http/stored-body-replay.js +23 -0
  97. package/dist/http/wire-types.d.ts +7 -1
  98. package/dist/leader/leader.d.ts +22 -1
  99. package/dist/leader/leader.js +4 -1
  100. package/dist/leader/repair-wire.d.ts +1 -1
  101. package/dist/leader/repair-wire.js +2 -2
  102. package/dist/leader/run-entry.d.ts +27 -0
  103. package/dist/leader/run-entry.js +29 -0
  104. package/dist/leader/wire.d.ts +10 -0
  105. package/dist/leader/wire.js +15 -10
  106. package/dist/model-reach-scope.d.ts +15 -3
  107. package/dist/model-reach-scope.js +3 -2
  108. package/dist/model-route-url.d.ts +13 -20
  109. package/dist/model-route-url.js +28 -24
  110. package/dist/model-unreachable-notice.d.ts +2 -1
  111. package/dist/model-unreachable-notice.js +1 -0
  112. package/dist/observability/fail-open.d.ts +35 -2
  113. package/dist/observability/fail-open.js +20 -0
  114. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  115. package/dist/plugins/approval-ask-store-file.js +3 -0
  116. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  117. package/dist/plugins/approval-ask-store-memory.js +6 -0
  118. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  119. package/dist/plugins/approval-ask-store-sql.js +4 -0
  120. package/dist/plugins/one-time-migrations.js +14 -1
  121. package/dist/plugins/park-commit-watch.d.ts +19 -5
  122. package/dist/plugins/park-commit-watch.js +24 -17
  123. package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
  124. package/dist/plugins/remote-env-device.js +0 -1
  125. package/dist/request-key-closure.js +1 -1
  126. package/dist/router/route-orchestration.d.ts +4 -2
  127. package/dist/router/route-orchestration.js +2 -1
  128. package/dist/rules-consent.d.ts +6 -2
  129. package/dist/rules-consent.js +3 -0
  130. package/dist/run-local.js +3 -1
  131. package/dist/runs.js +1 -1
  132. package/dist/runtime-governance.d.ts +44 -32
  133. package/dist/runtime-governance.js +23 -9
  134. package/dist/sighup-idle.d.ts +7 -1
  135. package/dist/sighup-idle.js +4 -4
  136. package/dist/task-settings.d.ts +37 -14
  137. package/dist/task-settings.js +20 -20
  138. package/dist/tool-approval.d.ts +13 -0
  139. package/dist/tool-approval.js +17 -2
  140. package/dist/trace/core-keyset-guard.d.ts +2 -2
  141. package/dist/trace/redact.d.ts +3 -3
  142. package/dist/trace/self-minted-wire.d.ts +64 -0
  143. package/dist/trace/self-minted-wire.js +30 -0
  144. package/dist/unattended-permission-mode.d.ts +17 -13
  145. package/dist/unattended-permission-mode.js +3 -1
  146. package/dist/url-shape.d.ts +31 -0
  147. package/dist/url-shape.js +41 -0
  148. package/package.json +3 -3
  149. package/scripts/drain-check-local.mjs +1 -1
@@ -31,6 +31,23 @@ export { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDis
31
31
  export { MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills };
32
32
  import { explicitOperatorOk, isOperator } from "./principal-gate.js";
33
33
  export { explicitOperatorOk, isOperator };
34
+ class DrainYieldingPreemptBook extends Map {
35
+ drainingNow;
36
+ constructor(drainingNow) {
37
+ super();
38
+ this.drainingNow = drainingNow;
39
+ }
40
+ set(taskId, ctrl) {
41
+ super.set(taskId, ctrl);
42
+ if (this.drainingNow())
43
+ ctrl.abort();
44
+ return this;
45
+ }
46
+ yieldAll() {
47
+ for (const ctrl of this.values())
48
+ ctrl.abort();
49
+ }
50
+ }
34
51
  export function flattenServiceDeps(deps) {
35
52
  const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
36
53
  if (!(stores || coordinators || seams || observability || governance || deployment || knobs))
@@ -44,7 +61,7 @@ export function createHttpServer(rawDeps) {
44
61
  }
45
62
  const idemCache = new IdempotencyCache();
46
63
  const inflightRuns = new Map();
47
- const preemptableRuns = new Map();
64
+ const preemptableRuns = new DrainYieldingPreemptBook(() => deps.drainState?.draining === true);
48
65
  const steerableRuns = new Map();
49
66
  const wakeParkMints = new Map();
50
67
  const counters = { uncountedBillableInflight: 0, admittedInflight: 0 };
@@ -58,6 +75,8 @@ export function createHttpServer(rawDeps) {
58
75
  };
59
76
  if (deps.drainState)
60
77
  deps.drainState.lastActivityAt = () => lastBillableActivityAt;
78
+ if (deps.drainState)
79
+ deps.drainState.yieldInflight = () => preemptableRuns.yieldAll();
61
80
  if (deps.hookWakeBus) {
62
81
  deps.hookWakeBus.deliver = async (sessionId, text) => {
63
82
  try {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 「这条停驻行的持久化体在本版上能否重放」—— **唯一**判据(三问一只;五个问点同调:续跑驱动段 `resume-legs.ts` `resolveResumeSpecOrBlocked`、
3
+ * plan_review 的 prepare 段、SLA 拒扫腿的跳过臂 `approval-sweeps.ts`、终态 run 的 steer 铸唤醒卡之前 `routes/runs.ts`;`resolveSpec` 本身留作提交腿的拒点)。
4
+ * 三问的规则、记号、拒句与删除条件的属主 = `http/checkpoint-ctx.ts` 顶注;答复 = 那里的 `storedBodyReplayAnswer`。
5
+ *
6
+ * 为什么判据住在这里而不是 `checkpoint-ctx.ts`:第三问要读体的**生效模式词**(`task-settings.ts` `storedBodyPermissionMode`,请求设置的解析层)与本部署此刻的
7
+ * **入口地板**(`runtime-governance.ts` `autonomyModeFloor`)—— 这两只属主带着配置层的包外依赖;`checkpoint-ctx.ts` 被随包的本地排空脚本经编译产物 import,
8
+ * 它的闭包只许 node 内建 / 引擎包根(`test/drain-check-local.test.ts` 的闭包门)。判据只有这一只,前两问的谓词照旧住在各自属主里。
9
+ */
10
+ import { type PermissionMode } from "@sema-agent/core";
11
+ import { type Autonomy } from "../runtime-governance.js";
12
+ import { type StoredBodyReplayRefusal } from "./checkpoint-ctx.js";
13
+ import type { CheckpointCtx } from "./route-ctx.js";
14
+ /**
15
+ * 第三问:本行体里的模式词能否按当前读法重放。不能 ⇔ 本部署此刻有入口地板 ∧ 行上没有当前的模式词读法记号 ∧ 体的生效词被地板钳会变
16
+ * (core `clampPermissionMode`,与提交入口的盖章同一只钳)⇒ 返回拒因的两个事实(体的词、地板);能 ⇒ `undefined`。
17
+ */
18
+ export declare function permissionModeReadingUnstated(ctx: CheckpointCtx, floor: PermissionMode | undefined): {
19
+ stored: PermissionMode | undefined;
20
+ floor: PermissionMode;
21
+ } | undefined;
22
+ /**
23
+ * 三问一只判据:不能重放 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。顺序:allow 读法记号 → 技能拒侧读成空 → 模式词读法记号(几问同中时答先中的那一问 ——
24
+ * 任一问中都不重放,句子只需一句)。`autonomy` = 本部署**此刻**的 `config.autonomy`(活 config:热改即生效);地板由它的唯一属主算,问点不自己算。
25
+ */
26
+ export declare function storedBodyReplayRefusal(ctx: CheckpointCtx, autonomy: Autonomy | undefined): StoredBodyReplayRefusal | undefined;
27
+ //# sourceMappingURL=stored-body-replay.d.ts.map
@@ -0,0 +1,23 @@
1
+ import { clampPermissionMode } from "@sema-agent/core";
2
+ import { storedBodyPermissionMode } from "../task-settings.js";
3
+ import { autonomyModeFloor } from "../runtime-governance.js";
4
+ import { skillDenyInertEntryOf } from "../rule-entry-list.js";
5
+ import { allowListReadingEraOf, allowListReadingUnstated, permissionModeReadingStated } from "./checkpoint-ctx.js";
6
+ export function permissionModeReadingUnstated(ctx, floor) {
7
+ if (floor === undefined || permissionModeReadingStated(ctx))
8
+ return undefined;
9
+ const stored = storedBodyPermissionMode(ctx.body);
10
+ return clampPermissionMode(stored, floor).effective === stored ? undefined : { stored, floor };
11
+ }
12
+ export function storedBodyReplayRefusal(ctx, autonomy) {
13
+ if (allowListReadingUnstated(ctx))
14
+ return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx) };
15
+ const entry = skillDenyInertEntryOf(ctx.body);
16
+ if (entry !== undefined)
17
+ return { kind: "skill_deny_inert", entry };
18
+ const mode = permissionModeReadingUnstated(ctx, autonomyModeFloor(autonomy));
19
+ if (mode !== undefined)
20
+ return { kind: "permission_mode_reading", ...mode };
21
+ return undefined;
22
+ }
23
+ //# sourceMappingURL=stored-body-replay.js.map
@@ -6,7 +6,7 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec, TaskSpec } from "@sema-agent/core";
10
10
  import type { wiringManifestFaceEventData } from "../trace/project.js";
11
11
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
12
12
  export interface TaskRequestBody {
@@ -63,6 +63,12 @@ export interface TaskRequestBody {
63
63
  * question (raised on the board). Rejected fail-loud when `systemPrompt` is an already-assembled prompt
64
64
  * (anchors detected) — that assembler arm cannot mount the rider (silent drop would belie the capability bit). */
65
65
  appendSystemPrompt?: string;
66
+ /** 7.121.0:输出风格的**每轮一句**(引擎 `TaskSpec.outputStyle`,core 7.39.0 起):原样投给引擎,引擎在每个附件周期用自己的标记帧陈述
67
+ * `"<name> output style is active. <turnReminder>"`(`turnReminder` 缺席 ⇒ 引擎的缺省句),回声为 `steering_injected` `source:"output_style"`。
68
+ * **一个事实一个座位**:风格的**正文**(指南全文)走 `settings.outputStyle`(折进 `appendSystemPrompt` 那一块),这里只放名字与每轮那一句;
69
+ * 只在非缺省风格时声明。值形的属主是引擎(`{ name, turnReminder? }` 恰两键、`name` 非空且 ≤ `OUTPUT_STYLE_NAME_MAX`、`turnReminder` 在场即非空串):
70
+ * 本服务**不另判**,坏值在 prepare 被引擎拒 `config.output_style_invalid`(不会被静默忽略)。能力位 `outputStyle`。 */
71
+ outputStyle?: TaskSpec["outputStyle"];
66
72
  /** Explicit model selection (CLI `/model` picker + `run --model <ref>`). An INTENT by catalog
67
73
  * name (e.g. "deepseek-pro") — resolveSpec gates it against the SAME allow-list as an inline `@name` mention
68
74
  * (config.models keys), so it picks WHICH configured model but can never inject one. Wins over an inline
@@ -15,7 +15,8 @@
15
15
  import { type RunnerLike, type WorkerReport } from "./fanout.js";
16
16
  import { type ExecStreamLike } from "./diffout.js";
17
17
  import { type MergeDeps, type MergeResult } from "./merge.js";
18
- import { type TaskSpec, type VerifyConfig, type CheckpointToken, type TerminalCause, type ToolSpec, type RepairTerminal, type OracleTier, type RepairResult } from "@sema-agent/core";
18
+ import { type PermissionMode, type TaskSpec, type VerifyConfig, type CheckpointToken, type TerminalCause, type ToolSpec, type RepairTerminal, type OracleTier, type RepairResult } from "@sema-agent/core";
19
+ import { type UnattendedLegSpec } from "../unattended-permission-mode.js";
19
20
  /** [ref] Slice 6: drive a worker's resource-suspend auto-resume to a terminal (or bound) state. Exported
20
21
  * for unit tests. Loops `resume(token, slice)` WHILE the result's terminal cause is a park on a
21
22
  * `resource_limit` gate. Exits on: a terminal cause that is not a park, a park on any OTHER gate
@@ -64,7 +65,27 @@ export interface ProvisionedWorker {
64
65
  * python3/tests → PARTIAL. Provision a read-effect exec/probe tool here so the verifier can actually verify. */
65
66
  verifierTools?: ToolSpec[];
66
67
  }
68
+ /**
69
+ * 一条 leader run 的**入口体**(提交体去掉 objective 的那一半)—— 运维 `AUTONOMY` 的模式词地板在这条 run 出生的那一拍读一次、经受理门同一只盖章
70
+ * 盖出来的结果(`run-entry.ts` `leaderRunEntry`);地板缺席 ⇒ `{}`。leader 请求不带调用方的模式词,所以入口词就是地板本身(与 run-local 同)。
71
+ * 本 run 铸的每一条 spec 经 {@link withLeaderLeg} 从这一份取词;worker 的续跑体由它展开(`{ ...entry, objective }`)⇒ 首跑、续跑、修复腿同一个词。
72
+ * 入口词是 `plan` 的 run 进不了这条车道(`run-entry.ts` `leaderEntryRefusal`:worker 会话没有计划审阅通道)—— 受理口与出生处同判,到不了 worker。
73
+ * 热改 `AUTONOMY` 只影响之后起的 leader run:在飞的 run 保持它出生时盖下的这一份。
74
+ */
75
+ export interface LeaderRunEntry {
76
+ readonly permissionMode?: PermissionMode;
77
+ }
78
+ /**
79
+ * leader 车道铸 spec 的**唯一**合并口:入口体的词写进 spec,再交给无人值守腿的合并口(`withUnattendedLeg` 的 `default` 钳在入口词之下)。
80
+ * 入口体无词 ⇒ spec 原物交给 `withUnattendedLeg`,与不经本口逐字同。本车道的 spec 自己不带模式词(子任务体只有 objective,其余并进的键是预算 / 审批 / 身份),
81
+ * 所以这里是「写」而不是「钳」。五个出生点(worker 首跑的工厂 / 静态两条车道、集成修复、模块门修复、单代理修复环)都调它、不直调 `withUnattendedLeg`
82
+ * (`test/leader-entry-floor.test.ts` 按语法树数调用点):「这条腿的词从哪来」只有入口体一个答案。
83
+ */
84
+ export declare function withLeaderLeg<S extends Partial<TaskSpec>>(spec: S, entry: LeaderRunEntry): S & UnattendedLegSpec;
67
85
  export interface LeaderDeps {
86
+ /** 本 run 的入口体({@link LeaderRunEntry}:运维地板在 run 出生时盖一次)。必填:没有地板的装配写 `{}` —— 缺席不是「没有地板」的合法拼法。
87
+ * 模块门修复与单代理修复环这两个出生点从这里取词(经 {@link withLeaderLeg})。 */
88
+ entry: LeaderRunEntry;
68
89
  /** Plan a task into N disjoint-file sub-tasks (MVP: simple/hand-authored; adaptive replan = scale). */
69
90
  plan: (task: string) => Promise<SubtaskSpec[]>;
70
91
  /** Provision a leader-owned worker (its env seeded with base at `branch` + an env-bound runner). */
@@ -16,6 +16,9 @@ export async function autoResumeOnResource(initial, signal, resume, maxSlices =
16
16
  }
17
17
  return vr;
18
18
  }
19
+ export function withLeaderLeg(spec, entry) {
20
+ return withUnattendedLeg(entry.permissionMode === undefined ? spec : { ...spec, permissionMode: entry.permissionMode });
21
+ }
19
22
  export async function runLeaderTask(task, durableBaseSha, deps) {
20
23
  let subs;
21
24
  try {
@@ -201,7 +204,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
201
204
  }
202
205
  deps.logger?.info?.("module_repair", { workerId: r.workerId, attempt: attempt + 1, reason: redactHead(verdict.reason ?? "", 500) });
203
206
  try {
204
- const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` })).result();
207
+ const rr = await w.runner.runTaskStream(withLeaderLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` }, deps.entry)).result();
205
208
  if (r.stats && rr?.stats)
206
209
  r.stats = { ...r.stats, tokens: (r.stats.tokens ?? 0) + (rr.stats.tokens ?? 0), costMicroUsd: (r.stats.costMicroUsd ?? 0) + (rr.stats.costMicroUsd ?? 0) };
207
210
  }
@@ -31,7 +31,7 @@
31
31
  import { type Runner, type TaskSpec, type ExecutionEnv, type ExecStep, type OracleTier, type RepairResult, type RepairBundle, type RepairTerminal } from "@sema-agent/core";
32
32
  import { type WorkerDiff } from "./repair-oracle.js";
33
33
  import type { GraderHandle } from "./grader-env-factory.js";
34
- import type { LeaderDeps, ProvisionedWorker, SubtaskSpec } from "./leader.js";
34
+ import { type LeaderDeps, type ProvisionedWorker, type SubtaskSpec } from "./leader.js";
35
35
  import type { WorkerReport } from "./fanout.js";
36
36
  /** The per-run inputs the leader's single-agent repair loop needs, resolved by the wire (§3.3). */
37
37
  export interface RepairWireCfg {
@@ -1,8 +1,8 @@
1
1
  import { runRepairLoop, delimitUntrusted, } from "@sema-agent/core";
2
2
  import { mkRepairOracle } from "./repair-oracle.js";
3
+ import { withLeaderLeg } from "./leader.js";
3
4
  import { outwardPlane } from "../terminal.js";
4
5
  import { redactHead } from "../observability/run-terminal.js";
5
- import { withUnattendedLeg } from "../unattended-permission-mode.js";
6
6
  function statusForTerminal(t) {
7
7
  return t === "fixed" || t === "candidate_only" ? "completed" : "failed";
8
8
  }
@@ -87,7 +87,7 @@ export function attachRepairLoopDeps(deps, resolve) {
87
87
  deps.repairLoop = {
88
88
  run: async (solo, sub) => {
89
89
  const cfg = await resolve(solo, sub);
90
- const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId });
90
+ const implSpec = withLeaderLeg({ ...sub.spec, sessionId: solo.sessionId }, deps.entry);
91
91
  return runRepairLoopForLeader(solo, implSpec, cfg);
92
92
  },
93
93
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 一条 leader run 的**入口体**与「它进不进得了 leader 车道」的唯一判据。
3
+ *
4
+ * 规则(与受理门、run-local 同一条):运维 `AUTONOMY=plan` 是提交入口的模式词地板,盖在每一个 spec 出生点。leader 请求不带调用方的模式词,
5
+ * 所以入口体是一份空体盖上地板(受理门同一只盖章 `task-settings.ts` `stampEntryPermissionMode`;地板的唯一属主 `runtime-governance.ts`
6
+ * `autonomyModeFloor`)。
7
+ *
8
+ * `plan` 是**生命周期词**:它靠人审通道(`plan_review`)出口 —— 一个人批准计划,run 才离开 plan。leader 的 worker 会话**没有**那条通道
9
+ * (计划审阅口按 HTTP run 行找卡,worker 会话没有 run 行;审批回决口答不是工具审批、续跑口也拒)⇒ 入口词是 `plan` 的 leader run 交出计划就停在
10
+ * 一张没有端点能决的卡上,写也无人可答。所以这一档在 leader 车道上**响亮拒**(fail-closed):不静默降级成别的模式、不换成只读手、不给 leader 另造审阅口。
11
+ *
12
+ * 问点两个,调同一只 {@link leaderEntryRefusal}:HTTP 受理口(`http/routes/leader.ts`,422,一条 run 都不建)与 run 出生处(`leader/wire.ts`,
13
+ * 受理之后才被热改成 plan 的那一窄窗,以及不经 HTTP 的调用方 —— run 以同一句拒收场,一只 worker 都不 provision)。
14
+ */
15
+ import { type Autonomy } from "../runtime-governance.js";
16
+ import type { LeaderRunEntry } from "./leader.js";
17
+ /** 一条 leader run 的入口体:地板读一次、受理门同一只盖章(地板缺席 ⇒ `{}`)。调用方读**活** config(热改只影响之后起的 run)。 */
18
+ export declare function leaderRunEntry(autonomy: Autonomy | undefined): LeaderRunEntry;
19
+ /** 拒句:原因(运维的 plan 地板 × 这条车道没有计划审阅通道)+ 两条出路(改走有计划审阅的 run 车道 / 运维换档)。不含任何配置值之外的回显。
20
+ * 受理口(HTTP 422 的 `error`)与 run 出生处(run 的 `error`)读的是这一份;字节由 `test/api-error-text-freeze.test.ts` 的常量锚格冻。 */
21
+ export declare const LEADER_PLAN_ENTRY_REFUSAL: string;
22
+ /**
23
+ * 入口词进不进得了 leader 车道 —— **唯一**判据。返回拒句;`undefined` = 进得了。闭集穷尽 switch:引擎的模式词表加一词 ⇒ 这里编译红,
24
+ * 逼着回答「那个词靠不靠人审通道出口」。
25
+ */
26
+ export declare function leaderEntryRefusal(entry: LeaderRunEntry): string | undefined;
27
+ //# sourceMappingURL=run-entry.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { autonomyModeFloor } from "../runtime-governance.js";
2
+ import { stampEntryPermissionMode } from "../task-settings.js";
3
+ export function leaderRunEntry(autonomy) {
4
+ return stampEntryPermissionMode({}, autonomyModeFloor(autonomy));
5
+ }
6
+ export const LEADER_PLAN_ENTRY_REFUSAL = "this deployment runs with AUTONOMY=plan, which starts every task in plan mode and lets it out of plan mode only when a person approves its plan through plan review; " +
7
+ "a leader task's workers have no plan review channel, so they could neither write nor hand their plan to anyone who can approve it. Nothing was started. " +
8
+ "Submit the work as a run instead (POST /v1/runs, whose plan is reviewed through POST /v1/assistant/tasks/:id/plan_review), " +
9
+ "or ask the operator to set AUTONOMY=read-only or AUTONOMY=ask, under which leader tasks run.";
10
+ export function leaderEntryRefusal(entry) {
11
+ const mode = entry.permissionMode;
12
+ if (mode === undefined)
13
+ return undefined;
14
+ switch (mode) {
15
+ case "plan":
16
+ return LEADER_PLAN_ENTRY_REFUSAL;
17
+ case "default":
18
+ case "acceptEdits":
19
+ case "auto":
20
+ case "bypassPermissions":
21
+ case "dontAsk":
22
+ return undefined;
23
+ default: {
24
+ const unhandled = mode;
25
+ throw new Error(`leader entry word "${String(unhandled)}" has no lane arm`);
26
+ }
27
+ }
28
+ }
29
+ //# sourceMappingURL=run-entry.js.map
@@ -1,4 +1,5 @@
1
1
  import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core";
2
+ import type { Autonomy } from "../runtime-governance.js";
2
3
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
4
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
5
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
@@ -63,6 +64,15 @@ export interface LeaderWireConfig {
63
64
  * fail-OPEN。改成函数席之后每只 Runner 构造时现读,与主车道经 swap 门换代**同代**;已经准备好的腿
64
65
  * 保留它准备时的那一份(core 的自然快照语义,两条腿同律)。 */
65
66
  deploymentPosture?: () => DeploymentPostureSeats;
67
+ /**
68
+ * 运维 `AUTONOMY`(活 config 的那一位)—— 本车道每条 leader run **出生那一拍**读一次,经 `run-entry.ts` 的 `leaderRunEntry`(入口地板 + 受理门同一只盖章)
69
+ * 盖成这条 run 的入口体(`LeaderDeps.entry`);本 run 铸的每一条 spec 与 worker 的续跑体都从它取词。入口词是 `plan` 的 run 进不了这条车道
70
+ * (`leaderEntryRefusal`:worker 会话没有计划审阅通道)⇒ 出生处即以拒句收场;HTTP 受理口用同一只判据先答 422。
71
+ *
72
+ * 🔴 **函数席**(与 `deploymentPosture` / `durable` 同一条先例):`config.autonomy` 被 config-center 原地热改,boot 期取一次值会把这条腿钉在起服那一刻。
73
+ * **必填**:没有 AUTONOMY 的装配递 `() => undefined` —— 缺席不是「没有地板」的合法拼法(权限轴上不许静默放宽)。
74
+ */
75
+ autonomy: () => Autonomy | undefined;
66
76
  /**
67
77
  * [ref]([ref])—— **计费/遥测追踪席**,原样递给本车道的每一只 Runner。唯一属主 =
68
78
  * `boot/budget-tracing.ts` 的 `createTracer(...)`(主 runner / subRunner / hook runner 经共享基座吃的
@@ -5,7 +5,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
- import { withUnattendedLeg } from "../unattended-permission-mode.js";
8
+ import { leaderEntryRefusal, leaderRunEntry } from "./run-entry.js";
9
9
  import { buildCheckpointCtx } from "../http/checkpoint-ctx.js";
10
10
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
11
11
  import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
@@ -14,7 +14,7 @@ import { gitSubprocessEnv } from "../server-secret-env.js";
14
14
  import { presignS3Url } from "../plugins/s3-presign.js";
15
15
  import { buildUploadScript, buildUploadLoopScript, uploadLoopCommand, uploadLoopScriptPath, uploadStepSuffix, fetchUploadedDiff, withStaging, UPLOAD_LOOP_PERIOD_SEC } from "./diffup.js";
16
16
  import { pullDiff, safeRev } from "./diffout.js";
17
- import { runLeaderTask } from "./leader.js";
17
+ import { runLeaderTask, withLeaderLeg } from "./leader.js";
18
18
  import { attachRepairLoopDeps } from "./repair-wire.js";
19
19
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
20
20
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
@@ -171,7 +171,7 @@ export function createLeaderRunner(cfg) {
171
171
  const p = currentPrincipal();
172
172
  return p ? { principal: p } : {};
173
173
  };
174
- const mkRepair = (rawEnv, repoDir, oracleFiles) => {
174
+ const mkRepair = (rawEnv, repoDir, oracleFiles, entry) => {
175
175
  if (repairRounds <= 0)
176
176
  return undefined;
177
177
  return {
@@ -194,12 +194,12 @@ export function createLeaderRunner(cfg) {
194
194
  delimitUntrusted("FAILING OUTPUT (untrusted)", errorText),
195
195
  ].join("\n");
196
196
  const res = await runner
197
- .runTaskStream(withUnattendedLeg({
197
+ .runTaskStream(withLeaderLeg({
198
198
  objective,
199
199
  ...principalSpec(),
200
200
  sessionId: `leader-repair-${Date.now()}-${round}`,
201
201
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
202
- }))
202
+ }, entry))
203
203
  .result();
204
204
  if (res.terminal.kind !== "completed")
205
205
  throw new Error(`repair round ${round} did not complete (status=${outwardPlane(res.terminal).status})`);
@@ -226,6 +226,10 @@ export function createLeaderRunner(cfg) {
226
226
  if (!safeRev(body.baseSha)) {
227
227
  return { ok: false, reports: [], cancelled: false, error: "baseSha must be a plain git rev (sha/branch token)" };
228
228
  }
229
+ const entry = leaderRunEntry(cfg.autonomy());
230
+ const refused = leaderEntryRefusal(entry);
231
+ if (refused !== undefined)
232
+ return { ok: false, reports: [], cancelled: false, error: refused };
229
233
  const injectOracles = async (env) => {
230
234
  const ofs = body.oracleFiles ?? [];
231
235
  if (ofs.length === 0)
@@ -268,7 +272,7 @@ export function createLeaderRunner(cfg) {
268
272
  const provisionWorker = async (sub) => {
269
273
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
270
274
  const durable = cfg.durable?.();
271
- const resumeCtx = buildCheckpointCtx({ body: { objective: sub.spec.objective }, unattendedLeg: true });
275
+ const resumeCtx = buildCheckpointCtx({ body: { ...entry, objective: sub.spec.objective }, unattendedLeg: true });
272
276
  let durableSpec = {};
273
277
  if (durable) {
274
278
  const d = durable;
@@ -353,7 +357,7 @@ export function createLeaderRunner(cfg) {
353
357
  },
354
358
  })),
355
359
  fetchDiff: () => fetchUploadedDiff(getUrl),
356
- spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }),
360
+ spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }, resumeCtx.body),
357
361
  };
358
362
  }
359
363
  if (!cfg.e2bApiKey)
@@ -368,7 +372,7 @@ export function createLeaderRunner(cfg) {
368
372
  runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
369
373
  diffEnv: env,
370
374
  destroy: () => env.destroy().then(() => { }),
371
- spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }),
375
+ spec: withLeaderLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }, resumeCtx.body),
372
376
  };
373
377
  };
374
378
  const provisionIntegrationSandbox = async () => {
@@ -386,7 +390,7 @@ export function createLeaderRunner(cfg) {
386
390
  }
387
391
  const rawEnv = env;
388
392
  const oracleFiles = body.oracleFiles ?? [];
389
- const repair = mkRepair(rawEnv, repo, oracleFiles);
393
+ const repair = mkRepair(rawEnv, repo, oracleFiles, entry);
390
394
  return { env: asIntegrationEnv(env), destroy: reap, ...(repair ? { repair } : {}) };
391
395
  }
392
396
  if (!cfg.e2bApiKey)
@@ -404,10 +408,11 @@ export function createLeaderRunner(cfg) {
404
408
  await env.destroy().catch(() => { });
405
409
  throw e;
406
410
  }
407
- return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}) };
411
+ return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? [], entry) ? { repair: mkRepair(env, repo, body.oracleFiles ?? [], entry) } : {}) };
408
412
  };
409
413
  const push = createCoordinatorPush(body.durableRemote, body.targetRef, ident);
410
414
  const deps = {
415
+ entry,
411
416
  plan,
412
417
  provisionWorker,
413
418
  provisionIntegrationSandbox,
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * 两层,一只 `AsyncLocalStorage`:
5
5
  * · **腿层**(`withModelReachScope`,三条带用户面投递口的 run 腿各包一次:流式 `POST /v1/tasks/stream`、后台 `POST /v1/runs`、续跑):
6
- * 带着这条腿的 `sessionId`(通告归属键)与本腿已经说过的目标集合(去重单位 = 每腿每目标一次);
6
+ * 带着这条腿的两枚身份 —— `sessionId`(通告归属键)与 `taskId`(这条腿的 run 资源 id,= 本腿交给引擎的 `spec.taskId`;7.121.0)——
7
+ * 与本腿已经说过的目标集合(去重单位 = 每腿每目标一次);
7
8
  * · **调用层**(`createModelReachBrain` 每次模型调用包一次):一只可变的 {@link ModelCallReach},出网观察者
8
9
  * (`outbound-dispatcher.ts` 的 undici 诊断通道订阅)把这次调用发出的每一条请求的**结局**记在上面 —— 最后一条是
9
10
  * 「连上了、有了回答」还是「建连就失败了」。
@@ -33,6 +34,12 @@ export interface ModelCallReach {
33
34
  /** 一条 run 腿的作用域。 */
34
35
  export interface ModelReachLegScope {
35
36
  readonly sessionId: string;
37
+ /**
38
+ * 7.121.0:这条腿的 **run 资源 id** —— 与本腿交给引擎的 `spec.taskId` 同值(流式腿 = `X-Task-Id` / `meta.taskId`,后台腿 = 提交回执的
39
+ * `taskId`,续跑腿 = 被续的那条 run 行 id)。缺席 = 这条腿没有 run 资源(没有 run 店的部署上的流式腿)。不是引擎的 run 身份
40
+ * (`TaskResult.runId`,每次 prepare 一铸)—— 那一枚在模型调用这一层读不到。
41
+ */
42
+ readonly taskId?: string;
36
43
  /** 本腿已经通告过的目标(`targetHost`)—— 去重单位 = 每腿每目标一次。 */
37
44
  readonly announced: Set<string>;
38
45
  }
@@ -43,9 +50,14 @@ export interface ModelReachStore {
43
50
  export declare const modelReachContext: AsyncLocalStorage<ModelReachStore>;
44
51
  /**
45
52
  * 腿层入口:每条腿都开**自己的**作用域 —— `sessionId` 缺席(无会话腿)⇒ 空作用域(没有归属键就没有用户面可投;也不继承外层腿的
46
- * 归属键 —— codex r1 旁注:修前缺席即原样执行、会沿用外层腿)。嵌套时内层覆盖外层 —— 归属跟着「这一轮是谁的腿」走。
53
+ * 归属键 —— codex r1 旁注:修前缺席即原样执行、会沿用外层腿)。嵌套时内层覆盖外层 —— 归属跟着「这一轮是谁的腿」走,两枚身份一起走
54
+ * (内层没有 run 资源就是没有,不借外层的)。两键都是**必填形参**(值可 `undefined`):三个调用点各自说清这条腿有没有 run 资源,
55
+ * 漏传一枚 = 编译红,不会静默变成「没有」。
47
56
  */
48
- export declare function withModelReachScope<T>(sessionId: string | undefined, fn: () => T): T;
57
+ export declare function withModelReachScope<T>(leg: {
58
+ readonly sessionId: string | undefined;
59
+ readonly taskId: string | undefined;
60
+ }, fn: () => T): T;
49
61
  /** 调用层入口(`createModelReachBrain` 用):在当前腿作用域(可能缺席)之上开一只新的调用观察。 */
50
62
  export declare function withModelCallReach<T>(call: ModelCallReach, fn: () => T): T;
51
63
  /** 观察者读口:当前异步上下文里的模型调用观察(作用域外 ⇒ `undefined`)。 */
@@ -1,7 +1,8 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  export const modelReachContext = new AsyncLocalStorage();
3
- export function withModelReachScope(sessionId, fn) {
4
- return modelReachContext.run(sessionId === undefined || sessionId.length === 0 ? {} : { leg: { sessionId, announced: new Set() } }, fn);
3
+ export function withModelReachScope(leg, fn) {
4
+ const { sessionId, taskId } = leg;
5
+ return modelReachContext.run(sessionId === undefined || sessionId.length === 0 ? {} : { leg: { sessionId, ...(taskId !== undefined && taskId.length > 0 ? { taskId } : {}), announced: new Set() } }, fn);
5
6
  }
6
7
  export function withModelCallReach(call, fn) {
7
8
  const outer = modelReachContext.getStore();
@@ -1,7 +1,7 @@
1
1
  /** 机读码(拼法随 `config.<键名蛇形>_*` 既有族:`config.model_provider_invalid` / `config.retention_policy_invalid`)。 */
2
2
  export declare const GATEWAY_URL_USERINFO_CODE = "config.gateway_url_userinfo";
3
- /** 同族第二码(S-483 座⑨):这一位根本不是一个层级式绝对 URL。三根键共用一码,与上面那只同纪律
4
- * (码按**规则**命名,不按键名 —— 键名已经逐字在拒因文案里点过了)。 */
3
+ /** 同族第二码(S-483 座⑨):这一位不是一个层级式 http(s) URL(不透明 / 解析不动 / 非 http(s) —— S-1269 起后者同码)。三根键共用一码,
4
+ * 与上面那只同纪律(码按**规则**命名,不按键名 —— 键名已经逐字在拒因文案里点过了)。 */
5
5
  export declare const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_invalid";
6
6
  /**
7
7
  * 🔴 **第三条臂的机读码(S-514;core 7.24.2 / [ref] 提货)——请求根带 `#fragment`**。
@@ -12,26 +12,19 @@ export declare const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_inva
12
12
  */
13
13
  export declare const MODEL_ROUTE_URL_FRAGMENT_CODE = "config.model_route_url_fragment";
14
14
  /**
15
- * 模型路由 base URL 的拒因(`undefined` = 这一位合法)—— **这三根键的唯一判官**。
16
- *
17
- * 两臂,顺序即语义,**两臂都经 display-safe 铸点**:
18
- * ① **不是一个层级式绝对 URL** ⇒ `config.model_route_url_invalid`;
19
- * ② 是,但带 userinfo ⇒ `config.gateway_url_userinfo`。
20
- *
21
- * 🔴 ① 为什么必须在这里、而不是「由别的门管」(S-483 座⑨,与 ME/A2A 两座**同病镜像**):本函数此前先问
22
- * 凭据位、并把「解析不动」读成**没有**凭据(`urlCarriesUserinfo` 的 `return false`),旁注写着「不是 URL 由
23
- * 别的门管」—— 而 config 层对这三根**没有**那道门(`config.ts` 与 `config-invariants.ts` 只调本函数)。
24
- * 于是半写 userinfo `http://user:pass@`(`URL.canParse` 假)与 `alice:pw@host:8000`(不透明 URL,
25
- * `username`/`password` 恒空)两形被当「无凭据」放行,一路到 brain 装配期才由 core 拒;core 那一臂
26
- * 今天不回显原值 —— 但**「恰好不漏」不是防线**,而且拒启点越晚,运维越读不出是哪根旋钮配错了。
27
- * ⇒ 判据改成与铸点**同一只** {@link isHierarchicalUrl}:两处再也不可能各判各的。
28
- *
29
- * 🚧 **未闭环(不在本车射程)**:层级式但**非 http(s)** 的形(`ftp://h/v1`,不带 userinfo)今天仍放行 ——
30
- * `fetch` 对它同样必然失败,但那是第三条臂、是一次新的行为面拒启,按三问另开车(S-483 收车档已登记)。
31
- *
32
- * 拒因四件齐:**码** + **哪个 env 键** + **洗过的 URL**(host/port/path 逐字,userinfo 与凭据参数成记号 ——
15
+ * 模型路由 base URL 的拒因(`undefined` = 这一位合法)—— 判由 `url-shape.ts` 那一只,本函数按**首个**拒因选码与出路句:
16
+ * ① 不是层级式 http(s) URL ⇒ `config.model_route_url_invalid`;
17
+ * ② 带 userinfo ⇒ `config.gateway_url_userinfo`;
18
+ * ③ 带 `#fragment` ⇒ `config.model_route_url_fragment`。
19
+ * 拒因四件齐:**码** + **哪个 env 键** + **值的样子**(判官的 `buildRefusalShownUrl`:层级式只到 `scheme://host[:port]` + `/…`,不透明形整只成记号 ——
33
20
  * 运维要靠 host 认出是哪一条路由,而密码一个字节都不许进启动错误)+ **出路一句**(点名真实旋钮)。
34
21
  *
22
+ * ① 为什么在**声明关口**而不是「由别的门管」(S-483 座⑨):半写 userinfo `http://user:pass@`(解析不动)与 `alice:pw@host:8000`
23
+ * (不透明 URL,`username`/`password` 恒空)两形若被当「无凭据」放行,一路到 brain 装配期才由 core 拒,拒启点越晚运维越读不出
24
+ * 是哪根旋钮配错了;层级式非 http(s)(`ftp://h/v1`)对 `fetch` 同样必然失败(S-1269 起同判,修前放行)。
25
+ * ③ 判在声明关口而不是留给 core 的 brain 构造期(S-514):core 的拒因只过位置律、pathname 逐字,`…/v1/sk-…#note` 那一形的
26
+ * core 拒因会把 path 上的令牌原文打进启动日志 —— 值在到达 core 之前就被这里拒掉,那条文本根本不由 core 铸。
27
+ *
35
28
  * @param envKey 出问题的那根 env 旋钮的名字(CSV 键上还带第几项)。
36
29
  * @param credentialKnob 这条路由把凭据放哪儿 —— 出路一句点名的旋钮名。
37
30
  */
@@ -1,33 +1,37 @@
1
- import { buildDisplaySafeUrl, isHierarchicalUrl } from "./trace/redact.js";
1
+ import { buildHttpUrlRefusal, httpUrlFlaws } from "./url-shape.js";
2
2
  export const GATEWAY_URL_USERINFO_CODE = "config.gateway_url_userinfo";
3
3
  export const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_invalid";
4
4
  export const MODEL_ROUTE_URL_FRAGMENT_CODE = "config.model_route_url_fragment";
5
+ const MODEL_ROUTE_FORBIDS = ["userinfo", "fragment"];
5
6
  export function modelRouteUrlRefusal(envKey, raw, credentialKnob) {
6
7
  if (raw === "")
7
8
  return undefined;
8
- if (!isHierarchicalUrl(raw)) {
9
- return (`env ${envKey} is not a parseable absolute URL (${buildDisplaySafeUrl(raw)}) [${MODEL_ROUTE_URL_INVALID_CODE}] — ` +
10
- "a model-route base URL is consumed by fetch, which requires an absolute hierarchical URL " +
11
- "(scheme://host[:port][/path]); half-written forms like \"http://user:pass@\" and opaque forms like " +
12
- "\"host:8000\" are not one, and they also hide any credentials they carry from every credential-position check. " +
13
- `Set ${envKey} to a full scheme://host URL (and put the credential in ${credentialKnob}, never in the URL).`);
14
- }
15
- const u = new URL(raw);
16
- if (u.username !== "" || u.password !== "")
17
- return (`env ${envKey} carries credentials in the URL userinfo (${buildDisplaySafeUrl(raw)}) [${GATEWAY_URL_USERINFO_CODE}] — ` +
18
- "WHATWG forbids constructing a fetch Request from a URL with credentials, so EVERY model call on this route " +
19
- "fails deterministically at request-construction time (it used to look like a slow network flake: the error was " +
20
- "retried for ~3 minutes before surfacing). " +
21
- `Put the credential in ${credentialKnob} (or the matching header knob) instead of the URL, and keep the URL to ` +
22
- "scheme/host/port/path.");
23
- if (u.hash !== "") {
24
- return (`env ${envKey} carries a URL fragment (${buildDisplaySafeUrl(raw)}#…) [${MODEL_ROUTE_URL_FRAGMENT_CODE}] — ` +
25
- "a fragment is never sent on the wire, so this route would silently target the root's own path and every " +
26
- "endpoint suffix (/chat/completions, /v1/messages) would land inside the discarded part: the request goes out, " +
27
- "may even return 200, and points somewhere else without saying so. " +
28
- `Drop everything from the "#" on in ${envKey}. A per-deployment QUERY is fully supported and rides every ` +
29
- "request (a gateway's `?api-version=…`); a fragment cannot be.");
9
+ const flaws = httpUrlFlaws(raw, MODEL_ROUTE_FORBIDS);
10
+ const first = flaws[0];
11
+ if (first === undefined)
12
+ return undefined;
13
+ const head = buildHttpUrlRefusal(`env ${envKey}`, raw, flaws);
14
+ switch (first) {
15
+ case "not-http-url":
16
+ return (`${head} [${MODEL_ROUTE_URL_INVALID_CODE}] — ` +
17
+ "a model-route base URL is consumed by fetch, which only sends to an absolute hierarchical http(s) URL; " +
18
+ "half-written forms like \"http://user:pass@\", opaque forms like \"host:8000\" and other schemes are not one, and the first two also hide " +
19
+ "any credentials they carry from every credential-position check. " +
20
+ `Set ${envKey} to a full http(s)://host URL (and put the credential in ${credentialKnob}, never in the URL).`);
21
+ case "userinfo":
22
+ return (`${head} [${GATEWAY_URL_USERINFO_CODE}] — ` +
23
+ "WHATWG forbids constructing a fetch Request from a URL with credentials, so EVERY model call on this route " +
24
+ "fails deterministically at request-construction time (it used to look like a slow network flake: the error was " +
25
+ "retried for ~3 minutes before surfacing). " +
26
+ `Put the credential in ${credentialKnob} (or the matching header knob) instead of the URL, and keep the URL to ` +
27
+ "scheme/host/port/path.");
28
+ case "fragment":
29
+ return (`${head} [${MODEL_ROUTE_URL_FRAGMENT_CODE}] — ` +
30
+ "a fragment is never sent on the wire, so this route would silently target the root's own path and every " +
31
+ "endpoint suffix (/chat/completions, /v1/messages) would land inside the discarded part: the request goes out, " +
32
+ "may even return 200, and points somewhere else without saying so. " +
33
+ `Drop everything from the "#" on in ${envKey}. A per-deployment QUERY is fully supported and rides every ` +
34
+ "request (a gateway's `?api-version=…`); a fragment cannot be.");
30
35
  }
31
- return undefined;
32
36
  }
33
37
  //# sourceMappingURL=model-route-url.js.map
@@ -20,7 +20,8 @@
20
20
  * 没有归属键 ⇒ 不铸(运维日志 `outbound_connect_failed` 照有)。腿里的**辅助**调用(WebFetch 摘要器、压实、提示建议、分类器 …)与
21
21
  * 主循环**同一条规则**:本帧说的是「这个模型端点够不着」,不是「这条 run 失败了」(措辞如此,不暗示终态);不设豁免名单 —— codex r1 / r2
22
22
  * 两轮证明豁免名单结构上开放(core 自己发起的辅助调用本仓包不到)。按 **run 终态**归属是另一种设计,候裁(收车档 ⑤)。去重单位 = **每腿每目标一次**(同一条腿里
23
- * 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId }` ——
23
+ * 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId, taskId? }` ——
24
+ * `taskId`(7.121.0)= 这条腿的 run 资源 id,腿没有 run 资源时缺席;引擎的 run 身份 `runId` 在这一层读不到,本码不带 ——
24
25
  * `proxyInUse` 只在这一跳走了代理时在场,且只是**变量名**(代理地址是基础设施细节,不上租户 wire,与 `OutboundProxyError` 的
25
26
  * F3 裁定同一条)。`targetHost`(模型端点的 `host:port`)对 user 受众是**新披露的部署事实**(F7 裁定:它是部署自己声明的模型端点,
26
27
  * 不是基础设施秘密,可以上;代理地址仍不上)—— 不要读成「终局句里本就有」:代理形下终局句只带码与 `via outbound proxy (from <变量>)`、不含任何主机地址,
@@ -17,6 +17,7 @@ function announceModelUnreachable(leg, failure, router = defaultEngineNoticeRout
17
17
  ...(failure.proxyInUse !== null ? { proxyInUse: failure.proxyInUse } : {}),
18
18
  remedy: MODEL_UNREACHABLE_REMEDY,
19
19
  sessionId: leg.sessionId,
20
+ ...(leg.taskId !== undefined ? { taskId: leg.taskId } : {}),
20
21
  },
21
22
  sessionId: leg.sessionId,
22
23
  });