@sema-agent/server 7.101.1 → 7.103.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 (113) hide show
  1. package/MIGRATION.md +84 -0
  2. package/README.md +4 -4
  3. package/README.zh-CN.md +4 -4
  4. package/USAGE.md +156 -9
  5. package/dist/approval-card.d.ts +35 -2
  6. package/dist/approval-card.js +19 -1
  7. package/dist/background-session-reap.d.ts +58 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/bake-runner/main.d.ts +21 -3
  10. package/dist/bake-runner/main.js +18 -11
  11. package/dist/boot/config-center.d.ts +21 -4
  12. package/dist/boot/config-center.js +62 -19
  13. package/dist/boot/exit-boundary.js +17 -0
  14. package/dist/boot/runner-deps.js +1 -0
  15. package/dist/boot/runtime-caps.js +3 -2
  16. package/dist/boot/session-faces.js +3 -4
  17. package/dist/boot/shutdown.d.ts +2 -1
  18. package/dist/boot/shutdown.js +33 -12
  19. package/dist/boot/stage-01-config.js +2 -0
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-07-capability-layer.js +6 -4
  22. package/dist/boot/stage-08-reapers.d.ts +1 -1
  23. package/dist/boot/stage-09-leader.d.ts +1 -1
  24. package/dist/boot/stage-10-http-server.d.ts +1 -1
  25. package/dist/boot/stage-10-http-server.js +13 -4
  26. package/dist/brain.d.ts +28 -8
  27. package/dist/brain.js +20 -4
  28. package/dist/capabilities/builtin-tools.d.ts +14 -1
  29. package/dist/capabilities/builtin-tools.js +3 -0
  30. package/dist/capabilities/center-plugins.d.ts +9 -3
  31. package/dist/capabilities/center-plugins.js +5 -10
  32. package/dist/capabilities/scenarios.d.ts +1 -1
  33. package/dist/capabilities/scenarios.js +3 -3
  34. package/dist/center-credential-renewer.d.ts +76 -0
  35. package/dist/center-credential-renewer.js +359 -0
  36. package/dist/center-credential.d.ts +76 -22
  37. package/dist/center-credential.js +119 -43
  38. package/dist/config-catalog.js +17 -8
  39. package/dist/config-center/apply-effective.d.ts +16 -0
  40. package/dist/config-center/apply-effective.js +26 -21
  41. package/dist/config-center/center-request.d.ts +13 -0
  42. package/dist/config-center/center-request.js +23 -3
  43. package/dist/config-center/facade.d.ts +1 -1
  44. package/dist/config-center/http-client.js +2 -5
  45. package/dist/config-center/mcp-revocation.d.ts +2 -2
  46. package/dist/config-center/mcp-revocation.js +1 -1
  47. package/dist/config-center/read-warnings.d.ts +19 -48
  48. package/dist/config-center/read-warnings.js +7 -49
  49. package/dist/config-center/restart-signal.d.ts +8 -21
  50. package/dist/config-center/restart-signal.js +2 -12
  51. package/dist/config-center/types.d.ts +4 -2
  52. package/dist/config-provider.js +1 -48
  53. package/dist/config-types.d.ts +107 -17
  54. package/dist/config-types.js +7 -2
  55. package/dist/config.d.ts +7 -0
  56. package/dist/config.js +91 -29
  57. package/dist/device-store.d.ts +1 -1
  58. package/dist/env-name-allowlist-knobs.d.ts +21 -7
  59. package/dist/env-name-allowlist-knobs.js +7 -2
  60. package/dist/fleet-client.d.ts +27 -7
  61. package/dist/fleet-client.js +43 -12
  62. package/dist/fleet-lease.d.ts +1 -1
  63. package/dist/host-lsp-manager.d.ts +4 -3
  64. package/dist/host-lsp-manager.js +2 -6
  65. package/dist/http/approval-sweeps.js +5 -20
  66. package/dist/http/dispatch.js +24 -14
  67. package/dist/http/readiness.d.ts +87 -0
  68. package/dist/http/readiness.js +54 -0
  69. package/dist/http/resume-legs.js +5 -4
  70. package/dist/http/route-table.d.ts +7 -0
  71. package/dist/http/route-table.js +6 -0
  72. package/dist/http/routes/a2a-serve.js +6 -14
  73. package/dist/http/routes/approvals-assistant.js +1 -0
  74. package/dist/http/routes/background.d.ts +80 -0
  75. package/dist/http/routes/background.js +94 -0
  76. package/dist/http/routes/capabilities.js +2 -0
  77. package/dist/http/server.d.ts +25 -8
  78. package/dist/http/server.js +2 -2
  79. package/dist/leader/wire.d.ts +8 -1
  80. package/dist/leader/wire.js +4 -3
  81. package/dist/model-entry-refusal.d.ts +42 -19
  82. package/dist/model-entry-refusal.js +34 -19
  83. package/dist/model-route-endpoint.d.ts +10 -0
  84. package/dist/model-route-endpoint.js +6 -2
  85. package/dist/observability/fail-open.d.ts +2 -2
  86. package/dist/observability/fail-open.js +2 -2
  87. package/dist/observability/secret-env-scrub.d.ts +12 -0
  88. package/dist/observability/secret-env-scrub.js +12 -0
  89. package/dist/outbound-dispatcher.d.ts +125 -0
  90. package/dist/outbound-dispatcher.js +211 -0
  91. package/dist/parked-decide.d.ts +11 -5
  92. package/dist/parked-decide.js +4 -1
  93. package/dist/parked-revive-surface.d.ts +47 -0
  94. package/dist/parked-revive-surface.js +20 -0
  95. package/dist/plugins/background-shell-support.d.ts +20 -1
  96. package/dist/plugins/background-shell-support.js +26 -0
  97. package/dist/plugins/host-platform.d.ts +16 -1
  98. package/dist/plugins/host-platform.js +11 -0
  99. package/dist/plugins/remote-env-host.d.ts +14 -1
  100. package/dist/plugins/remote-env-host.js +15 -10
  101. package/dist/project-memory.js +3 -6
  102. package/dist/run-local.js +2 -0
  103. package/dist/server-secret-env.d.ts +97 -24
  104. package/dist/server-secret-env.js +37 -1
  105. package/dist/tool-approval.d.ts +30 -1
  106. package/dist/tool-approval.js +31 -8
  107. package/dist/trace/core-keyset-guard.d.ts +9 -4
  108. package/dist/trace/engine-notice-wire.d.ts +1 -1
  109. package/dist/trace/engine-notice-wire.js +2 -0
  110. package/dist/trace/project.js +13 -0
  111. package/dist/trace/redact.d.ts +11 -0
  112. package/dist/trace/redact.js +1 -0
  113. package/package.json +4 -3
@@ -3,8 +3,9 @@ import { setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isDestructiveSessio
3
3
  import { publicStoreProbeError } from "../store-live-probe.js";
4
4
  import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
5
5
  import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
6
- import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute } from "./route-table.js";
6
+ import { PRE_CREDENTIAL_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
7
7
  import { sendJson, sendError, isSseResponse } from "./send.js";
8
+ import { readinessOf, healthReadinessKeys, sendReadyz, billableRefusalOf, sendBillableRefusal } from "./readiness.js";
8
9
  import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal } from "./principal-gate.js";
9
10
  const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
10
11
  export function hasServiceAuth(config) {
@@ -67,7 +68,7 @@ export function createDispatch(ctx) {
67
68
  const search = qMark === -1 ? "" : rawUrl.slice(qMark);
68
69
  const allow = { pre: [], post: [] };
69
70
  noteActivity(url);
70
- const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
71
+ const admitted = method !== "OPTIONS" && !isProbePath(url);
71
72
  if (admitted)
72
73
  counters.admittedInflight++;
73
74
  if (admitted)
@@ -89,7 +90,7 @@ export function createDispatch(ctx) {
89
90
  const status = aborted ? "499" : String(res.statusCode);
90
91
  deps.metrics?.inc("http_requests_total", { route, method, status });
91
92
  deps.metrics?.observe("http_request_duration_seconds", seconds, { route });
92
- const log = route === "/health" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
93
+ const log = route === "/health" || route === "/readyz" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
93
94
  log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(reqState.runStatus !== undefined ? { runStatus: reqState.runStatus } : {}), ...(reqState.runErrorCode !== undefined ? { errorCode: reqState.runErrorCode } : {}), ...(aborted ? { aborted: true, ...(reqState.streamTaskId ? { taskId: reqState.streamTaskId } : {}), ...(reqState.streamDetached ? { detached: true } : {}) } : {}) });
94
95
  };
95
96
  res.on("finish", () => record(false));
@@ -112,7 +113,7 @@ export function createDispatch(ctx) {
112
113
  ...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}), ...(deps.drainState.reason ? { drainReason: deps.drainState.reason } : {}) } : {}),
113
114
  ...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
114
115
  ...(deps.storeDegraded ? { durable: false, storeDegraded: true } : {}),
115
- ...(deps.modelReady && !deps.modelReady() ? { ready: false, readyReason: "model roster pending (first effective-config pull)" } : {}),
116
+ ...healthReadinessKeys(readinessOf(deps)),
116
117
  model: deps.config.model.id,
117
118
  sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
118
119
  sessions: deps.runner.sessions.size,
@@ -123,7 +124,7 @@ export function createDispatch(ctx) {
123
124
  startedAt: PROCESS_STARTED_AT_MS,
124
125
  port: deps.config.port,
125
126
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
126
- ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
127
+ ...(restart ? { restartRequired: true, restart } : {}),
127
128
  ...(() => {
128
129
  const stuck = deps.planeDeferredState?.();
129
130
  return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
@@ -140,6 +141,14 @@ export function createDispatch(ctx) {
140
141
  ...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
141
142
  };
142
143
  })(),
144
+ ...(() => {
145
+ const credential = deps.centerCredentialState?.();
146
+ return credential ? { centerCredential: credential } : {};
147
+ })(),
148
+ ...(() => {
149
+ const modelRefusals = deps.modelRefusalsState?.();
150
+ return modelRefusals ? { modelRefusals } : {};
151
+ })(),
143
152
  ...(() => {
144
153
  const probe = deps.storeLiveState?.();
145
154
  if (!probe)
@@ -152,6 +161,10 @@ export function createDispatch(ctx) {
152
161
  });
153
162
  return;
154
163
  }
164
+ if (req.method === "GET" && url === "/readyz") {
165
+ sendReadyz(res, readinessOf(deps));
166
+ return;
167
+ }
155
168
  if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
156
169
  if (!deps.metrics) {
157
170
  sendError(res, 404, "feature.metrics_disabled", "metrics disabled");
@@ -218,15 +231,12 @@ export function createDispatch(ctx) {
218
231
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
219
232
  return;
220
233
  }
221
- if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
222
- res.setHeader("retry-after", "15");
223
- sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance", ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}) });
224
- return;
225
- }
226
- if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
227
- res.setHeader("retry-after", "5");
228
- sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
229
- return;
234
+ if (req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
235
+ const verdict = readinessOf(deps);
236
+ if (!verdict.ready) {
237
+ sendBillableRefusal(res, billableRefusalOf(verdict, deps.drainState?.reason));
238
+ return;
239
+ }
230
240
  }
231
241
  if (isDestructiveSessionWrite(req.method ?? "", url) && !anyServiceAuth && !deps.config.allowUnauthedWrites) {
232
242
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
@@ -0,0 +1,87 @@
1
+ /**
2
+ * S-739 / S-740(车OE,7.102.0):**就绪判的唯一属主** —— 一只谓词 {@link readinessOf},全部读点经它:
3
+ *
4
+ * ① `GET /readyz`(`dispatch.ts`,凭据门之前,与 `/health` 同位):200 `{ready:true}` / 503 `{ready:false, reasons}` ——
5
+ * 给**摘流**语义的探针(k8s readinessProbe / LB 健康检查);**重启**语义的探针(Docker HEALTHCHECK、k8s livenessProbe)
6
+ * 留在 `/health`:中心宕机时整队副本同卡 roster 未落,指 `/readyz` 就会一起重启打转(对抗复审 #6);
7
+ * ② `GET /health` 的 `ready` / `readyReason` / `reasons` 三键({@link healthReadinessKeys});
8
+ * ③ 计费提交门的 503(`dispatch.ts` 的 billable 门、`routes/a2a-serve.ts` 的 `message/send`、
9
+ * `approval-sweeps.ts` 的 PARKED 赎回席 —— 三处同调 {@link billableRefusalOf},拒绝体只此一处铸);
10
+ * ④ fleet 心跳 `InstanceAnnouncement.ready` / `draining`(`fleet-client.ts`;装配层接
11
+ * `readinessOf(deployment)` —— 与 HTTP 面读的是同一个 deps 组)。
12
+ *
13
+ * 修前是四份手抄:`/health` 读 `modelReady`、计费门两道各读一只、a2a 与赎回席各抄一遍,心跳读
14
+ * `!isDraining()`(不含 roster)⇒ roster 未落时心跳说「就绪」而计费提交在 503,中心按心跳把它当可路由。
15
+ *
16
+ * **reasons 闭集 = `draining | model_roster_pending`,刻意不含 storeLive**(web-admin [ref] 要、[ref] 裁):
17
+ * DB 抖动是**共模**失效 —— 一次库抖若进就绪门,整队副本会在同一拍被 k8s / LB 摘出路由,比「打到一台库连不上
18
+ * 的副本」更坏。`storeLive:false` 留在 `/health` 体位 + 既有告警键,摘不摘流由读体的编排器裁(披露不代裁)。
19
+ * 加一个 reason = 改 {@link READINESS_REASONS} ⇒ 下面两只按 reason 穷举的表(`READY_REASON_PHRASE` 的
20
+ * 映射型 / `billableRefusalOf` 的 switch)当场编译红,逼你给出它的 `/health` 句与计费门拒绝体。
21
+ *
22
+ * 模块形:纯叶(只值 import `send.ts`)—— `fleet-client.ts` / `boot/` 都会读它,不许长出回环。
23
+ */
24
+ import type { ServerResponse } from "node:http";
25
+ /**
26
+ * 就绪原因闭集,**数组序 = 优先序**:`/health.readyReason` 取首条的句、计费门取首条的拒绝码。
27
+ * draining 在前 —— 一台正在下线的副本「去找替补」(Retry-After 15)比「稍后重试本机」(5)更对。
28
+ */
29
+ export declare const READINESS_REASONS: readonly ["draining", "model_roster_pending"];
30
+ export type ReadinessReason = (typeof READINESS_REASONS)[number];
31
+ /** 就绪判。未就绪时 `reasons` 非空(类型上就是非空元组 —— 空数组的未就绪判构造不出来)、按
32
+ * {@link READINESS_REASONS} 序、无重复。 */
33
+ export type ReadinessVerdict = {
34
+ readonly ready: true;
35
+ } | {
36
+ readonly ready: false;
37
+ readonly reasons: readonly [ReadinessReason, ...ReadinessReason[]];
38
+ };
39
+ export type NotReadyVerdict = Extract<ReadinessVerdict, {
40
+ ready: false;
41
+ }>;
42
+ /**
43
+ * 谓词的输入 —— `ServiceDeps` 上的同名两键(`modelReady` / `drainState`),HTTP 面与心跳读的是**同一个对象**。
44
+ * 两键缺席 = 该输入恒就绪(纯 env 部署没有 roster 可等;嵌入宿主 / 测试夹具没有 drain 面)。
45
+ */
46
+ export interface ReadinessInputs {
47
+ /** boot ready 门(b):false = 布防且 roster 未落(`CONFIG_REQUIRE_ROSTER=true`)。 */
48
+ readonly modelReady?: () => boolean;
49
+ /** 收尾链翻转的共享 drain 态。 */
50
+ readonly drainState?: {
51
+ readonly draining: boolean;
52
+ };
53
+ }
54
+ /** **唯一**的就绪谓词(活判,每次调用现读输入 —— 不是快照)。 */
55
+ export declare function readinessOf(deps: ReadinessInputs): ReadinessVerdict;
56
+ /**
57
+ * `/health` 的就绪三键(additive:就绪 ⇒ 空对象,`/health` 正常形状逐字不变)。
58
+ * `reasons` = 与 `/readyz` 逐字同一份闭集词;`readyReason` = 首条的人读句(旧消费端读的就是它)。
59
+ */
60
+ export declare function healthReadinessKeys(v: ReadinessVerdict): Record<string, unknown>;
61
+ /** `/readyz` 两形:200 `{ready:true}` / 503 `{ready:false, reasons}` + `Retry-After: 5`。体只这两键(不带身份字段:
62
+ * 就绪探针给 k8s readinessProbe / Traefik / LB 摘流用,不是 `/health` 的替身;`drainReason` 等运维自由文本也不上这一面)。 */
63
+ export declare function sendReadyz(res: ServerResponse, v: ReadinessVerdict): void;
64
+ /** 计费提交门的拒绝(首条 reason 定码)。`retryAfterSec` 由发送形(`sendBillableRefusal`)镜像成头;交回
65
+ * `{status, body}` 的赎回席只取体(它的路由刻意不带头,见 `approval-sweeps.ts` 该臂注)。 */
66
+ export interface BillableRefusal {
67
+ readonly status: 503;
68
+ readonly body: {
69
+ readonly error: string;
70
+ readonly errorCode: string;
71
+ readonly message: string;
72
+ readonly reason?: string;
73
+ };
74
+ readonly retryAfterSec: 15 | 5;
75
+ }
76
+ /**
77
+ * 计费提交门的拒绝体 —— **唯一铸点**(`error` 字面 `"draining"` / `"model_roster_pending"` 与 `message` 是冻结的
78
+ * wire 契约:SDK / 壳的 pre-stream 重试按它们判型;冻结在 `test/api-error-text-freeze.test.ts` 的
79
+ * `FROZEN_RESULT_BODY` 与 `test/api-notice-text-freeze.test.ts` §2)。`drainReason` = `POST /v1/admin/drain`
80
+ * 声明的停机因由([ref],additive 键 `reason`;未声明 = 键缺席)。
81
+ */
82
+ export declare function billableRefusalOf(v: NotReadyVerdict, drainReason: string | undefined): BillableRefusal;
83
+ /** 发送形:`Retry-After` + 体(`dispatch.ts` 的 billable 门与 a2a `message/send` 同调)。走 `sendError` —— errorCode
84
+ * 是它的必填位置参(对抗复审 #3:修前这里是 `sendJson(res, r.status, r.body)`,与 leader 口的豁免行逐字同文,
85
+ * 在 error-code-key-gate 上借了别人的豁免过门);体键序与修前逐字同(`error, errorCode, message, reason?`)。 */
86
+ export declare function sendBillableRefusal(res: ServerResponse, refusal: BillableRefusal): void;
87
+ //# sourceMappingURL=readiness.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { sendJson, sendError } from "./send.js";
2
+ export const READINESS_REASONS = ["draining", "model_roster_pending"];
3
+ export function readinessOf(deps) {
4
+ const [first, ...rest] = READINESS_REASONS.filter((r) => holds(r, deps));
5
+ return first === undefined ? { ready: true } : { ready: false, reasons: [first, ...rest] };
6
+ }
7
+ function holds(reason, deps) {
8
+ switch (reason) {
9
+ case "draining":
10
+ return deps.drainState?.draining === true;
11
+ case "model_roster_pending":
12
+ return deps.modelReady !== undefined && !deps.modelReady();
13
+ default: {
14
+ const unhandled = reason;
15
+ throw new Error(`readiness reason without a predicate arm: ${String(unhandled)}`);
16
+ }
17
+ }
18
+ }
19
+ const READY_REASON_PHRASE = {
20
+ draining: "draining for shutdown or upgrade",
21
+ model_roster_pending: "model roster pending (first effective-config pull)",
22
+ };
23
+ export function healthReadinessKeys(v) {
24
+ if (v.ready)
25
+ return {};
26
+ return { ready: false, readyReason: READY_REASON_PHRASE[v.reasons[0]], reasons: [...v.reasons] };
27
+ }
28
+ export function sendReadyz(res, v) {
29
+ if (v.ready) {
30
+ sendJson(res, 200, { ready: true });
31
+ return;
32
+ }
33
+ res.setHeader("retry-after", "5");
34
+ sendJson(res, 503, { ready: false, reasons: [...v.reasons] });
35
+ }
36
+ export function billableRefusalOf(v, drainReason) {
37
+ const primary = v.reasons[0];
38
+ switch (primary) {
39
+ case "draining":
40
+ return { status: 503, body: { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance", ...(drainReason ? { reason: drainReason } : {}) }, retryAfterSec: 15 };
41
+ case "model_roster_pending":
42
+ return { status: 503, body: { error: "model_roster_pending", errorCode: "state.model_roster_pending", message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" }, retryAfterSec: 5 };
43
+ default: {
44
+ const unhandled = primary;
45
+ throw new Error(`readiness reason without a billable refusal: ${String(unhandled)}`);
46
+ }
47
+ }
48
+ }
49
+ export function sendBillableRefusal(res, refusal) {
50
+ const { error, errorCode, ...extra } = refusal.body;
51
+ res.setHeader("retry-after", String(refusal.retryAfterSec));
52
+ sendError(res, refusal.status, errorCode, error, extra);
53
+ }
54
+ //# sourceMappingURL=readiness.js.map
@@ -236,10 +236,10 @@ export function createResumeLegs(ctx) {
236
236
  const ownerLeaseAdmittedHere = req !== undefined;
237
237
  if (req !== undefined) {
238
238
  const parkedAgentStore = deps.backgroundAgentStore;
239
- const parkedTool = deps.parkedReviveTool;
239
+ const parkedSurface = deps.parkedReviveSurface;
240
240
  const parkWarn = (event, fields) => deps.logger?.warn?.(event, fields);
241
241
  const origin = await findParkOrigin({
242
- ...(parkedAgentStore !== undefined && parkedTool !== undefined ? { agentStore: parkedAgentStore } : {}),
242
+ ...(parkedAgentStore !== undefined && parkedSurface !== undefined ? { agentStore: parkedAgentStore } : {}),
243
243
  ...(deps.workflowAgentSessionIndex !== undefined && deps.workflowRunStore !== undefined
244
244
  ? { workflowIndex: deps.workflowAgentSessionIndex, workflowRunStore: deps.workflowRunStore }
245
245
  : {}),
@@ -252,12 +252,13 @@ export function createResumeLegs(ctx) {
252
252
  ...(answer !== undefined ? { answer } : {}),
253
253
  ...(boundGrant ? { grantRemember: (rootSessionId) => boundGrant(rootSessionId) } : {}),
254
254
  };
255
- if (origin?.kind === "background" && parkedAgentStore !== undefined && parkedTool !== undefined) {
255
+ if (origin?.kind === "background" && parkedAgentStore !== undefined && parkedSurface !== undefined) {
256
256
  const parked = await withPrincipal(decodeCheckpointScope(cp.scope), () => decideParkedAgent({
257
257
  agentStore: parkedAgentStore,
258
258
  checkpointStore: cs,
259
259
  registry: defaultTaskRegistry,
260
- reviveTool: parkedTool,
260
+ reviveSurface: parkedSurface,
261
+ ...(deps.sessionStorage?.ownerOf ? { sessionOwnerOf: deps.sessionStorage.ownerOf.bind(deps.sessionStorage) } : {}),
261
262
  ...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
262
263
  ...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
263
264
  ...(deps.logger ? { warn: parkWarn } : {}),
@@ -83,6 +83,13 @@ export declare const ROUTES: readonly RouteRow[];
83
83
  * 「门前行不许出现在门后行之后」由 `test/route-order-freeze.test.ts` 钉住。 */
84
84
  export declare const PRE_CREDENTIAL_ROUTES: readonly RouteRow[];
85
85
  export declare const POST_CREDENTIAL_ROUTES: readonly RouteRow[];
86
+ /**
87
+ * **探针路径**(k8s / 编排器 / 抓取器的常态高频采样):不计 admission inflight、不推活性时钟(`http/server.ts`
88
+ * 的 `noteActivity`)、不入附着租约集 —— 常态采样噪音不许把 SIGHUP 空闲窗 / 引擎租约的「无人使用」判据永远撑开。
89
+ * 两处消费(`dispatch.ts` 的 admission 门、`server.ts` 的活性时钟)读这一只;S-739 前是两份手抄
90
+ * (`url === "/health" || url.startsWith("/metrics")`),加 `/readyz` 时漏一份 = k8s 每 5-10s 一探就让副本永不空闲。
91
+ */
92
+ export declare function isProbePath(url: string): boolean;
86
93
  /**
87
94
  * 全表 = 流水线四行 + **每一行 `ROUTES` 自己的 `decls`**,按行序摊平。
88
95
  *
@@ -29,6 +29,7 @@ import { handleApprovalsAssistant, APPROVALS_ASSISTANT_ROUTES } from "./routes/a
29
29
  import { handleRuns, handleRunVerbs, RUNS_ROUTES, RUN_VERBS_ROUTES } from "./routes/runs.js";
30
30
  import { handleRunMemoryCapture, RUN_MEMORY_CAPTURE_ROUTES } from "./routes/run-memory-capture.js";
31
31
  import { handleMcpReconnect, MCP_RECONNECT_ROUTES } from "./routes/mcp-reconnect.js";
32
+ import { handleBackground, BACKGROUND_ROUTES } from "./routes/background.js";
32
33
  import { handleSideQuery, SIDE_QUERY_ROUTES } from "./routes/side-query.js";
33
34
  import { handleTasks, TASKS_ROUTES } from "./routes/tasks.js";
34
35
  import { handleLeader, LEADER_ROUTES } from "./routes/leader.js";
@@ -50,6 +51,7 @@ export const ROUTES = [
50
51
  row({ handler: handleRunVerbs, decls: RUN_VERBS_ROUTES, position: "post-credential", label: "run-verbs" }),
51
52
  row({ handler: handleRunMemoryCapture, decls: RUN_MEMORY_CAPTURE_ROUTES, position: "post-credential", label: "run-memory-capture" }),
52
53
  row({ handler: handleMcpReconnect, decls: MCP_RECONNECT_ROUTES, position: "post-credential", label: "mcp-reconnect" }),
54
+ row({ handler: handleBackground, decls: BACKGROUND_ROUTES, position: "post-credential", label: "background" }),
53
55
  row({ handler: handleLeader, decls: LEADER_ROUTES, position: "post-credential", label: "leader", enabled: (deps) => Boolean(deps.leaderEndpoint) }),
54
56
  row({ handler: handleImages, decls: IMAGES_ROUTES, position: "post-credential", label: "images" }),
55
57
  row({ handler: handleApprovalsAssistant, decls: APPROVALS_ASSISTANT_ROUTES, position: "post-credential", label: "approvals-assistant", enabled: (deps) => Boolean(deps.checkpointStore) }),
@@ -80,10 +82,14 @@ export const PRE_CREDENTIAL_ROUTES = ROUTES.filter((r) => r.position === "pre-cr
80
82
  export const POST_CREDENTIAL_ROUTES = ROUTES.filter((r) => r.position === "post-credential");
81
83
  const PIPELINE_ROUTES = [
82
84
  { id: "health", path: "/health", methods: ["GET"] },
85
+ { id: "readyz", path: "/readyz", methods: ["GET"] },
83
86
  { id: "metrics", path: "/metrics", methods: ["GET"] },
84
87
  { id: "metrics-summary", path: "/metrics/summary", methods: ["GET"] },
85
88
  { id: "metrics-plan-cache", path: "/metrics/plan-cache", methods: ["GET"] },
86
89
  ];
90
+ export function isProbePath(url) {
91
+ return url === "/health" || url === "/readyz" || url.startsWith("/metrics");
92
+ }
87
93
  export const ROUTE_DECLARATIONS = [...PIPELINE_ROUTES, ...ROUTES.flatMap((r) => r.decls)];
88
94
  function declMatches(d, url) {
89
95
  if ("path" in d)
@@ -6,6 +6,7 @@ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-cont
6
6
  import { HttpError } from "../../security.js";
7
7
  import { scopedIdempotencyKey } from "../idempotency.js";
8
8
  import { sendJson, sendError } from "../send.js";
9
+ import { readinessOf, billableRefusalOf, sendBillableRefusal } from "../readiness.js";
9
10
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
10
11
  import { assertNever } from "../route-ctx.js";
11
12
  import { createDurableRun } from "./runs.js";
@@ -169,20 +170,11 @@ function rpcErrorFromTypedFailure(err, skillId) {
169
170
  }
170
171
  function submitUnavailable(res, ctx) {
171
172
  const { deps } = ctx;
172
- if (deps.drainState?.draining) {
173
- res.setHeader("retry-after", "15");
174
- sendError(res, 503, "draining", "draining", {
175
- message: "this instance is draining for shutdown/upgrade — retry against the replacement instance",
176
- ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}),
177
- });
178
- return true;
179
- }
180
- if (deps.modelReady && !deps.modelReady()) {
181
- res.setHeader("retry-after", "5");
182
- sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
183
- return true;
184
- }
185
- return false;
173
+ const verdict = readinessOf(deps);
174
+ if (verdict.ready)
175
+ return false;
176
+ sendBillableRefusal(res, billableRefusalOf(verdict, deps.drainState?.reason));
177
+ return true;
186
178
  }
187
179
  function readBlocking(params) {
188
180
  if (params.configuration === undefined)
@@ -289,6 +289,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
289
289
  ...(s.hasBidiControls === true ? { hasBidiControls: true } : {}),
290
290
  ...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
291
291
  ...(s.mandated === true ? { mandated: true } : {}),
292
+ ...(s.mandate !== undefined ? { mandate: s.mandate } : {}),
292
293
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
293
294
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
294
295
  ...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
@@ -0,0 +1,80 @@
1
+ /**
2
+ * S-745(core 7.32.0 #1107 的 server 半场;wire 形 [ref] / [ref] / [ref] 定)—— **会话后台任务的两只读口**:
3
+ *
4
+ * `GET /v1/sessions/:id/background` → 200 `{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained? }] }`
5
+ * `POST /v1/sessions/:id/background/stop` body `{ includeRetained: boolean }` → 200 `{ receipts: [{ id, outcome }] }`
6
+ *
7
+ * 消费方:cli L-694 ①(删前核登记)/ L-686(退出三选一:「Exit and stop tasks」= `includeRetained:true`,`sema kill` =
8
+ * `includeRetained:false`,两臂都等受据再退)/ client-core CC-194 B 半(对账谓词)。老 server 404 ⇒ 壳只出 Stay / 退出两臂。
9
+ *
10
+ * ── 读口:逐字投 core `list()` 行 ─────────────────────────────────────────────────────────────────
11
+ * 读的是**进程内**注册表(`defaultTaskRegistry.list`),访问元组 = 会话自己的 `{ owner: sid, sessionId: sid, scope }`
12
+ * (scope = 会话行属主 `owner ?? "default"`,与 core 注册铸值同源)⇒ 可见集完全由 core 的 `canAccess` 判(fail-closed:
13
+ * 缺轴即拒;本会话的会话驻留行、task id 即会话 id 的 run 行、本会话发起的 workflow、根会话是本会话的后台 agent)。
14
+ * 键逐个处置(下面的 {@link ROW_KEY_DISPOSITION},core 给行加键 ⇒ 编译红):`task_id` 改名 `id`;`retained` **真才带**
15
+ * (core:缺席 ⇒ 这一行在 `includeRetained:false` 的硬面上会停);`description` 是模型写的描述、缺席时是启动时的命令文本 ——
16
+ * 内容族 ⇒ `redactSecrets` + 截 {@link DESCRIPTION_MAX}(显示串;core 不另存原始命令)。多副本部署:只列**本副本**上的行。
17
+ *
18
+ * ── 停止口:会话级收敛同一只函数 ────────────────────────────────────────────────────────────────
19
+ * `reapSessionBackgroundHard`(`background-session-reap.ts`,与 `DELETE /v1/sessions/:id` 同一只):软面(会话的后台 agent /
20
+ * workflow 一并结算;`includeRetained:true` 时 retain 行也先收到 SIGTERM)→ 等一次 `HOST_BG_KILL_GRACE_MS` → 硬面(车OL O1 / O2);
21
+ * `includeRetained` **没有缺省**(core 同一条:调用方说明这是哪一种停),缺席 / 非布尔 ⇒ 400。
22
+ * 受据只含 shell 行(`background_bash` / `monitor`),`outcome` 四词逐字透传:`killed` / `already_gone` / `retained_skipped` /
23
+ * `no_pgid`(= 没有同步硬收落地、已派普通 kill,「可能还在跑」—— 远端 / 沙箱车道的常态,不是失败)。受据在 200 之前已齐(`await`
24
+ * 收敛函数;有后台行时应答慢一次宽限)。
25
+ *
26
+ * ── 鉴权与门 ────────────────────────────────────────────────────────────────────────────────────
27
+ * 与 `POST /v1/sessions/:id/mcp/reconnect` 同一套:会话属主面缺席 ⇒ 501;未知会话与他人会话**同码同串** 404(无存在性
28
+ * oracle);显式 operator 可读可停任何会话(监督动作)。零模型工作 ⇒ 不进 billable、不吃 drain 503;停止口是 mutating ⇒
29
+ * `rateLimited`。能力位 `background: { listFace, exitFaces }` 与两口的 501 从同一个谓词推导(会话属主面在场)。
30
+ *
31
+ * 🔴 **停止口在无凭据写门内**(`credentialGated: ["POST"]`;车OL F1 = 7.102.0 合并复审 gpt-6 腿 [high]):门的两个合取项它都中
32
+ * —— ① 授权的唯一输入是 principal 头(属主判 `runOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
33
+ * 无 service 凭证的部署上它收调用方自报的头);② 不可撤销、爆炸半径跨租户(知道会话 id + 自报它的属主 ⇒ 停掉那个会话的后台
34
+ * 进程,`includeRetained:true` 连显式 retain 的活服务一起停)。于是无 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` 的部署上它与
35
+ * 其余改写门同码同句 503 `auth.service_token_required`(在路由之前,`dispatch.ts` 那一道),同一只会话级收敛函数的另一个调用点
36
+ * `DELETE /v1/sessions/:id` 早在同一形下被 `isDestructiveSessionWrite` 拒。读口 `GET …/background` 不进:它有属主门兜底、只读、
37
+ * 只列本会话,与 `GET /v1/rules` 同判。已知同族缺口(不在本口单独打补丁):能力位 `exitFaces` 不含「凭据就绪」这一项,见契约
38
+ * 附录 B.1 `memoryCompliance` 行的同族说明。
39
+ */
40
+ import type { IncomingMessage, ServerResponse } from "node:http";
41
+ import { type TaskRegistry, type BackgroundShellHardKillReceipt } from "@sema-agent/core";
42
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
43
+ /** `description` 的显示截断(模型描述通常一句话;回落的命令文本可能是一整段脚本)。 */
44
+ export declare const DESCRIPTION_MAX = 500;
45
+ /** core `list()` 的行型(从唯一的消费位取,不抄结构)。 */
46
+ type RegistryListRow = ReturnType<TaskRegistry["list"]>[number];
47
+ /** 读口的一行(白名单显式构造;缺席的可选键不铸)。 */
48
+ export interface SessionBackgroundRow {
49
+ id: string;
50
+ description?: string;
51
+ type: RegistryListRow["type"];
52
+ status: RegistryListRow["status"];
53
+ createdAt: number;
54
+ sessionScoped: boolean;
55
+ retained?: true;
56
+ }
57
+ /** 停止口的一条受据(`outcome` 逐字透传 core 闭集)。 */
58
+ export interface SessionBackgroundReceipt {
59
+ id: string;
60
+ outcome: BackgroundShellHardKillReceipt["outcome"];
61
+ }
62
+ /** 能力位与两口 501 的**同一个**谓词:会话属主面在场(入站 owner 门 fail-closed,缺它两口都 501)。 */
63
+ export declare function sessionBackgroundFacesAvailable(deps: Pick<RouteCtx["deps"], "sessionStorage">): boolean;
64
+ export declare function handleBackground(req: IncomingMessage, res: ServerResponse, match: RouteMatch<BackgroundRouteId>, ctx: RouteCtx): Promise<void>;
65
+ export declare const BACKGROUND_ROUTES: readonly [{
66
+ readonly id: "session-background-list";
67
+ readonly pattern: RegExp;
68
+ readonly label: "/v1/sessions/:id/background";
69
+ readonly methods: readonly ["GET"];
70
+ }, {
71
+ readonly id: "session-background-stop";
72
+ readonly pattern: RegExp;
73
+ readonly label: "/v1/sessions/:id/background/stop";
74
+ readonly credentialGated: readonly ["POST"];
75
+ readonly methods: readonly ["POST"];
76
+ }];
77
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
78
+ export type BackgroundRouteId = RouteIdsOf<typeof BACKGROUND_ROUTES>;
79
+ export {};
80
+ //# sourceMappingURL=background.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { defaultTaskRegistry } from "@sema-agent/core";
3
+ import { reapSessionBackgroundHard } from "../../background-session-reap.js";
4
+ import { redactSecrets } from "../../trace/redact.js";
5
+ import { sendJson, sendError } from "../send.js";
6
+ import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
7
+ import {} from "../route-ctx.js";
8
+ import { assertNever } from "../route-ctx.js";
9
+ const LIST_LIMIT = 500;
10
+ export const DESCRIPTION_MAX = 500;
11
+ const ROW_KEY_DISPOSITION = {
12
+ task_id: "keep",
13
+ type: "keep",
14
+ status: "keep",
15
+ createdAt: "keep",
16
+ sessionScoped: "keep",
17
+ retained: "keep",
18
+ description: "keep",
19
+ toolUseId: "strip",
20
+ updatedAt: "strip",
21
+ };
22
+ void ROW_KEY_DISPOSITION;
23
+ function projectRow(r) {
24
+ return {
25
+ id: r.task_id,
26
+ ...(r.description !== undefined ? { description: redactSecrets(r.description).slice(0, DESCRIPTION_MAX) } : {}),
27
+ type: r.type,
28
+ status: r.status,
29
+ createdAt: r.createdAt,
30
+ sessionScoped: r.sessionScoped,
31
+ ...(r.retained === true ? { retained: true } : {}),
32
+ };
33
+ }
34
+ const StopBodySchema = z.object({ includeRetained: z.boolean() }).strict();
35
+ export function sessionBackgroundFacesAvailable(deps) {
36
+ return Boolean(deps.sessionStorage?.ownerOf);
37
+ }
38
+ export async function handleBackground(req, res, match, ctx) {
39
+ const { deps } = ctx;
40
+ const id = match.decl.id;
41
+ if (id === "session-background-stop" && ctx.helpers.rateLimited(req, res))
42
+ return;
43
+ const principal = gatedPrincipal(req, deps.config);
44
+ if (deps.config.requirePrincipal && principal === undefined) {
45
+ sendPrincipalRefusal(res, req, deps.config);
46
+ return;
47
+ }
48
+ let includeRetained = false;
49
+ if (id === "session-background-stop") {
50
+ const parsed = StopBodySchema.safeParse((await ctx.helpers.readJson(req)) ?? {});
51
+ if (!parsed.success) {
52
+ sendError(res, 400, "request.body_shape", "body must be { includeRetained: boolean } — required, no default: true = also stop the background processes this session was asked to keep running (explicit retain), false = keep them (they are reported as retained_skipped)");
53
+ return;
54
+ }
55
+ includeRetained = parsed.data.includeRetained;
56
+ }
57
+ const sessionId = ctx.helpers.safeDecode(match.params.sessionId);
58
+ if (sessionId === null) {
59
+ sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
60
+ return;
61
+ }
62
+ if (!sessionBackgroundFacesAvailable(deps)) {
63
+ sendError(res, 501, "capability.session_ownership_required", "the session background faces require the session-ownership face (sessionStorage.ownerOf)");
64
+ return;
65
+ }
66
+ const owner = await deps.sessionStorage.ownerOf(sessionId);
67
+ if (owner === undefined) {
68
+ sendError(res, 404, "not_found.session", "session not found");
69
+ return;
70
+ }
71
+ const isOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
72
+ if (!isOperator && !ctx.helpers.runOwnerOk(req, res, owner, "session"))
73
+ return;
74
+ const scope = owner ?? "default";
75
+ switch (id) {
76
+ case "session-background-list": {
77
+ const rows = defaultTaskRegistry.list({ owner: sessionId, sessionId, scope }, { limit: LIST_LIMIT }).map(projectRow);
78
+ sendJson(res, 200, { rows });
79
+ return;
80
+ }
81
+ case "session-background-stop": {
82
+ const { receipts } = await reapSessionBackgroundHard(sessionId, scope, { includeRetained, reason: includeRetained ? "session_background_stop_all" : "session_background_stop" }, deps.logger);
83
+ sendJson(res, 200, { receipts: receipts.map((r) => ({ id: r.taskId, outcome: r.outcome })) });
84
+ return;
85
+ }
86
+ default:
87
+ return assertNever(id);
88
+ }
89
+ }
90
+ export const BACKGROUND_ROUTES = [
91
+ { id: "session-background-list", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/background$/, label: "/v1/sessions/:id/background", methods: ["GET"] },
92
+ { id: "session-background-stop", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/background\/stop$/, label: "/v1/sessions/:id/background/stop", credentialGated: ["POST"], methods: ["POST"] },
93
+ ];
94
+ //# sourceMappingURL=background.js.map
@@ -25,6 +25,7 @@ import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
25
25
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
26
26
  import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS } from "../../task-settings.js";
27
27
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
28
+ import { sessionBackgroundFacesAvailable } from "./background.js";
28
29
  function routePairingOf(deps, name, m) {
29
30
  if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
30
31
  return "unknown";
@@ -92,6 +93,7 @@ export async function handleCapabilities(req, res, match, ctx) {
92
93
  sessionBackgroundable: Boolean(deps.runStore),
93
94
  runMemoryCaptureOptOut: Boolean(deps.runStore),
94
95
  mcpReconnect: Boolean(deps.runStore) && Boolean(deps.sessionStorage?.ownerOf),
96
+ background: { listFace: sessionBackgroundFacesAvailable(deps), exitFaces: sessionBackgroundFacesAvailable(deps) },
95
97
  sharedMemory: Boolean(deps.sharedMemoryStore && deps.orgMemoryDirectory),
96
98
  adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
97
99
  memoryBundle: Boolean(deps.memoryBundleExport && deps.memoryBundleImport) && deps.config.operatorPrincipals.length > 0,