@sema-agent/server 7.126.0 → 7.127.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 (77) hide show
  1. package/MIGRATION.md +34 -0
  2. package/USAGE.md +86 -8
  3. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +15 -5
  4. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +28 -1
  5. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  6. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  7. package/deploy/sema-up/chart/values.yaml +16 -6
  8. package/deploy/sema-up/kube-up.sh +10 -4
  9. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  10. package/deploy/sema-up/stack/sema-stack-cluster.yaml +20 -1
  11. package/dist/audit.js +1 -0
  12. package/dist/boot/execution-env.js +17 -4
  13. package/dist/boot/leader.d.ts +2 -1
  14. package/dist/boot/memory-consolidation.d.ts +37 -3
  15. package/dist/boot/memory-consolidation.js +18 -2
  16. package/dist/boot/runner-deps.d.ts +2 -1
  17. package/dist/boot/session-faces.d.ts +2 -1
  18. package/dist/boot/stage-06-runners.js +5 -2
  19. package/dist/config-catalog.js +3 -1
  20. package/dist/config-types.d.ts +5 -1
  21. package/dist/config.js +2 -0
  22. package/dist/execution-lane-caps.d.ts +12 -0
  23. package/dist/execution-lane-caps.js +16 -2
  24. package/dist/fleet/subagent-tail-bus.js +15 -5
  25. package/dist/http/admission.js +5 -1
  26. package/dist/http/durable-run-leg.js +1 -3
  27. package/dist/http/resume-legs.js +15 -6
  28. package/dist/http/route-ctx.d.ts +7 -1
  29. package/dist/http/routes/runs.js +23 -1
  30. package/dist/http/routes/tasks.js +17 -8
  31. package/dist/http/routes/workflows.js +8 -0
  32. package/dist/http/server.js +1 -3
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +1 -0
  35. package/dist/lsp/manager.js +1 -1
  36. package/dist/observability/fail-open.d.ts +16 -0
  37. package/dist/observability/fail-open.js +16 -0
  38. package/dist/observability/metrics.js +1 -1
  39. package/dist/observability/run-terminal.js +3 -0
  40. package/dist/plugins/caching-session-store.d.ts +39 -22
  41. package/dist/plugins/caching-session-store.js +16 -9
  42. package/dist/plugins/k8s-exec-protocol.js +3 -0
  43. package/dist/plugins/one-time-migrations.d.ts +28 -4
  44. package/dist/plugins/one-time-migrations.js +22 -4
  45. package/dist/plugins/remote-env-host.d.ts +11 -1
  46. package/dist/plugins/remote-env-host.js +9 -1
  47. package/dist/plugins/remote-env-k8s.d.ts +40 -5
  48. package/dist/plugins/remote-env-k8s.js +119 -25
  49. package/dist/plugins/session-store.d.ts +4 -3
  50. package/dist/plugins/session-store.js +1 -1
  51. package/dist/plugins/store-backend.d.ts +12 -5
  52. package/dist/plugins/store-backend.js +12 -3
  53. package/dist/plugins/store-contracts.d.ts +3 -0
  54. package/dist/plugins/store-open-wait.js +3 -0
  55. package/dist/request-key-closure.d.ts +20 -7
  56. package/dist/request-key-closure.js +22 -6
  57. package/dist/run-acceptance.d.ts +51 -0
  58. package/dist/run-acceptance.js +27 -0
  59. package/dist/runs.d.ts +7 -11
  60. package/dist/runs.js +5 -16
  61. package/dist/sandbox-egress-policy.d.ts +61 -0
  62. package/dist/sandbox-egress-policy.js +235 -0
  63. package/dist/session-error-wire.d.ts +2 -2
  64. package/dist/session-titler.d.ts +5 -3
  65. package/dist/trace/core-keyset-guard.d.ts +3 -2
  66. package/dist/trace/ledger-events.d.ts +9 -3
  67. package/dist/trace/ledger-sink.d.ts +7 -0
  68. package/dist/trace/ledger-sink.js +14 -4
  69. package/dist/trace/project.d.ts +49 -8
  70. package/dist/trace/project.js +39 -7
  71. package/dist/trace/self-minted-wire.d.ts +19 -29
  72. package/dist/trace/self-minted-wire.js +2 -9
  73. package/dist/trace/tool-output-media.d.ts +26 -0
  74. package/dist/trace/tool-output-media.js +57 -0
  75. package/dist/trace/wire-projection-faces.d.ts +7 -1
  76. package/dist/trace/wire-projection-faces.js +2 -1
  77. package/package.json +3 -3
@@ -12,7 +12,7 @@ import { FileRunStore } from "./file-run-store.js";
12
12
  import { StoreBootRefusalError } from "./store-contracts.js";
13
13
  import { buildStoreOpenRefusalTail } from "./store-open-refusal.js";
14
14
  import { acquireLocalBackendWithWait } from "./store-lock-wait.js";
15
- import { NO_SQL_MIGRATIONS, runSqlOneTimeMigrations } from "./one-time-migrations.js";
15
+ import { NO_SQL_MIGRATIONS, assertRetiredSchemaAbsentOnPool, runSqlOneTimeMigrations } from "./one-time-migrations.js";
16
16
  import { LocalSessionStore, createLocalSessionRepo } from "./local-session-store.js";
17
17
  import { LocalCheckpointStore } from "./local-checkpoint-store.js";
18
18
  import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
@@ -337,11 +337,20 @@ export function createStoreBackend(config, onWarn, onError) {
337
337
  if (!config.pg)
338
338
  return undefined;
339
339
  const pool = createPgPool(pgPoolOptions(config.pg, config.dbQueryTimeoutMs), onWarn);
340
- return new PgBackend(pool, config);
340
+ return withRetiredSchemaProbe(new PgBackend(pool, config), { dialect: "pg", pool });
341
341
  }
342
342
  if (!config.tidb)
343
343
  return undefined;
344
- return new TiDBBackend(createTidbPool(config), config);
344
+ const pool = createTidbPool(config);
345
+ return withRetiredSchemaProbe(new TiDBBackend(pool, config), { dialect: "tidb", pool });
346
+ }
347
+ function withRetiredSchemaProbe(inner, target) {
348
+ const guarded = Object.create(inner);
349
+ guarded.ensureSchema = async () => {
350
+ await assertRetiredSchemaAbsentOnPool(target);
351
+ await inner.ensureSchema();
352
+ };
353
+ return guarded;
345
354
  }
346
355
  export async function openStoreBackendWithFallback(config, logger) {
347
356
  const sqlEngine = config.dbBackend === "mysql" || config.dbBackend === "pg" ? config.dbBackend : undefined;
@@ -36,6 +36,9 @@ import type { PeerSessionTempo, TaskResult, TaskStatus } from "@sema-agent/core"
36
36
  * 收法:**一个基类,一条重抛**。新 File 店的拒启只要 `extends StoreBootRefusalError` 就自动落在正确的
37
37
  * 一侧;忘了继承是**可见**的(它是一个必须显式选择的父类),而忘了往 if 链里加一行是不可见的。
38
38
  * (core 的 `AdoptionError` 不在本仓,改不了它的继承链 ⇒ 降级臂保留它那一条 `instanceof`,共两条。)
39
+ *
40
+ * 族的含义是「**确定的拒启表态**」,不限 File 店:SQL `ensureSchema()` 里库带着已退役 schema 的拒启(`RetiredSchemaPresentError`)也属此族。三处读它,读法一致 ——
41
+ * 降级臂不吞(穿过 `mayFallback`)、SQL 开库的等待窗不重试它(`store-open-wait.ts`:再等也等不来另一个答案)、「库打不开」的拒句不改写它(它自己的拒句才是真话)。
39
42
  */
40
43
  export declare class StoreBootRefusalError extends Error {
41
44
  }
@@ -1,4 +1,5 @@
1
1
  import { STORE_LOCK_POLL_MS } from "./store-lock-wait.js";
2
+ import { StoreBootRefusalError } from "./store-contracts.js";
2
3
  const REAL_CLOCK = {
3
4
  now: () => performance.now(),
4
5
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
@@ -46,6 +47,8 @@ export async function openWithinWait(open, waitMs, onWait, clock = REAL_CLOCK) {
46
47
  throw lastFailure.error;
47
48
  throw new StoreOpenWindowExpired(waitMs);
48
49
  case "failed": {
50
+ if (outcome.error instanceof StoreBootRefusalError)
51
+ throw outcome.error;
49
52
  if (lastFailure === undefined)
50
53
  onWait({ waitMs, error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) });
51
54
  lastFailure = { error: outcome.error };
@@ -37,13 +37,26 @@ export type ScenarioBodyKey = (typeof TASK_BODY_SCENARIO_KEYS)[number];
37
37
  export type NamedKeysOf<T> = {
38
38
  [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
39
39
  };
40
- /**
41
- * FRESH 提交面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
42
- * 全部受理 ⇒ `null`。
43
- *
44
- * 判据边界(与 settings 那只同源,有意窄):只判**键名**;体本身非对象 ⇒ 本门不判(留给既有的
45
- * 「missing 'objective' string」那道门,免得同一个体被两句话抢答)。
46
- */
40
+ /** FRESH 提交面(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)的顶层键裁决。 */
47
41
  export declare function taskBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
42
+ declare const RUN_STEER_BODY_KEYS: readonly ["mode", "priority", "text"];
43
+ declare const CONTENT_STEER_BODY_KEYS: readonly ["content"];
44
+ declare const INTERRUPT_STEER_BODY_KEYS: readonly ["text"];
45
+ /** `POST /v1/runs/:id/steer` 的体形(键 = 受理集,值形由路由逐键判)。 */
46
+ export type RunSteerBody = {
47
+ readonly [K in (typeof RUN_STEER_BODY_KEYS)[number]]?: unknown;
48
+ };
49
+ /** 子代 steer / resume 与 workflow 子代 steer 的体形。 */
50
+ export type ContentSteerBody = {
51
+ readonly [K in (typeof CONTENT_STEER_BODY_KEYS)[number]]?: unknown;
52
+ };
53
+ /** `POST /v1/runs/:id/interrupt` 带 text 形的体形。 */
54
+ export type InterruptSteerBody = {
55
+ readonly [K in (typeof INTERRUPT_STEER_BODY_KEYS)[number]]?: unknown;
56
+ };
57
+ /** 送话口的三种体形(闭集;词外是编译错)。 */
58
+ export type SteerBodyForm = "run" | "content" | "interrupt";
59
+ /** 送话口的顶层键裁决(四扇门同一只;`form` 选受理集)。 */
60
+ export declare function steerBodyKeyIssue(raw: unknown, form: SteerBodyForm): KeyClosureIssue | null;
48
61
  export {};
49
62
  //# sourceMappingURL=request-key-closure.d.ts.map
@@ -32,26 +32,42 @@ const TASK_BODY_DECLARED_KEYS = [
32
32
  ];
33
33
  const TASK_BODY_SCENARIO_KEYS = ["repo"];
34
34
  const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
35
- const UNSUPPORTED_BODY = new Set(["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "writeFace"]);
36
- export function taskBodyKeyIssue(raw) {
35
+ const UNSUPPORTED_BODY_KEYS = ["handsReadOnly", "readDenyPatterns", "readFace", "shellGate", "userAttachments", "writeFace"];
36
+ const UNSUPPORTED_BODY = new Set(UNSUPPORTED_BODY_KEYS);
37
+ const UNSUPPORTED_LABEL = "unsupported (an engine seat this service does not read from the request body — set by the deployment, or not offered in this version)";
38
+ function closedBodyKeyIssue(raw, accepted, door) {
37
39
  if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw))
38
40
  return null;
39
41
  const buckets = { unknown: [], unsupported: [] };
40
- scanKeyClosure(raw, "", ACCEPTED_BODY, UNSUPPORTED_BODY, buckets);
42
+ scanKeyClosure(raw, "", accepted, UNSUPPORTED_BODY, buckets);
41
43
  if (buckets.unknown.length === 0 && buckets.unsupported.length === 0)
42
44
  return null;
43
45
  buckets.unknown.sort();
44
46
  buckets.unsupported.sort();
45
47
  const parts = [
46
- ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, "unknown (not a task-request key — check the spelling)")] : []),
47
- ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, "unsupported (an engine seat this service derives, never reads from the body)")] : []),
48
+ ...(buckets.unknown.length > 0 ? [keyClosureClause(buckets.unknown, `unknown (not ${door} key — check the spelling)`)] : []),
49
+ ...(buckets.unsupported.length > 0 ? [keyClosureClause(buckets.unsupported, UNSUPPORTED_LABEL)] : []),
48
50
  ];
49
51
  return {
50
52
  message: `body: unrecognized top-level key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
51
53
  `${parts.join("; ")}. ` +
52
- `Accepted: ${[...ACCEPTED_BODY].sort().join(",")}`,
54
+ `Accepted: ${[...accepted].sort().join(",")}`,
53
55
  unknownKeys: buckets.unknown.slice(0, MAX_KEY_ECHO),
54
56
  unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
55
57
  };
56
58
  }
59
+ export function taskBodyKeyIssue(raw) {
60
+ return closedBodyKeyIssue(raw, ACCEPTED_BODY, "a task-request");
61
+ }
62
+ const RUN_STEER_BODY_KEYS = ["mode", "priority", "text"];
63
+ const CONTENT_STEER_BODY_KEYS = ["content"];
64
+ const INTERRUPT_STEER_BODY_KEYS = ["text"];
65
+ const STEER_DOORS = {
66
+ run: { accepted: new Set(RUN_STEER_BODY_KEYS), door: "a steer-request" },
67
+ content: { accepted: new Set(CONTENT_STEER_BODY_KEYS), door: "a steer-request" },
68
+ interrupt: { accepted: new Set(INTERRUPT_STEER_BODY_KEYS), door: "an interrupt-request" },
69
+ };
70
+ export function steerBodyKeyIssue(raw, form) {
71
+ return closedBodyKeyIssue(raw, STEER_DOORS[form].accepted, STEER_DOORS[form].door);
72
+ }
57
73
  //# sourceMappingURL=request-key-closure.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * **一次提交的「受理」读法** —— 标题器只在引擎**受理**这条 run 之后起(一条律,唯一属主 = 本文件)。
3
+ *
4
+ * ## 受理是哪一刻
5
+ * 引擎把这次提交的 objective 收进会话的那一帧:`human_input{ carrier: "objective", delivery: "applied" }`。它是第一次模型调用
6
+ * 之前的最后一帧;在它之前引擎能拒的一切 —— prepare 门(`config.tool_face_control` / `config.user_invoked_skill_unknown` /
7
+ * `config.memory_capture_unsupported` …)、第一回合前的内联图片准入(`config.image_invalid`)、部署的 UserPromptSubmit 拦截、
8
+ * 开跑前的预算 / 墙钟 —— 都还没让这次输入进会话。`wiring_manifest` 不是受理:它在 prepare 通过时就出了,图片准入与提示拦截
9
+ * 都在它之后(core `run-identity-wiring.js` 推 wiring_manifest,`run-leg.js` 才做图片准入、推 human_input、调模型)。
10
+ *
11
+ * 只回结果、不交流的腿(verify / cascade:core 在自己里面排空每条腿的流,宿主手里没有流)看不到那一帧。它们的受理证据是结果本身:
12
+ * 结果里跑过 turn(`stats.turns > 0`)⇒ objective 进过会话、模型答过。**token 不是证据**:被 UserPromptSubmit 拦下的 objective 不进会话,
13
+ * 而同一条 run 的收尾压缩可以在已有历史的会话上花 token(turns 0、tokens > 0)—— 按 token 判会把被拦下的输入送去起名。
14
+ * 两种证据说的是同一件事,本文件把它们收成一只闩:先到的那一个算数,只算一次。
15
+ *
16
+ * ## 受理之后 / 之前
17
+ * · 受理 ⇒ 标题器起一次(会话没有标题时它才真去生成;判据在标题器里,本文件不重复)。
18
+ * · 没受理就收场(零工作的拒、受理前取消 / 拦截)⇒ 标题器不起:被拒的输入不花一次模型调用,也不被送去起名。
19
+ * 这次提交在提交门铸下的会话行照旧留下(无标题、零消息),与此前同 —— 「提交没让引擎受理就收场」的会话去留是另一件事,不在本文件。
20
+ *
21
+ * ## 谁喂它
22
+ * 三条提交腿(同步 / 流式 / durable)各在两处喂同一只:持流的那一形把每一帧交给 {@link RunAcceptance.observe};终局落行之后调一次
23
+ * {@link RunAcceptance.settle}。腿上不再有「什么时候起标题」的判断,标题器的唯一调用点是本文件的闩。座由提交门在 `prepareSpec` 里
24
+ * 随 spec 一起铸(`PreparedTaskSubmission.acceptance`);不经提交门的内部车道铸一只不带标题器的座,也就不起标题。
25
+ */
26
+ export interface RunAcceptanceDeps {
27
+ /** 标题器(缺席 = 本部署不自动起名)。 */
28
+ sessionTitler?: {
29
+ maybeTitle(sessionId: string, objective: string, model?: string): void;
30
+ };
31
+ }
32
+ export interface RunAcceptanceInput {
33
+ sessionId?: string;
34
+ objective: string;
35
+ /** 本 turn 真实驱动的模型 id(标题器与 turn 同链路)。 */
36
+ model?: string;
37
+ }
38
+ export interface RunAcceptance {
39
+ /** 持流的腿逐帧调用(判哪一帧算受理只在这里)。不抛、不等。 */
40
+ observe(ev: {
41
+ type: string;
42
+ }): void;
43
+ /** 终局落行之后:还没在流上见过受理帧、而结果里跑过 turn(只回结果的腿)⇒ 受理。只读 `stats.turns`。不抛、不等。 */
44
+ settle(result: {
45
+ readonly stats: {
46
+ readonly turns: number;
47
+ };
48
+ }): void;
49
+ }
50
+ export declare function createRunAcceptance(deps: RunAcceptanceDeps, input: RunAcceptanceInput): RunAcceptance;
51
+ //# sourceMappingURL=run-acceptance.d.ts.map
@@ -0,0 +1,27 @@
1
+ function isObjectiveApplied(ev) {
2
+ if (ev.type !== "human_input")
3
+ return false;
4
+ const h = ev;
5
+ return h.carrier === "objective" && h.delivery === "applied";
6
+ }
7
+ export function createRunAcceptance(deps, input) {
8
+ let accepted = false;
9
+ const accept = () => {
10
+ if (accepted)
11
+ return;
12
+ accepted = true;
13
+ if (input.sessionId)
14
+ deps.sessionTitler?.maybeTitle(input.sessionId, input.objective, input.model);
15
+ };
16
+ return {
17
+ observe(ev) {
18
+ if (!accepted && isObjectiveApplied(ev))
19
+ accept();
20
+ },
21
+ settle(result) {
22
+ if (result.stats.turns > 0)
23
+ accept();
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=run-acceptance.js.map
package/dist/runs.d.ts CHANGED
@@ -9,6 +9,7 @@ import type { QuestionCoordinator } from "./question.js";
9
9
  import { type ToolApprovalCoordinator } from "./tool-approval.js";
10
10
  import { type FleetRunPublisher } from "./fleet/fleet-bus.js";
11
11
  import { type WorkflowCompletionInbox } from "./orchestration/workflow-completion-inbox.js";
12
+ import type { RunAcceptance } from "./run-acceptance.js";
12
13
  import type { VerifyRoundsSpec } from "./http/verify-rounds.js";
13
14
  import type { UserInputLedger } from "./user-input-ledger.js";
14
15
  /**
@@ -91,14 +92,6 @@ export declare function taskHandleStop(registry: typeof defaultTaskRegistry, han
91
92
  * 语义上这两种时序是同一件事:本请求赢下了这条 session,而这条 session 的连接是被人断掉的。
92
93
  */
93
94
  export declare function markChildrenStoppedByUserOnAbort(signal: AbortSignal, taskId: string, principal: string | null | undefined): void;
94
- /**
95
- * A session CAS conflict is swallowed by core into a failed `TaskResult` (it is not thrown from
96
- * `runTaskStream`), so detect it from the result. Prefer the structured `code` (core ≥1.8); fall
97
- * back to the conflict wording only for older results without one.
98
- */
99
- export declare function isSessionConflictResult(result: {
100
- terminal: TerminalCause;
101
- }): boolean;
102
95
  export declare function resumeAtHttpStatus(result: {
103
96
  terminal: TerminalCause;
104
97
  }): number | undefined;
@@ -202,8 +195,6 @@ export declare function parkToolCallId(result: unknown, cs: ParkCheckpointReader
202
195
  * 不塞进 `checkpointGate`——那是 core 的类型,server 不改其形)。非 park / 无 token / tool-less /
203
196
  * 读失败 ⇒ **同一引用**原样返回(键缺席)。graft 不负责 strip——组合序恒为 graft → strip。 */
204
197
  export declare function graftParkToolCallId<T>(result: T, cs: ParkCheckpointReader | undefined): Promise<T>;
205
- /** Evict a stale warm-cache entry after a cross-instance write conflict (broken-affinity backstop). */
206
- export declare function evictIfConflict(runner: Runner, sessionId: string | undefined, result: TaskResult): void;
207
198
  /**
208
199
  * Drive a task to completion in the background, persisting its events to the durable run log (S2).
209
200
  *
@@ -328,7 +319,12 @@ trustedInternals?: Pick<RunInternals, "workflowParkedResume">,
328
319
  * 已收敛、未告知的崩溃丢失铸成 `task.user_steer_undrained` / `task.user_followup_undrained`(`cause:"crashed"`,码按受理时的车道选);腿收场时把本腿上的受理 settle 掉。
329
320
  * 缺席 ⇒ 两手都不做(与本参数存在之前逐字相同)。
330
321
  */
331
- userInputLedger?: UserInputLedger): Promise<void>;
322
+ userInputLedger?: UserInputLedger,
323
+ /**
324
+ * 这次提交的受理座(`src/run-acceptance.ts`):标题器只在引擎受理之后起。普通流形逐帧喂;只回结果的 verify / cascade 形(以及终局写口的
325
+ * 重试腿)终局落行之后收尾一次。缺席 ⇒ 不起标题(起标题此前由路由层在建行时做)。
326
+ */
327
+ acceptance?: RunAcceptance): Promise<void>;
332
328
  /** How often a running instance refreshes its run's updated_at (liveness, independent of events).
333
329
  * Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
334
330
  * reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
package/dist/runs.js CHANGED
@@ -64,13 +64,6 @@ export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
64
64
  }
65
65
  signal.addEventListener("abort", mark, { once: true });
66
66
  }
67
- export function isSessionConflictResult(result) {
68
- if (result.terminal.kind !== "failed")
69
- return false;
70
- if (result.terminal.code)
71
- return result.terminal.code === "conflict";
72
- return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
73
- }
74
67
  const PREPARE_REFUSAL_HTTP_STATUS = {
75
68
  "resume.session_not_found": 404,
76
69
  "rewind_snapshot.unresolvable": 422,
@@ -170,12 +163,7 @@ export async function graftParkToolCallId(result, cs) {
170
163
  const id = await parkToolCallId(result, cs);
171
164
  return id === undefined ? result : { ...result, toolCallId: id };
172
165
  }
173
- export function evictIfConflict(runner, sessionId, result) {
174
- if (sessionId && isSessionConflictResult(result)) {
175
- runner.sessions.evict?.(sessionId);
176
- }
177
- }
178
- export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals, userInputLedger) {
166
+ export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals, userInputLedger, acceptance) {
179
167
  const startedAt = Date.now();
180
168
  metrics?.addGauge("runs_active", 1);
181
169
  fleetPublisher?.onStart();
@@ -338,7 +326,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
338
326
  if (verify !== undefined) {
339
327
  const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify, { ...fwdInternals, ...statusSeat });
340
328
  await flush();
341
- evictIfConflict(runner, spec.sessionId, result);
342
329
  await flushPromptManifest();
343
330
  const safe = await withModelUsage(sealResultForExit(result));
344
331
  await append("done", { result: safe });
@@ -346,13 +333,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
346
333
  const plane = outwardPlane(safe.terminal);
347
334
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
348
335
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, safe);
336
+ acceptance?.settle(safe);
349
337
  recordDone(safe, plane.status);
350
338
  return;
351
339
  }
352
340
  if (cascade !== undefined) {
353
341
  const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade, { ...fwdInternals, ...statusSeat });
354
342
  await flush();
355
- evictIfConflict(runner, spec.sessionId, result);
356
343
  await flushPromptManifest();
357
344
  const safe = await withModelUsage(sealResultForExit(result));
358
345
  await append("done", { result: safe });
@@ -360,6 +347,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
360
347
  const plane = outwardPlane(safe.terminal);
361
348
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
362
349
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, safe);
350
+ acceptance?.settle(safe);
363
351
  recordDone(safe, plane.status);
364
352
  return;
365
353
  }
@@ -373,6 +361,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
373
361
  }
374
362
  fleetPublisher?.onEvent(ev);
375
363
  await userInputLedger?.observeLegEvent(stream, ev);
364
+ acceptance?.observe(ev);
376
365
  await flushPromptManifest();
377
366
  switch (ev.type) {
378
367
  case "text_delta":
@@ -406,7 +395,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
406
395
  await parkNeedsReview(ev.result);
407
396
  continue;
408
397
  }
409
- evictIfConflict(runner, spec.sessionId, ev.result);
410
398
  {
411
399
  const settled = causeAtDone !== undefined ? cancelledResult(causeAtDone, ev.result) : ev.result;
412
400
  settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
@@ -491,6 +479,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
491
479
  settledAsCancel = failureCodeOf(reached.result.terminal) === CANCELLED_CODE;
492
480
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
493
481
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, reached.result);
482
+ acceptance?.settle(reached.result);
494
483
  }
495
484
  else if (reached?.kind === "suspended") {
496
485
  await runStore.setSuspended(taskId, reached.result);
@@ -0,0 +1,61 @@
1
+ /** `K8S_SANDBOX_IPV6` 闭集:`enabled` = pod 保留平台给的 IPv6(pod spec 与未引入本旋钮时逐字同形);`disabled` = pod 级 sysctl 关掉 IPv6。 */
2
+ export declare const SANDBOX_IPV6_WORDS: readonly ["enabled", "disabled"];
3
+ export type SandboxIpv6 = (typeof SANDBOX_IPV6_WORDS)[number];
4
+ /** 旋钮缺席时的姿态(缺省不动 pod:关 v6 要每个节点的 kubelet 放行这两只 sysctl,没放行的集群 pod 起不来)。 */
5
+ export declare const SANDBOX_IPV6_DEFAULT: SandboxIpv6;
6
+ /** 一只 pod 级 sysctl(k8s `PodSecurityContext.sysctls[]` 的形)。 */
7
+ export interface PodSysctl {
8
+ readonly name: string;
9
+ readonly value: string;
10
+ }
11
+ /**
12
+ * 沙箱 pod 的 sysctl 只有这一个属主:`disabled` ⇒ `all`(已有接口)与 `default`(之后建的接口)都关 IPv6;`enabled` ⇒ 不铸。
13
+ * 两只都不在 k8s 的 safe sysctl 表里 ⇒ 每个节点 kubelet 要 `--allowed-unsafe-sysctls` 点名它们,否则 pod 起不来(车道连沙箱失败,响亮)。
14
+ */
15
+ export declare function sandboxPodSysctls(ipv6: SandboxIpv6): readonly PodSysctl[] | undefined;
16
+ /** 自检读的两张表(车道侧各一只 GET)。 */
17
+ export type EgressPolicyRead = "nodes" | "networkpolicies";
18
+ /** 节点地址里自检判的两类(`Hostname` / `InternalDNS` / `ExternalDNS` 不是地址)。 */
19
+ type NodeAddressType = "InternalIP" | "ExternalIP";
20
+ export type SandboxEgressGap = {
21
+ readonly kind: "node_address_open";
22
+ readonly node: string;
23
+ readonly addressType: NodeAddressType;
24
+ readonly address: string;
25
+ readonly policy: string;
26
+ readonly ports: string;
27
+ } | {
28
+ readonly kind: "policy_absent";
29
+ } | {
30
+ readonly kind: "ipv6_enabled";
31
+ } | {
32
+ readonly kind: "unreadable";
33
+ readonly what: EgressPolicyRead;
34
+ readonly detail: string;
35
+ };
36
+ /** 一只 GET 的读数:答了 2xx ⇒ 体;否则一句为什么读不到(HTTP 码 / 错误句)。 */
37
+ export type ApiRead = {
38
+ readonly ok: true;
39
+ readonly json: unknown;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly detail: string;
43
+ };
44
+ export interface EgressPolicyInputs {
45
+ readonly nodes: ApiRead;
46
+ readonly policies: ApiRead;
47
+ /** 沙箱 pod 的标签(车道建 pod 时铸的那一份)。 */
48
+ readonly podLabels: Readonly<Record<string, string>>;
49
+ readonly ipv6: SandboxIpv6;
50
+ }
51
+ /**
52
+ * 读数 → 缺口(纯函数、全函数:不抛;顺序:读不到的表 → 策略缺席 → 逐节点逐地址 → IPv6)。一张表读不到,只跳过要它的那一判;另一张照判。
53
+ */
54
+ export declare function sandboxEgressGaps(inputs: EgressPolicyInputs): SandboxEgressGap[];
55
+ /** 读数的说法:每个缺口一行 warn `sandbox_egress_policy_gap` + 一次 `recordFailOpen`(P-DEBT);零缺口一行 info `sandbox_egress_policy_checked`。 */
56
+ export declare function sayEgressPolicyReading(gaps: readonly SandboxEgressGap[], logger: {
57
+ info(msg: string, fields?: Record<string, unknown>): void;
58
+ warn(msg: string, fields?: Record<string, unknown>): void;
59
+ } | undefined, namespace: string): void;
60
+ export {};
61
+ //# sourceMappingURL=sandbox-egress-policy.d.ts.map
@@ -0,0 +1,235 @@
1
+ import { BlockList, isIP } from "node:net";
2
+ import { recordFailOpen } from "./observability/fail-open.js";
3
+ export const SANDBOX_IPV6_WORDS = ["enabled", "disabled"];
4
+ export const SANDBOX_IPV6_DEFAULT = "enabled";
5
+ export function sandboxPodSysctls(ipv6) {
6
+ switch (ipv6) {
7
+ case "enabled":
8
+ return undefined;
9
+ case "disabled":
10
+ return [
11
+ { name: "net.ipv6.conf.all.disable_ipv6", value: "1" },
12
+ { name: "net.ipv6.conf.default.disable_ipv6", value: "1" },
13
+ ];
14
+ }
15
+ }
16
+ class Unjudgeable extends Error {
17
+ }
18
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19
+ function recordAt(v, where) {
20
+ if (!isRecord(v))
21
+ throw new Unjudgeable(`${where} is not an object`);
22
+ return v;
23
+ }
24
+ function optRecordAt(v, where) {
25
+ return v === undefined || v === null ? undefined : recordAt(v, where);
26
+ }
27
+ function arrayAt(v, where) {
28
+ if (!Array.isArray(v))
29
+ throw new Unjudgeable(`${where} is not a list`);
30
+ return v;
31
+ }
32
+ function optArrayAt(v, where) {
33
+ return v === undefined || v === null ? undefined : arrayAt(v, where);
34
+ }
35
+ function stringAt(v, where) {
36
+ if (typeof v !== "string")
37
+ throw new Unjudgeable(`${where} is not a string`);
38
+ return v;
39
+ }
40
+ function listItems(json, what) {
41
+ const items = isRecord(json) ? json.items : undefined;
42
+ if (!Array.isArray(items))
43
+ throw new Unjudgeable(`${what} answer carries no items list`);
44
+ return items;
45
+ }
46
+ const SELECTOR_OPERATORS = ["In", "NotIn", "Exists", "DoesNotExist"];
47
+ function readSelector(v, where) {
48
+ const sel = optRecordAt(v, where) ?? {};
49
+ const labels = optRecordAt(sel.matchLabels, `${where}.matchLabels`) ?? {};
50
+ const matchLabels = {};
51
+ for (const [k, val] of Object.entries(labels))
52
+ matchLabels[k] = stringAt(val, `${where}.matchLabels.${k}`);
53
+ const matchExpressions = (optArrayAt(sel.matchExpressions, `${where}.matchExpressions`) ?? []).map((e, i) => {
54
+ const x = recordAt(e, `${where}.matchExpressions[${i}]`);
55
+ const operator = stringAt(x.operator, `${where}.matchExpressions[${i}].operator`);
56
+ if (!SELECTOR_OPERATORS.includes(operator))
57
+ throw new Unjudgeable(`${where} operator ${JSON.stringify(operator)} is not one of ${SELECTOR_OPERATORS.join(" / ")}`);
58
+ return { key: stringAt(x.key, `${where}.matchExpressions[${i}].key`), operator, values: (optArrayAt(x.values, `${where}.matchExpressions[${i}].values`) ?? []).map((y, j) => stringAt(y, `${where}.matchExpressions[${i}].values[${j}]`)) };
59
+ });
60
+ return { matchLabels, matchExpressions };
61
+ }
62
+ function selects(sel, labels) {
63
+ for (const [k, v] of Object.entries(sel.matchLabels))
64
+ if (labels[k] !== v)
65
+ return false;
66
+ for (const e of sel.matchExpressions) {
67
+ const has = Object.prototype.hasOwnProperty.call(labels, e.key);
68
+ switch (e.operator) {
69
+ case "In":
70
+ if (!has || !e.values.includes(labels[e.key]))
71
+ return false;
72
+ break;
73
+ case "NotIn":
74
+ if (has && e.values.includes(labels[e.key]))
75
+ return false;
76
+ break;
77
+ case "Exists":
78
+ if (!has)
79
+ return false;
80
+ break;
81
+ case "DoesNotExist":
82
+ if (has)
83
+ return false;
84
+ break;
85
+ default:
86
+ throw new Unjudgeable(`podSelector operator ${JSON.stringify(e.operator)} is not one of ${SELECTOR_OPERATORS.join(" / ")}`);
87
+ }
88
+ }
89
+ return true;
90
+ }
91
+ function readCidr(v, where) {
92
+ const cidr = stringAt(v, where);
93
+ parseCidr(cidr);
94
+ return cidr;
95
+ }
96
+ function readRule(v, where) {
97
+ const r = recordAt(v, where);
98
+ const to = optArrayAt(r.to, `${where}.to`);
99
+ const blocks = [];
100
+ for (const [i, peer] of (to ?? []).entries()) {
101
+ const ip = optRecordAt(recordAt(peer, `${where}.to[${i}]`).ipBlock, `${where}.to[${i}].ipBlock`);
102
+ if (ip === undefined)
103
+ continue;
104
+ blocks.push({ cidr: readCidr(ip.cidr, `${where}.to[${i}].ipBlock.cidr`), except: (optArrayAt(ip.except, `${where}.to[${i}].ipBlock.except`) ?? []).map((x, j) => readCidr(x, `${where}.to[${i}].ipBlock.except[${j}]`)) });
105
+ }
106
+ const ports = optArrayAt(r.ports, `${where}.ports`);
107
+ const portText = ports === undefined || ports.length === 0
108
+ ? "all"
109
+ : ports
110
+ .map((p, i) => {
111
+ const x = recordAt(p, `${where}.ports[${i}]`);
112
+ return `${typeof x.protocol === "string" ? x.protocol : "TCP"}/${typeof x.port === "number" || typeof x.port === "string" ? x.port : "*"}${typeof x.endPort === "number" ? `-${x.endPort}` : ""}`;
113
+ })
114
+ .join(",");
115
+ return { to: to === undefined || to.length === 0 ? undefined : blocks, ports: portText };
116
+ }
117
+ function isolatesEgress(spec, where) {
118
+ const types = optArrayAt(spec.policyTypes, `${where}.policyTypes`);
119
+ return types !== undefined ? types.map((t, i) => stringAt(t, `${where}.policyTypes[${i}]`)).includes("Egress") : spec.egress !== undefined && spec.egress !== null;
120
+ }
121
+ function egressPolicies(json, podLabels) {
122
+ const out = [];
123
+ for (const [i, item] of listItems(json, "networkpolicies").entries()) {
124
+ const p = recordAt(item, `networkpolicies[${i}]`);
125
+ const name = stringAt(recordAt(p.metadata, `networkpolicies[${i}].metadata`).name, `networkpolicies[${i}].metadata.name`);
126
+ const spec = recordAt(p.spec, `networkpolicy ${name} spec`);
127
+ if (!isolatesEgress(spec, `networkpolicy ${name}`) || !selects(readSelector(spec.podSelector, `networkpolicy ${name} podSelector`), podLabels))
128
+ continue;
129
+ out.push({ name, rules: (optArrayAt(spec.egress, `networkpolicy ${name} egress`) ?? []).map((r, j) => readRule(r, `networkpolicy ${name} egress[${j}]`)) });
130
+ }
131
+ return out;
132
+ }
133
+ function nodeAddresses(json) {
134
+ const out = [];
135
+ for (const [i, item] of listItems(json, "nodes").entries()) {
136
+ const n = recordAt(item, `nodes[${i}]`);
137
+ const node = stringAt(recordAt(n.metadata, `nodes[${i}].metadata`).name, `nodes[${i}].metadata.name`);
138
+ const addresses = arrayAt(recordAt(n.status, `node ${node} status`).addresses, `node ${node} status.addresses`);
139
+ let seen = 0;
140
+ for (const [j, a] of addresses.entries()) {
141
+ const x = recordAt(a, `node ${node} status.addresses[${j}]`);
142
+ const type = stringAt(x.type, `node ${node} status.addresses[${j}].type`);
143
+ if (type !== "InternalIP" && type !== "ExternalIP")
144
+ continue;
145
+ const address = stringAt(x.address, `node ${node} status.addresses[${j}].address`);
146
+ if (isIP(address) === 0)
147
+ throw new Unjudgeable(`node ${node} ${type} ${JSON.stringify(address)} is not an IP address`);
148
+ out.push({ node, addressType: type, address });
149
+ seen++;
150
+ }
151
+ if (seen === 0)
152
+ throw new Unjudgeable(`node ${node} reports no InternalIP / ExternalIP`);
153
+ }
154
+ return out;
155
+ }
156
+ function parseCidr(cidr) {
157
+ const slash = cidr.indexOf("/");
158
+ const net = slash < 0 ? cidr : cidr.slice(0, slash);
159
+ const fam = isIP(net);
160
+ const max = fam === 6 ? 128 : 32;
161
+ const prefix = slash < 0 ? max : Number(cidr.slice(slash + 1));
162
+ if (fam === 0 || !/^\d+$/.test(slash < 0 ? String(max) : cidr.slice(slash + 1)) || prefix > max)
163
+ throw new Unjudgeable(`ipBlock cidr ${JSON.stringify(cidr)} does not parse`);
164
+ return { net, prefix, family: fam === 6 ? "ipv6" : "ipv4" };
165
+ }
166
+ function inCidr(address, cidr) {
167
+ const c = parseCidr(cidr);
168
+ if ((isIP(address) === 6 ? "ipv6" : "ipv4") !== c.family)
169
+ return false;
170
+ const list = new BlockList();
171
+ list.addSubnet(c.net, c.prefix, c.family);
172
+ return list.check(address, c.family);
173
+ }
174
+ function ruleAllows(rule, address) {
175
+ if (rule.to === undefined)
176
+ return true;
177
+ return rule.to.some((b) => inCidr(address, b.cidr) && !b.except.some((x) => inCidr(address, x)));
178
+ }
179
+ function readTable(read, what, parse, gaps) {
180
+ if (!read.ok) {
181
+ gaps.push({ kind: "unreadable", what, detail: read.detail });
182
+ return undefined;
183
+ }
184
+ try {
185
+ return parse(read.json);
186
+ }
187
+ catch (e) {
188
+ gaps.push({ kind: "unreadable", what, detail: e instanceof Unjudgeable ? e.message : `answer not understood: ${e instanceof Error ? e.message : String(e)}` });
189
+ return undefined;
190
+ }
191
+ }
192
+ export function sandboxEgressGaps(inputs) {
193
+ const gaps = [];
194
+ const policies = readTable(inputs.policies, "networkpolicies", (json) => egressPolicies(json, inputs.podLabels), gaps);
195
+ const addresses = readTable(inputs.nodes, "nodes", nodeAddresses, gaps);
196
+ if (policies !== undefined && policies.length === 0)
197
+ gaps.push({ kind: "policy_absent" });
198
+ if (policies !== undefined && policies.length > 0 && addresses !== undefined) {
199
+ for (const a of addresses) {
200
+ for (const p of policies) {
201
+ const rule = p.rules.find((r) => ruleAllows(r, a.address));
202
+ if (rule !== undefined) {
203
+ gaps.push({ kind: "node_address_open", node: a.node, addressType: a.addressType, address: a.address, policy: p.name, ports: rule.ports });
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ if (inputs.ipv6 === "enabled")
210
+ gaps.push({ kind: "ipv6_enabled" });
211
+ return gaps;
212
+ }
213
+ function effectOf(gap) {
214
+ switch (gap.kind) {
215
+ case "node_address_open":
216
+ return "sandbox pods can reach this node address through the egress policy — add it to sandbox.networkPolicy.nodeCidrs (kube-up lists every node's addresses) or narrow the allowing rule";
217
+ case "policy_absent":
218
+ return "no NetworkPolicy selects the sandbox pods for egress — the policy layer does not restrict sandbox egress (chart sandbox.networkPolicy.enabled, or the CNI's own policy)";
219
+ case "ipv6_enabled":
220
+ return "sandbox pods keep IPv6 — an enforcement layer that filters only IPv4 leaves the pods' IPv6 (link-local included) unfiltered; K8S_SANDBOX_IPV6=disabled turns it off in the pod (needs the two sysctls allowed by every node's kubelet)";
221
+ case "unreadable":
222
+ return `the ${gap.what} could not be read — this part of the egress check did not run (the server's k8s account needs ${gap.what === "nodes" ? "get/list on nodes (cluster scope)" : "get/list on networkpolicies in the sandbox namespace"})`;
223
+ }
224
+ }
225
+ export function sayEgressPolicyReading(gaps, logger, namespace) {
226
+ if (gaps.length === 0) {
227
+ logger?.info("sandbox_egress_policy_checked", { namespace, gaps: 0 });
228
+ return;
229
+ }
230
+ for (const gap of gaps) {
231
+ recordFailOpen("server.sandbox-egress.policy-gap", `kind=${gap.kind}${gap.kind === "unreadable" ? ` what=${gap.what}` : ""}`);
232
+ logger?.warn("sandbox_egress_policy_gap", { namespace, ...gap, effect: effectOf(gap) });
233
+ }
234
+ }
235
+ //# sourceMappingURL=sandbox-egress-policy.js.map