@sema-agent/server 7.119.0 → 7.121.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 (127) hide show
  1. package/MIGRATION.md +129 -0
  2. package/USAGE.md +72 -23
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +37 -13
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/execution-env.js +3 -2
  10. package/dist/boot/fatal-exit.d.ts +7 -1
  11. package/dist/boot/fatal-exit.js +2 -1
  12. package/dist/boot/resolve-spec.js +1 -0
  13. package/dist/boot/runner-deps.d.ts +4 -3
  14. package/dist/boot/runner-deps.js +7 -3
  15. package/dist/boot/shutdown.d.ts +9 -10
  16. package/dist/boot/shutdown.js +54 -16
  17. package/dist/boot/stage-01-config.js +2 -0
  18. package/dist/boot/stage-09-leader.d.ts +2 -9
  19. package/dist/boot/stage-10-http-server.d.ts +1 -9
  20. package/dist/boot/stage-10-http-server.js +31 -8
  21. package/dist/capabilities/builtin-tools.d.ts +31 -8
  22. package/dist/capabilities/builtin-tools.js +22 -1
  23. package/dist/capabilities/composite-members.d.ts +1 -1
  24. package/dist/capabilities/composite-members.js +1 -1
  25. package/dist/capabilities/scenarios.d.ts +8 -6
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  28. package/dist/capabilities/send-user-file-tool.js +1 -1
  29. package/dist/config-catalog.js +13 -7
  30. package/dist/config-center/center-request.d.ts +16 -1
  31. package/dist/config-center/center-request.js +36 -5
  32. package/dist/config-lkg.d.ts +33 -5
  33. package/dist/config-lkg.js +55 -11
  34. package/dist/config-types.d.ts +46 -3
  35. package/dist/config-types.js +6 -0
  36. package/dist/config.d.ts +33 -4
  37. package/dist/config.js +107 -15
  38. package/dist/deployment-governance.d.ts +3 -1
  39. package/dist/deployment-governance.js +3 -1
  40. package/dist/device-store.d.ts +7 -0
  41. package/dist/device-store.js +4 -0
  42. package/dist/device-ws-hub.js +17 -9
  43. package/dist/device-ws-protocol.d.ts +15 -0
  44. package/dist/fleet/drain-entry.d.ts +87 -0
  45. package/dist/fleet/drain-entry.js +35 -0
  46. package/dist/fleet/engine-signal.d.ts +144 -0
  47. package/dist/fleet/engine-signal.js +60 -0
  48. package/dist/fleet/serving-lease.d.ts +47 -0
  49. package/dist/fleet/serving-lease.js +235 -0
  50. package/dist/fleet-client.d.ts +35 -19
  51. package/dist/fleet-client.js +49 -25
  52. package/dist/http/approval-sweeps.js +3 -5
  53. package/dist/http/dispatch.js +18 -13
  54. package/dist/http/health-self-report.d.ts +60 -0
  55. package/dist/http/health-self-report.js +53 -0
  56. package/dist/http/principal-gate.d.ts +8 -1
  57. package/dist/http/principal-gate.js +1 -0
  58. package/dist/http/readiness.d.ts +25 -5
  59. package/dist/http/readiness.js +31 -6
  60. package/dist/http/resume-legs.js +5 -1
  61. package/dist/http/route-ctx.d.ts +6 -4
  62. package/dist/http/route-table.d.ts +3 -0
  63. package/dist/http/route-table.js +4 -1
  64. package/dist/http/routes/a2a-serve.d.ts +1 -1
  65. package/dist/http/routes/a2a-serve.js +5 -5
  66. package/dist/http/routes/admin-drain.d.ts +12 -31
  67. package/dist/http/routes/admin-drain.js +15 -9
  68. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  69. package/dist/http/routes/approvals-assistant.js +3 -3
  70. package/dist/http/routes/capabilities.js +3 -1
  71. package/dist/http/routes/devices.js +6 -5
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +1 -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 +14 -6
  79. package/dist/http/routes/runs.d.ts +2 -2
  80. package/dist/http/routes/runs.js +2 -2
  81. package/dist/http/routes/side-query.d.ts +1 -1
  82. package/dist/http/routes/side-query.js +1 -1
  83. package/dist/http/routes/tasks.d.ts +2 -2
  84. package/dist/http/routes/tasks.js +3 -3
  85. package/dist/http/server.d.ts +7 -21
  86. package/dist/http/server.js +20 -1
  87. package/dist/http/wire-types.d.ts +7 -1
  88. package/dist/list-entry-display.d.ts +14 -0
  89. package/dist/list-entry-display.js +14 -0
  90. package/dist/model-reach-scope.d.ts +15 -3
  91. package/dist/model-reach-scope.js +3 -2
  92. package/dist/model-unreachable-notice.d.ts +2 -1
  93. package/dist/model-unreachable-notice.js +1 -0
  94. package/dist/namespaced-rule-name.js +2 -6
  95. package/dist/observability/fail-open.d.ts +20 -0
  96. package/dist/observability/fail-open.js +20 -0
  97. package/dist/operator-ask.d.ts +1 -1
  98. package/dist/plugins/one-time-migrations.d.ts +8 -0
  99. package/dist/plugins/one-time-migrations.js +72 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -6
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/request-key-closure.js +1 -1
  108. package/dist/rule-entry-list.d.ts +0 -1
  109. package/dist/rule-entry-list.js +4 -9
  110. package/dist/rules-consent.d.ts +16 -3
  111. package/dist/rules-consent.js +8 -4
  112. package/dist/run-local.js +2 -0
  113. package/dist/runs.js +1 -1
  114. package/dist/sema-config-dir.d.ts +19 -0
  115. package/dist/sema-config-dir.js +33 -0
  116. package/dist/session-policy-wire.js +5 -4
  117. package/dist/spec-fields.d.ts +5 -4
  118. package/dist/task-settings.d.ts +3 -3
  119. package/dist/tool-approval.d.ts +7 -0
  120. package/dist/tool-approval.js +4 -1
  121. package/dist/trace/core-keyset-guard.d.ts +2 -2
  122. package/dist/trace/engine-notice-wire.d.ts +6 -4
  123. package/dist/trace/engine-notice-wire.js +16 -9
  124. package/dist/trace/self-minted-wire.d.ts +78 -0
  125. package/dist/trace/self-minted-wire.js +32 -0
  126. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  127. package/package.json +3 -3
@@ -1,40 +1,21 @@
1
1
  /**
2
- * [ref](C17,[ref]/[ref] 认领,黑板 [ref] 原文即本口的契约):`POST /v1/admin/drain {reason}` ——
3
- * 编排壳在发停机信号(SIGTERM)**之前**先调本口显式声明停机因由;server 记入 `drainState.reason`,
4
- * draining 期间由 `/health`(`drainReason`)与 503 draining 体(`reason`)透出。未声明 = 两面键缺席
5
- * (additive,不编数)——升级/缩容/驱逐在消费端因此可判别,而不是一律读成「实例没了」。
2
+ * `POST /v1/admin/drain` —— 排空口(编排器长远稿片 2 件 2;契约体 = settings-schema 8.1.0 `DrainRequest`,回执 = `DrainReceipt`)。
6
3
  *
7
- * 族属 = adoption / retention-ops 的 operator lane(同一条 `explicitOperatorOk` 门、billable=false、
8
- * **刻意不吃 drain 503**——它正是 drain 开始前要调的口,吃了就自锁)。门序照 retention-ops 逐字:
9
- * 身份 → 授权 → 验型。刻意**不进** `isCredentialGatedRewrite` 名单:那族的判据是「授权唯一输入是
10
- * service token ∧ 爆炸半径跨租户持久改写」,本口只写一个进程内观测字段(重启即逝、零持久面),
11
- * operator 门(空表恒拒,fail-closed)已是正确强度。
12
- *
13
- * reason 校验:非空 trim 后 string ≤ {@link MAX_DRAIN_REASON_CHARS} —— 它会进 /health 与 503 体
14
- * (消费面是监控/壳),超长或怪型响亮拒([ref] 坏值立律),拒绝不留痕(不写半个值)。
15
- *
16
- * 🔴 **写侧特权、读侧公开 —— 这条不对称是刻意的,但必须被写的人知道**([ref],2026-08-19 合并重扫
17
- * partial,存活断言):本口是 operator-only + fail-closed(空 `OPERATOR_PRINCIPALS` 恒拒),而透出面里
18
- * `/health` 在 `server.ts` 的鉴权门**之前**、无鉴权、缺省全网卡监听,USAGE 三处白纸黑字把它定为「无需
19
- * 鉴权」—— 任何能连到这个端口的人都读得到这段**运维手打的自由文本**。
20
- * **裁定:行为不改。** 判据三条:① `/health` 免鉴权是既成契约,而它正是这条信息的设计消费者(k8s
21
- * probe / LB / 编排器带不了 token,`drainReason` 放这儿的全部理由就是给它们看);② 值的内容完全由
22
- * operator 自选,且窗口只在 draining 期;③ 同一免鉴权体上早已有 `dataRoot` 绝对路径 / `pid` / `port` /
23
- * `instanceId` / `configHash` / `storeProbe`,单独收紧本键不改变那面的暴露量级。
24
- * (那条另立的兄弟件 —— `storeProbe.error` 把**原始异常 message 未截断**塞出去、DSN 片段可随之外泄 ——
25
- * 已在 [ref] 修掉:免鉴权面上它改成闭集词,原文只走日志轴。两件的判据不同故不合并:本键的值由 operator
26
- * 自选且窗口只在 draining 期,`storeProbe.error` 的值则来自不受控的驱动异常。)
27
- * ⇒ 真正的缺口是**告诫缺席**:此前路由头注、400 文案、CHANGELOG、wire 契约四处都只说「透出」,没有一处
28
- * 说「其中一面免鉴权」,操作员要拼出「我写的字是公开的」得自己合并两处文档去推理。本批把它写进**写口
29
- * 现场**(400 文案 + 本注 + USAGE 端点条目),因为那是操作员唯一会读到的地方。
30
- * 词表化(把自由文本换成受限枚举)= 行为变更,**不在本批**:它会让「rolling upgrade to 7.36.0」这类真正
31
- * 有用的因由说不出来,取舍要属主拍。
4
+ * 一只入口的 HTTP 车道(Docker 车道 / 中心直调 / 人手形):体经契约读器 `readDrainRequest`(一处判形),身份四元经
5
+ * `fleet/drain-entry.ts` 的 `drainIdentityMismatches`(一处判身份;本引擎的 scope = `config.fleetScope`,env `FLEET_SCOPE` 的唯一读数),
6
+ * 进入排空经 `drainState.enter`(收尾链装的唯一入口)。
7
+ * · 门序 = operator 面的既有口径(`routes/retention-ops.ts` 逐字):身份 → 授权 → 验型;billable=false,**刻意不吃排空 503**
8
+ * (排空中的重放必须能拿到 `already_draining` 回执,吃了就自锁)。
9
+ * · 体坏 ⇒ 400 `request.field_invalid`(零状态变化);身份不符 ⇒ 409 `conflict.fleet_identity`(零状态变化;拒句点名不符的字段,不回显值);
10
+ * 没有收尾链(嵌入宿主 / 测试夹具)⇒ 501 `capability.drain_state_required`。
11
+ * · 首次 ⇒ 200 回执 `state:"draining"`;同一 `actionId`(或别的动作)再来 ⇒ 200 `already_draining`,回显第一次的 `since` / `deadlineAt`,不重置。
12
+ * · 人手形:`actionId` 自取(推荐 `manual:<uuid>`),身份照样四元比对 —— 非 fleet 成员不带 `expectedGeneration`(缺席对缺席);
13
+ * `instanceId` 在场才比(从本机 `/health` 读);不带 `scope` = 点名缺省 scope(多租引擎要带它自己的 `FLEET_SCOPE`)。
14
+ * 修前形(7.35.0 起的 `{reason}` 停机因由声明:只记一句话、不进排空)随契约体退役 —— 零别名;那一形的请求体读不成 `DrainRequest` ⇒ 400。
32
15
  */
33
16
  import type { IncomingMessage, ServerResponse } from "node:http";
34
17
  import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
35
18
  export declare const ADMIN_DRAIN_PATH = "/v1/admin/drain";
36
- /** 上限与 approvals 的 reason 族同量级(消费面同为人读短句;非同一常量——两面语义无关,独立演化)。 */
37
- export declare const MAX_DRAIN_REASON_CHARS = 256;
38
19
  export declare function handleAdminDrain(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AdminDrainRouteId>, ctx: RouteCtx): Promise<void>;
39
20
  export declare const ADMIN_DRAIN_ROUTES: readonly [{
40
21
  readonly id: "admin-drain";
@@ -1,9 +1,11 @@
1
+ import { readDrainRequest } from "@sema-agent/settings-schema/fleet";
1
2
  import { sendJson, sendError } from "../send.js";
2
3
  import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
3
4
  import * as facts from "../../capability-facts.js";
5
+ import { fleetScopeOf } from "../../config-types.js";
6
+ import { drainIdentityMismatches } from "../../fleet/drain-entry.js";
4
7
  import { assertNever } from "../route-ctx.js";
5
8
  export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
6
- export const MAX_DRAIN_REASON_CHARS = 256;
7
9
  const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS";
8
10
  export async function handleAdminDrain(req, res, match, ctx) {
9
11
  const { deps } = ctx;
@@ -22,18 +24,22 @@ export async function handleAdminDrain(req, res, match, ctx) {
22
24
  sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
23
25
  return;
24
26
  }
25
- const body = (await ctx.helpers.readJson(req));
26
- const reason = body?.reason;
27
- if (typeof reason !== "string" || reason.trim().length === 0 || reason.length > MAX_DRAIN_REASON_CHARS) {
28
- sendError(res, 400, "request.field_invalid", `the drain reason is surfaced verbatim on the UNAUTHENTICATED /health endpoint (and in 503 draining bodies), so anyone who can reach this port can read it — do not put ticket ids, hostnames, or internal identifiers in it. body must be { reason: string }: a non-empty string of at most ${MAX_DRAIN_REASON_CHARS} characters`);
27
+ const request = readDrainRequest(await ctx.helpers.readJson(req));
28
+ if (request === undefined) {
29
+ sendError(res, 400, "request.field_invalid", "the drain request must be { actionId, worker, deadlineAt, scope?, expectedGeneration?, instanceId?, term? }: actionId a non-empty idempotency key, worker this engine's worker name, deadlineAt epoch milliseconds, expectedGeneration a canonical decimal string (\"7\") — nothing was changed");
29
30
  return;
30
31
  }
31
- if (!deps.drainState) {
32
- sendError(res, 501, "capability.drain_state_required", "this deployment has no drain state wired (embedded in another process?) — the declared reason would have nowhere to surface");
32
+ const enter = deps.drainState?.enter;
33
+ if (enter === undefined) {
34
+ sendError(res, 501, "capability.drain_state_required", "this deployment has no drain entry wired (embedded in another process?) — there is no drain for this request to enter");
33
35
  return;
34
36
  }
35
- deps.drainState.reason = reason.trim();
36
- sendJson(res, 200, { ok: true });
37
+ const mismatched = drainIdentityMismatches(request, { scope: deps.config.fleetScope, worker: deps.config.configCenter?.worker, generation: deps.config.fleetGeneration, instanceId: deps.instanceId });
38
+ if (mismatched.length > 0) {
39
+ sendError(res, 409, "conflict.fleet_identity", `the drain request names a different instance than this engine (mismatched: ${mismatched.join(", ")}) — nothing was changed; compare with this engine's GET /health (worker comes from SEMA_REGISTRY_WORKER, generation from FLEET_GENERATION, scope from FLEET_SCOPE — unset, and a request without scope, mean "${fleetScopeOf(undefined)}")`);
40
+ return;
41
+ }
42
+ sendJson(res, 200, enter({ actionId: request.actionId, deadlineAt: request.deadlineAt }));
37
43
  return;
38
44
  }
39
45
  default:
@@ -183,17 +183,17 @@ export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
183
183
  readonly id: "assistant-task-resume";
184
184
  readonly pattern: RegExp;
185
185
  readonly label: "/v1/assistant/tasks/:id/resume";
186
- readonly billable: true;
186
+ readonly billable: "resume";
187
187
  readonly methods: readonly ["POST"];
188
188
  }, {
189
189
  readonly id: "assistant-task-plan-review";
190
190
  readonly pattern: RegExp;
191
191
  readonly label: "/v1/assistant/tasks/:id/plan_review";
192
- readonly billable: true;
192
+ readonly billable: "hitl-answer";
193
193
  readonly methods: readonly ["POST"];
194
194
  }, {
195
195
  readonly prefix: "/v1/approvals";
196
- readonly billable: true;
196
+ readonly billable: "hitl-answer";
197
197
  }];
198
198
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
199
199
  export type ApprovalsAssistantRouteId = RouteIdsOf<typeof APPROVALS_ASSISTANT_ROUTES>;
@@ -917,9 +917,9 @@ export const APPROVALS_ASSISTANT_ROUTES = [
917
917
  { id: "approval-exemptions-list", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["GET"] },
918
918
  { id: "approval-exemption-delete", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["DELETE"] },
919
919
  { id: "assistant-task-preempt", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/preempt$/, label: "/v1/assistant/tasks/:id/preempt", methods: ["POST"] },
920
- { id: "assistant-task-resume", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/resume$/, label: "/v1/assistant/tasks/:id/resume", billable: true, methods: ["POST"] },
921
- { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
922
- { prefix: "/v1/approvals", billable: true },
920
+ { id: "assistant-task-resume", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/resume$/, label: "/v1/assistant/tasks/:id/resume", billable: "resume", methods: ["POST"] },
921
+ { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: "hitl-answer", methods: ["POST"] },
922
+ { prefix: "/v1/approvals", billable: "hitl-answer" },
923
923
  ];
924
924
  const DECISION_DOOR_KEY_TABLE = deriveDecisionDoorKeyTable(DECISION_DOOR_FAMILY, APPROVALS_ASSISTANT_ROUTES);
925
925
  //# sourceMappingURL=approvals-assistant.js.map
@@ -79,7 +79,7 @@ export async function handleCapabilities(req, res, match, ctx) {
79
79
  : false,
80
80
  sessionEvents: facts.sessionEventStream(deps).available,
81
81
  fleet: facts.fleetStream(deps).available
82
- ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true }
82
+ ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "engine", hookFailureNotice: true, leaseClient: true, generationEcho: true }
83
83
  : false,
84
84
  workspace: facts.workspaceBrowse().available,
85
85
  leader: facts.leaderLane(deps).available,
@@ -130,6 +130,7 @@ export async function handleCapabilities(req, res, match, ctx) {
130
130
  readFace: deps.config.readFace ?? null,
131
131
  writeProtection: projectWriteProtectionCapability(deps.writeProtection),
132
132
  appendSystemPrompt: true,
133
+ outputStyle: true,
133
134
  compactionModel: true,
134
135
  subagentOutput: facts.subagentOutput(deps).available,
135
136
  subagentStream: facts.subagentOutput(deps).available,
@@ -161,6 +162,7 @@ export async function handleCapabilities(req, res, match, ctx) {
161
162
  approvalDecisionNote: true,
162
163
  approvalDecidedBy: true,
163
164
  permissionModes: [...PERMISSION_MODES],
165
+ sessionReadRedaction: true,
164
166
  sideQuery: true,
165
167
  agentRoster: facts.agentRoster(deps).available,
166
168
  retention: retentionAdvertisement(deps.config),
@@ -2,12 +2,13 @@ import { sendError, sendJson } from "../send.js";
2
2
  import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
3
3
  import { assertNever } from "../route-ctx.js";
4
4
  import * as facts from "../../capability-facts.js";
5
- import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, sameDeviceOwner, } from "../../device-store.js";
5
+ import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, isDeviceAbsolutePath, sameDeviceOwner, } from "../../device-store.js";
6
6
  import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
7
7
  import { deviceOwnerDomainOf } from "../device-placement.js";
8
8
  import { echoKeyName } from "../../request-key-closure.js";
9
9
  const DEVICE_NOT_FOUND_MESSAGE = "device not found";
10
10
  const NO_DEVICE_DOMAIN_MESSAGE = "device management needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) and is not an explicit operator — connect through the registry auth bridge, or configure OPERATOR_PRINCIPALS";
11
+ const NO_ENROLL_DOMAIN_MESSAGE = "device enrollment needs a registry login that carries a tenant scope: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`), and an operator principal carries no scope, so it cannot enroll a device or issue an enroll token — connect through the registry auth bridge with a tenant-scoped login";
11
12
  const DEVICE_LANE_REQUIRED_MESSAGE = "device management requires the device execution lane (REMOTE_EXEC=device with a SQL store backend) — this deployment has no device lane wired";
12
13
  const _deviceRouteCodesPinned = ["not_found.device", "device.rebind_rev_conflict", "device.rebind_active_run"];
13
14
  void _deviceRouteCodesPinned;
@@ -109,8 +110,8 @@ function parseEnrollBody(res, body) {
109
110
  return null;
110
111
  }
111
112
  }
112
- if (workspaceRoot.length === 0) {
113
- sendError(res, 400, "request.field_invalid", "workspaceRoot is required and must be non-empty (it is the device-side root every instruction resolves against)");
113
+ if (!isDeviceAbsolutePath(workspaceRoot)) {
114
+ sendError(res, 400, "request.field_invalid", "workspaceRoot must be an absolute POSIX path (it is the device-side root every instruction resolves against)");
114
115
  return null;
115
116
  }
116
117
  return { token: token, pubkey: pubkey, platformOs: os, platformArch: platformArch, displayName: displayName, workspaceRoot: workspaceRoot };
@@ -164,7 +165,7 @@ export async function handleDevices(req, res, match, ctx) {
164
165
  }
165
166
  if (match.decl.id === "device-enroll-tokens") {
166
167
  if (domain === undefined) {
167
- sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
168
+ sendError(res, 403, "auth.forbidden", NO_ENROLL_DOMAIN_MESSAGE);
168
169
  return;
169
170
  }
170
171
  if ((await readClosedBody(req, res, readJson, [])) === null)
@@ -180,7 +181,7 @@ export async function handleDevices(req, res, match, ctx) {
180
181
  }
181
182
  if (match.decl.id === "device-enroll") {
182
183
  if (domain === undefined) {
183
- sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
184
+ sendError(res, 403, "auth.forbidden", NO_ENROLL_DOMAIN_MESSAGE);
184
185
  return;
185
186
  }
186
187
  const raw = await readClosedBody(req, res, readJson, ENROLL_BODY_KEYS);
@@ -16,7 +16,7 @@ export declare function handleLeader(req: IncomingMessage, res: ServerResponse,
16
16
  export declare const LEADER_ROUTES: readonly [{
17
17
  readonly id: "leader-submit";
18
18
  readonly path: "/v1/leader";
19
- readonly billable: true;
19
+ readonly billable: "submit";
20
20
  readonly methods: readonly ["POST"];
21
21
  }, {
22
22
  readonly id: "leader-detail";
@@ -51,7 +51,7 @@ export async function handleLeader(req, res, match, ctx) {
51
51
  throw new Error(`leader endpoint did not claim its own declared route: ${match.decl.id}`);
52
52
  }
53
53
  export const LEADER_ROUTES = [
54
- { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: true, methods: ["POST"] },
54
+ { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: "submit", methods: ["POST"] },
55
55
  { id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
56
56
  ];
57
57
  //# sourceMappingURL=leader.js.map
@@ -58,7 +58,7 @@ export declare function handleMemoryConsolidation(req: IncomingMessage, res: Ser
58
58
  export declare const MEMORY_CONSOLIDATION_ROUTES: readonly [{
59
59
  readonly id: "memory-consolidation-run";
60
60
  readonly path: "/v1/admin/memory/consolidation/run";
61
- readonly billable: true;
61
+ readonly billable: "submit";
62
62
  readonly methods: readonly ["POST"];
63
63
  }, {
64
64
  readonly id: "memory-consolidation-status";
@@ -163,7 +163,7 @@ export async function handleMemoryConsolidation(req, res, match, ctx) {
163
163
  }
164
164
  }
165
165
  export const MEMORY_CONSOLIDATION_ROUTES = [
166
- { id: "memory-consolidation-run", path: MEMORY_CONSOLIDATION_RUN_PATH, billable: true, methods: ["POST"] },
166
+ { id: "memory-consolidation-run", path: MEMORY_CONSOLIDATION_RUN_PATH, billable: "submit", methods: ["POST"] },
167
167
  { id: "memory-consolidation-status", path: MEMORY_CONSOLIDATION_STATUS_PATH, methods: ["GET"] },
168
168
  ];
169
169
  //# sourceMappingURL=memory-consolidation.js.map
@@ -22,7 +22,7 @@ export declare const NOTIFY_WAKE_ROUTES: readonly [{
22
22
  readonly id: "session-wake";
23
23
  readonly pattern: RegExp;
24
24
  readonly label: "/v1/sessions/:id/$verb";
25
- readonly billable: true;
25
+ readonly billable: "submit";
26
26
  readonly methods: readonly ["POST"];
27
27
  }, {
28
28
  readonly id: "session-notify";
@@ -143,7 +143,7 @@ export async function handleNotifyWake(req, res, match, ctx) {
143
143
  }
144
144
  }
145
145
  export const NOTIFY_WAKE_ROUTES = [
146
- { id: "session-wake", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/wake$/, label: "/v1/sessions/:id/$verb", billable: true, methods: ["POST"] },
146
+ { id: "session-wake", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/wake$/, label: "/v1/sessions/:id/$verb", billable: "submit", methods: ["POST"] },
147
147
  { id: "session-notify", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/notify$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
148
148
  ];
149
149
  //# sourceMappingURL=notify-wake.js.map
@@ -4,10 +4,10 @@ import { gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired, sen
4
4
  import { assertNever } from "../route-ctx.js";
5
5
  import * as facts from "../../capability-facts.js";
6
6
  import { createLogger } from "../../observability/logger.js";
7
- import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
7
+ import { RULE_IMPORT_RETRY_AFTER_SEC, RULE_SCOPE_REFUSAL_SENTENCE, MAX_RULE_SCOPE_CHARS, buildCcImportLayerRootRefusal, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
8
8
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
9
9
  import { HttpError } from "../../security.js";
10
- import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
10
+ import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS, isValidDurableScope } from "@sema-agent/core";
11
11
  import { PERSISTED_RULE_MATCHES, isPersistedRuleTool, isDirectWritableRuleBehavior } from "../../permission-rule-vocab.js";
12
12
  const logger = createLogger();
13
13
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
@@ -26,7 +26,7 @@ const CcImportPrepareBodySchema = z
26
26
  .object({
27
27
  layer: z.enum(["userSettings", "projectSettings", "localSettings"]),
28
28
  path: z.string().min(1).max(1024),
29
- root: z.string().min(1).max(1024),
29
+ root: z.string().max(1024),
30
30
  content: z.string().max(MAX_LAYER_CONTENT_CHARS),
31
31
  })
32
32
  .strict())
@@ -34,6 +34,9 @@ const CcImportPrepareBodySchema = z
34
34
  .max(MAX_LAYERS),
35
35
  })
36
36
  .strict();
37
+ function ccImportRootRefused(layer) {
38
+ return !isValidDurableScope({ kind: "project", root: layer.root });
39
+ }
37
40
  const TICKET_CONTROL_CHARS_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
38
41
  const TicketStringSchema = z.string().min(1).max(190).refine((t) => !TICKET_CONTROL_CHARS_RE.test(t));
39
42
  const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
@@ -170,6 +173,11 @@ export async function handleRules(req, res, match, ctx) {
170
173
  sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
171
174
  return;
172
175
  }
176
+ const refusedLayer = parsed.data.layers.findIndex(ccImportRootRefused);
177
+ if (refusedLayer >= 0) {
178
+ sendError(res, 400, "request.body_shape", buildCcImportLayerRootRefusal(refusedLayer));
179
+ return;
180
+ }
173
181
  const prepared = await lane.prepareImport(principal, parsed.data.layers);
174
182
  if (!prepared.ok) {
175
183
  sendError(res, 413, "request.payload_too_large", "too many allow entries across the submitted settings layers");
@@ -231,7 +239,7 @@ async function handleRuleList(res, search, caller, lane, config) {
231
239
  const scopeRaw = q.get("scope");
232
240
  const scopeFilter = scopeRaw === null ? undefined : parseRuleScope(scopeRaw);
233
241
  if (scopeRaw !== null && scopeFilter === undefined) {
234
- sendError(res, 400, "request.query_invalid", "scope must be 'global' or 'project:<root>' with a non-empty root");
242
+ sendError(res, 400, "request.query_invalid", RULE_SCOPE_REFUSAL_SENTENCE);
235
243
  return;
236
244
  }
237
245
  const cursorRaw = q.get("cursor");
@@ -305,7 +313,7 @@ async function handleRuleWrite(res, rawBody, caller, lane, config) {
305
313
  }
306
314
  const scope = parseRuleScope(parsed.data.scope);
307
315
  if (scope === undefined) {
308
- sendError(res, 400, "request.body_shape", "scope must be 'global' or 'project:<root>' with a non-empty root");
316
+ sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
309
317
  return;
310
318
  }
311
319
  const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
@@ -354,7 +362,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, config) {
354
362
  }
355
363
  const scope = parseRuleScope(parsed.data.scope);
356
364
  if (scope === undefined) {
357
- sendError(res, 400, "request.body_shape", "scope must be 'global' or 'project:<root>' with a non-empty root");
365
+ sendError(res, 400, "request.body_shape", RULE_SCOPE_REFUSAL_SENTENCE);
358
366
  return;
359
367
  }
360
368
  const target = resolveTargetPrincipal(parsed.data.principal, caller, config);
@@ -40,7 +40,7 @@ idemKey: string | undefined): Promise<{
40
40
  export declare const RUNS_ROUTES: readonly [{
41
41
  readonly id: "runs-submit";
42
42
  readonly path: "/v1/runs";
43
- readonly billable: true;
43
+ readonly billable: "submit";
44
44
  readonly methods: readonly ["POST"];
45
45
  }, {
46
46
  readonly id: "run-steer";
@@ -99,7 +99,7 @@ export declare const RUN_VERBS_ROUTES: readonly [{
99
99
  readonly id: "run-subagent-resume";
100
100
  readonly pattern: RegExp;
101
101
  readonly label: "/v1/runs/:id/subagents/:sub/*";
102
- readonly billable: true;
102
+ readonly billable: "resume";
103
103
  readonly methods: readonly ["POST"];
104
104
  }, {
105
105
  readonly id: "run-task-output";
@@ -1656,7 +1656,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1656
1656
  return resp;
1657
1657
  }
1658
1658
  export const RUNS_ROUTES = [
1659
- { id: "runs-submit", path: "/v1/runs", billable: true, methods: ["POST"] },
1659
+ { id: "runs-submit", path: "/v1/runs", billable: "submit", methods: ["POST"] },
1660
1660
  { id: "run-steer", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/steer$/, label: "/v1/runs/:id/steer", methods: ["POST"] },
1661
1661
  { id: "run-interrupt", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/interrupt$/, label: "/v1/runs/:id/interrupt", methods: ["POST"] },
1662
1662
  { id: "run-detach", pattern: /^\/v1\/runs\/(?<taskId>[^/]+)\/detach$/, label: "/v1/runs/:id/detach", methods: ["POST"] },
@@ -1669,7 +1669,7 @@ export const RUN_VERBS_ROUTES = [
1669
1669
  { id: "run-subagent-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/output$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1670
1670
  { id: "run-subagent-stream", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/stream$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["GET"] },
1671
1671
  { id: "run-subagent-steer", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/steer$/, label: "/v1/runs/:id/subagents/:sub/*", methods: ["POST"] },
1672
- { id: "run-subagent-resume", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/resume$/, label: "/v1/runs/:id/subagents/:sub/*", billable: true, methods: ["POST"] },
1672
+ { id: "run-subagent-resume", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/subagents\/(?<sub>[^/]+)\/resume$/, label: "/v1/runs/:id/subagents/:sub/*", billable: "resume", methods: ["POST"] },
1673
1673
  { id: "run-task-output", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/output$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["GET"] },
1674
1674
  { id: "run-task-stop", pattern: /^\/v1\/runs\/(?<runId>[^/]+)\/tasks\/(?<tid>[^/]+)\/stop$/, label: "/v1/runs/:id/tasks/:tid/*", methods: ["POST"] },
1675
1675
  { id: "elicitation-respond", pattern: /^\/v1\/elicitations\/(?<id>[^/]+)\/respond$/, label: "/v1/elicitations/:id/respond", methods: ["POST"] },
@@ -17,7 +17,7 @@ export declare function handleSideQuery(req: IncomingMessage, res: ServerRespons
17
17
  export declare const SIDE_QUERY_ROUTES: readonly [{
18
18
  readonly id: "side-query";
19
19
  readonly path: "/v1/side-query";
20
- readonly billable: true;
20
+ readonly billable: "submit";
21
21
  readonly methods: readonly ["POST"];
22
22
  }];
23
23
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
@@ -133,6 +133,6 @@ export async function handleSideQuery(req, res, match, ctx) {
133
133
  }
134
134
  }
135
135
  export const SIDE_QUERY_ROUTES = [
136
- { id: "side-query", path: "/v1/side-query", billable: true, methods: ["POST"] },
136
+ { id: "side-query", path: "/v1/side-query", billable: "submit", methods: ["POST"] },
137
137
  ];
138
138
  //# sourceMappingURL=side-query.js.map
@@ -25,12 +25,12 @@ export declare function handleTasks(req: IncomingMessage, res: ServerResponse, m
25
25
  export declare const TASKS_ROUTES: readonly [{
26
26
  readonly id: "tasks-submit";
27
27
  readonly path: "/v1/tasks";
28
- readonly billable: true;
28
+ readonly billable: "submit";
29
29
  readonly methods: readonly ["POST"];
30
30
  }, {
31
31
  readonly id: "tasks-submit-stream";
32
32
  readonly path: "/v1/tasks/stream";
33
- readonly billable: true;
33
+ readonly billable: "submit";
34
34
  readonly methods: readonly ["POST"];
35
35
  }];
36
36
  /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
@@ -639,7 +639,7 @@ export async function handleTasks(req, res, match, ctx) {
639
639
  });
640
640
  }
641
641
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
642
- await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
642
+ await instructionLeg.run(prepared.spec.sessionId, () => withModelReachScope({ sessionId: prepared.spec.sessionId, taskId: durableTaskId }, () => deps.sendUserFile
643
643
  ? deps.sendUserFile.runWithContext({
644
644
  taskId: askTaskId,
645
645
  emit: async (f) => {
@@ -901,7 +901,7 @@ export async function handleTasks(req, res, match, ctx) {
901
901
  }
902
902
  }
903
903
  export const TASKS_ROUTES = [
904
- { id: "tasks-submit", path: "/v1/tasks", billable: true, methods: ["POST"] },
905
- { id: "tasks-submit-stream", path: "/v1/tasks/stream", billable: true, methods: ["POST"] },
904
+ { id: "tasks-submit", path: "/v1/tasks", billable: "submit", methods: ["POST"] },
905
+ { id: "tasks-submit-stream", path: "/v1/tasks/stream", billable: "submit", methods: ["POST"] },
906
906
  ];
907
907
  //# sourceMappingURL=tasks.js.map
@@ -649,8 +649,6 @@ export interface ServiceGovernanceDeps {
649
649
  * 「诊断说亮、位说暗」。缺席(桩/一次性 CLI)⇒ 读面如实答 `deployment_incapable`。 */
650
650
  autoModeFace?: import("../auto-mode-face.js").AutoModeFaceDeps;
651
651
  }
652
- /** **部署自述面**:只被 /health、/v1/capabilities 与提交前置门读的部署事实(多为 live getter,
653
- * 不是快照——/health 必须回答「此刻」)。 */
654
652
  export interface ServiceDeploymentDeps {
655
653
  /** Stable per-process id recorded on runs (audit / stale recovery). */
656
654
  instanceId?: string;
@@ -687,15 +685,10 @@ export interface ServiceDeploymentDeps {
687
685
  * (= 引擎附着租约集的大小)。消费方是 `boot/engine-lease.ts` 的自退判据;与 `inflight` 同族,
688
686
  * 同一个共享盒子,不另开第二条 main↔server 的通道。
689
687
  */
690
- drainState?: {
691
- draining: boolean;
692
- since?: number;
693
- reason?: string;
694
- inflight?: () => number;
695
- lastActivityAt?: () => number;
696
- attachedStreams?: () => number;
697
- lastAttachEndedAt?: () => number;
698
- };
688
+ drainState?: import("../fleet/drain-entry.js").DrainState;
689
+ /** 编排器片 2 件 3:服务租约此刻的读数(`fleet/serving-lease.ts` 的客户端交出;引擎信号铸点与计费准入判同读)。缺席 = 没装租约客户端
690
+ * (非 fleet 成员 / 没接中心 / 测试夹具)⇒ `none`(与没有租约的引擎逐字同)。 */
691
+ servingLease?: () => import("../fleet/engine-signal.js").ServingLeaseView;
699
692
  /** boot ready 门(b):false = 布防(`CONFIG_REQUIRE_ROSTER=true`)且首次 effective pull 尚未落 roster
700
693
  * (worker 只有占位模型)。就绪原因 `model_roster_pending`(`http/readiness.ts`):计费提交 503 + `/readyz` 503 +
701
694
  * /health 加性 `ready:false`。absent = 恒 ready(未布防 / 非 registry 部署),现有部署零影响。config-center 的
@@ -722,6 +715,9 @@ export interface ServiceDeploymentDeps {
722
715
  * (`boot/stage-10-http-server.ts` → `modelRefusals: () => deployment.modelRefusalsState?.()`)—— 一处铸、两出口。缺席 / 返回 undefined =
723
716
  * 没有可报的拒或没有配置管道 ⇒ `/health` 形状逐字不变(additive)。 */
724
717
  modelRefusalsState?: () => import("@sema-agent/settings-schema").ModelRefusalsSignal | undefined;
718
+ /** `/health.configHandoff`(只上 `/health`):在场 ⇔ 部署声明了耐久接力 ∧ 应用着的那一代没被确认(config-center 接力账的 `configHandoffState()`;合成律
719
+ * `fleet/engine-signal.ts` 的 `buildConfigHandoff`)。缺席 / 返回 undefined = 有接力、没有中心或本地腿 ⇒ 键缺席。 */
720
+ configHandoffState?: () => import("../fleet/engine-signal.js").ConfigHandoff | undefined;
725
721
  /** [ref] 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。
726
722
  * 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */
727
723
  configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">;
@@ -739,16 +735,6 @@ export interface ServiceDeploymentDeps {
739
735
  mcpRevocationState?: () => {
740
736
  revoked: string[];
741
737
  } | undefined;
742
- /** codex R18: the persistent "retained WITHOUT a next-boot handoff" state — a candidate whose restart signal is
743
- * withheld because no restart-surviving handoff exists yet (durable-declared LKG publication incomplete, or a
744
- * boot skills debt not yet proven repairable). Surfaced on /health (`modelPlaneDeferred` — the key keeps its birth
745
- * name: it was born for the model-plane deferral, which retired with the `models-tiers` slice in S-668 波 0) so
746
- * the stuck state outlives the one-shot warn (the etag advances → later ticks 304). */
747
- planeDeferredState?: () => {
748
- version: number;
749
- since: number;
750
- blocked?: string[];
751
- } | undefined;
752
738
  /** B1(鲁棒性批3 设计件):后台**缓存**探针的读座——/health 绝不逐请求打 DB(center/k8s 高频面),
753
739
  * main.ts 只在 SQL 后端(backend.kind !== "local")挂环(createStoreLiveProbe,STORE_PROBE_INTERVAL_MS)。
754
740
  * 缺席(local/memory/探针关)= /health 形状不变;接线 = `storeProbe{live,ageMs,error?}` 恒在
@@ -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 {
@@ -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
@@ -0,0 +1,14 @@
1
+ /**
2
+ * **机读位的值** —— 通告 `detail` 里的规则文本(`permission.allowed_tool_not_granted` 的 `detail.entries[].entry`、`permission.rule_row_dropped` 的
3
+ * `detail.rows[].rule`)在会话流行与运维日志行上一律是这一只:条目的脱敏原值。显示形不进机读位(壳若再加引号就是两层)。
4
+ */
5
+ export declare function redactListEntry(entry: string): string;
6
+ /**
7
+ * **人读句里条目的唯一写法** —— 本仓把规则条目 / 工具名原串写进任何一句给人读的话(拒句、起服拒启行)一律调这一只:引擎的条目写者
8
+ * `renderListEntry`(core 7.46.0 #1428 包根导出:JSON 串形带引号,看不见 / 印成空白的字符写成 `\uXXXX`,超长截断)—— 与引擎自己的通告句同一个渲染;
9
+ * 判定拿原串。
10
+ * 🔴 凭据先认、再渲染:渲染把看不见的字符写成 ASCII 转义,之后的脱敏器就认不出夹着零宽字符的凭据 ⇒ 原串上认出凭据时渲染它的脱敏值;
11
+ * 没有凭据的条目渲染**原串**(格式字符留给渲染器写成 `\uXXXX` —— 人要看得见它,不能先被脱敏器剥掉)。
12
+ */
13
+ export declare function displayListEntry(entry: string): string;
14
+ //# sourceMappingURL=list-entry-display.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { renderListEntry } from "@sema-agent/core";
2
+ import { redactSecretsWithArms } from "./trace/redact.js";
3
+ function scanListEntry(entry) {
4
+ const scan = redactSecretsWithArms(entry);
5
+ return { redacted: scan.text, credential: scan.arms.size > 0 };
6
+ }
7
+ export function redactListEntry(entry) {
8
+ return scanListEntry(entry).redacted;
9
+ }
10
+ export function displayListEntry(entry) {
11
+ const scan = scanListEntry(entry);
12
+ return renderListEntry(scan.credential ? scan.redacted : entry);
13
+ }
14
+ //# sourceMappingURL=list-entry-display.js.map