@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +6 -6
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -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 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
@@ -78,6 +78,11 @@ export declare function dockerBuildTickFrame(lineOrd: number, elapsedSec: number
78
78
  export declare function synthDoneFrame(lineOrd: number, kind: "cancelled" | "timeout", detail?: {
79
79
  error?: string;
80
80
  }): IngestFrame;
81
+ /**
82
+ * 失败尾巴的行数 —— **唯一**常量:缓冲({@link createStderrTail})保留这么多行,终局句({@link exitDoneFrame})取这么多行。
83
+ * 两处读同一个数,所以缓冲里「保留的」恒等于消费方「要的」:既没有消费方想要而已被丢掉的行,也没有永远用不上的行占内存。
84
+ */
85
+ export declare const STDERR_TAIL_LINES = 12;
81
86
  /**
82
87
  * The runner-side terminal `done` for a build.sh CHILD that exited non-zero WITHOUT printing its own `done` (build.sh
83
88
  * normally dies via `die()` which DOES print a `done:FAILED`, but a SIGKILL or a crash before the trap can leave the
@@ -87,6 +92,19 @@ export declare function synthDoneFrame(lineOrd: number, kind: "cancelled" | "tim
87
92
  export declare function exitDoneFrame(lineOrd: number, exitCode: number, stderrTail: string): IngestFrame;
88
93
  /** Keep only the last `n` lines of a (possibly long) stderr tail — bounds the error string we forward. */
89
94
  export declare function lastLines(text: string, n: number): string;
95
+ /** 有界的 stderr 尾部缓冲:只保留**末** {@link STDERR_TAIL_LINES} 行(环形:新行进来、最老的出去)。 */
96
+ export interface StderrTail {
97
+ /** 收一行(spawner 已按换行切好;这里仍按 {@link contentLines} 判据过一遍,空行不入)。 */
98
+ push(raw: string): void;
99
+ /** 当前保留的行,`\n` 连接;空缓冲 = 空串。可直接交 {@link exitDoneFrame}。 */
100
+ text(): string;
101
+ }
102
+ /**
103
+ * 修前缓冲是 `if (stderrTail.length < 80) push` —— 留的是**首** 80 行,而消费方({@link exitDoneFrame})要的是**末** 12 行:
104
+ * 超过 80 行的失败里,「尾巴」是前 80 行的末 12 行,真正的最后一行(失败原因所在)一个字也进不来。
105
+ * 现在留末 N 行;N = {@link STDERR_TAIL_LINES}。
106
+ */
107
+ export declare function createStderrTail(): StderrTail;
90
108
  /**
91
109
  * The profile-aware HARD DEADLINE (§P2.7/§P2.11) the runner enforces by killing the process group. flutter/full are
92
110
  * the heavy profiles (~60min); everything else ~45min. A bands build ("custom") is treated as heavy if it includes
@@ -33,6 +33,7 @@ export function synthDoneFrame(lineOrd, kind, detail) {
33
33
  }
34
34
  return { event: "done", lineOrd, status: "FAILED", exitCode: 124, error: detail?.error ?? "bake hard-deadline exceeded (killed)" };
35
35
  }
36
+ export const STDERR_TAIL_LINES = 12;
36
37
  export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
37
38
  const tail = stderrTail.trim();
38
39
  return {
@@ -40,13 +41,32 @@ export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
40
41
  lineOrd,
41
42
  status: "FAILED",
42
43
  exitCode,
43
- error: tail.length > 0 ? `build.sh exited ${exitCode}: ${lastLines(tail, 12)}` : `build.sh exited ${exitCode}`,
44
+ error: tail.length > 0 ? `build.sh exited ${exitCode}: ${lastLines(tail, STDERR_TAIL_LINES)}` : `build.sh exited ${exitCode}`,
44
45
  };
45
46
  }
47
+ function contentLines(text) {
48
+ return text
49
+ .split(/\r?\n/)
50
+ .map((l) => l.replace(/\r$/, ""))
51
+ .filter((l) => l.length > 0);
52
+ }
46
53
  export function lastLines(text, n) {
47
- const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
54
+ const lines = contentLines(text);
48
55
  return lines.slice(Math.max(0, lines.length - n)).join("\n");
49
56
  }
57
+ export function createStderrTail() {
58
+ const lines = [];
59
+ return {
60
+ push(raw) {
61
+ for (const l of contentLines(raw)) {
62
+ lines.push(l);
63
+ if (lines.length > STDERR_TAIL_LINES)
64
+ lines.shift();
65
+ }
66
+ },
67
+ text: () => lines.join("\n"),
68
+ };
69
+ }
50
70
  export function deadlineMsForProfile(profile, bands) {
51
71
  const HEAVY_MS = 60 * 60 * 1000;
52
72
  const NORMAL_MS = 45 * 60 * 1000;
@@ -11,10 +11,12 @@ export interface ClaimedBake {
11
11
  profile?: string | null;
12
12
  bands?: string[] | null;
13
13
  }
14
- /** The ingest/heartbeat response — image-api echoes whether a cancel was requested so the runner kills the group. */
15
- export interface IngestAck {
14
+ /** The heartbeat response (a no-frame `…/ingest` POST) — image-api echoes whether a cancel was requested so the runner
15
+ * kills the group, and whether the lease is still ours. The heartbeat is the ONE place these two bits are read
16
+ * ({@link BakeRunner.runOne}'s supervisor); a frame ingest's ack has no reader, so `ingest` returns nothing. */
17
+ export interface HeartbeatAck {
16
18
  accepted: boolean;
17
- /** The durable cancel flag (§P2.11) — polled off EVERY ingest/heartbeat response (≤heartbeat-interval lag). */
19
+ /** The durable cancel flag (§P2.11) — read off each HEARTBEAT response (≤ heartbeat-interval lag). */
18
20
  cancelRequested?: boolean;
19
21
  /** False when the lease was lost/stolen (the runner aborts + kills the child — it no longer owns the build). */
20
22
  leaseValid?: boolean;
@@ -51,10 +53,11 @@ export interface ChildSpawner {
51
53
  export interface ImageApiClient {
52
54
  /** Long-poll for + claim the next queued bake; null when none is claimable right now. */
53
55
  claimNext(): Promise<ClaimedBake | null>;
54
- /** POST one frame to `…/ingest` (carries the per-bake ingest secret header); returns the ack (cancel/lease). */
55
- ingest(bakeId: string, ingestSecret: string, frame: Record<string, unknown>): Promise<IngestAck>;
56
- /** Heartbeat the lease forward (also surfaces cancel/lease state) — distinct from a frame ingest. */
57
- heartbeat(bakeId: string, ingestSecret: string): Promise<IngestAck>;
56
+ /** POST one frame to `…/ingest` (carries the per-bake ingest secret header). Resolves with nothing: the runner reads no
57
+ * ack off a frame ingest (the cancel / lease bits are read off {@link ImageApiClient.heartbeat} only). */
58
+ ingest(bakeId: string, ingestSecret: string, frame: Record<string, unknown>): Promise<void>;
59
+ /** Heartbeat the lease forward and surface the cancel / lease state — the only call whose response the runner reads. */
60
+ heartbeat(bakeId: string, ingestSecret: string): Promise<HeartbeatAck>;
58
61
  }
59
62
  /** Host-side ops the runner needs that are NOT image-api calls (fs + a thin docker-push wrapper for the big-blob
60
63
  * retry — NOT a build.sh --replay). All injected so the orchestration is testable off build-host. */
@@ -208,7 +211,8 @@ export declare class BakeRunner {
208
211
  * reaper + a mid-build fill guard still protect the host). */
209
212
  private freeGbOrNull;
210
213
  /** Ingest one frame, swallowing transient errors (the buffered-retry of unsent lines is the real-runner's job;
211
- * image-api's uq_line makes a re-send a no-op, so a dropped frame is recoverable, never a crash). */
214
+ * image-api's uq_line makes a re-send a no-op, so a dropped frame is recoverable, never a crash). Returns nothing:
215
+ * the cancel / lease bits are read off the heartbeat response only, never off a frame ingest's ack. */
212
216
  private safeIngest;
213
217
  }
214
218
  /** Allocate the next per-bake lineOrd (monotonic from 1) — the at-least-once ingest dedupe key (§P2.10). */
@@ -1,5 +1,5 @@
1
1
  import { resolve as resolvePath } from "node:path";
2
- import { lineToFrame, dockerBuildTickFrame, synthDoneFrame, exitDoneFrame, deadlineMsForProfile, parseDfFreeGb, parseBuildShLine, manifestPathFromLine, } from "./protocol.js";
2
+ import { lineToFrame, dockerBuildTickFrame, synthDoneFrame, exitDoneFrame, deadlineMsForProfile, parseDfFreeGb, parseBuildShLine, manifestPathFromLine, createStderrTail, } from "./protocol.js";
3
3
  const DEFAULT_HEARTBEAT_MS = 30_000;
4
4
  const DEFAULT_IDLE_POLL_MS = 5_000;
5
5
  const DEFAULT_MIN_FREE_GB = 20;
@@ -137,7 +137,7 @@ export class BakeRunner {
137
137
  await this.safeIngest(bake, exitDoneFrame(nextOrd(ord), 1, `recipe checkout failed: ${String(err)}`));
138
138
  return;
139
139
  }
140
- const stderrTail = [];
140
+ const stderrTail = createStderrTail();
141
141
  let lastManifestPath = null;
142
142
  let lastImageTag = null;
143
143
  let sawDone = false;
@@ -166,10 +166,7 @@ export class BakeRunner {
166
166
  }
167
167
  enqueueIngest(lineToFrame(raw, nextOrd(ord)));
168
168
  },
169
- onStderrLine: (raw) => {
170
- if (stderrTail.length < 80)
171
- stderrTail.push(raw);
172
- },
169
+ onStderrLine: (raw) => stderrTail.push(raw),
173
170
  });
174
171
  inFlight.child = child;
175
172
  const deadlineMs = deadlineMsForProfile(bake.profile ?? "custom", bake.bands ?? null);
@@ -230,7 +227,7 @@ export class BakeRunner {
230
227
  return;
231
228
  }
232
229
  const code = exit.code ?? (exit.signal ? 137 : 1);
233
- await this.safeIngest(bake, exitDoneFrame(nextOrd(ord), code, stderrTail.join("\n")));
230
+ await this.safeIngest(bake, exitDoneFrame(nextOrd(ord), code, stderrTail.text()));
234
231
  }
235
232
  async forwardDone(bake, ord, doneLine, lastManifestPath, lastImageTag) {
236
233
  let line = doneLine;
@@ -309,11 +306,10 @@ export class BakeRunner {
309
306
  }
310
307
  async safeIngest(bake, frame) {
311
308
  try {
312
- return await this.o.api.ingest(bake.bakeId, bake.ingestSecret, frame);
309
+ await this.o.api.ingest(bake.bakeId, bake.ingestSecret, frame);
313
310
  }
314
311
  catch (err) {
315
312
  this.o.log.warn("bake_ingest_error", { bakeId: bake.bakeId, lineOrd: frame.lineOrd, err: String(err) });
316
- return null;
317
313
  }
318
314
  }
319
315
  }
@@ -316,7 +316,7 @@ export async function createConfigCenterRuntime(ctx) {
316
316
  };
317
317
  let bootConfigPending;
318
318
  const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
319
- const lkgPathFor = (identity) => process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker, identity);
319
+ const lkgPathFor = (identity) => config.configLkgPath ?? defaultLkgPath(config, cc?.worker, identity);
320
320
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
321
321
  let lkgBooted = false;
322
322
  let bootLkgCandidate;
@@ -335,10 +335,10 @@ export async function createConfigCenterRuntime(ctx) {
335
335
  const lkgPath = lkgPathFor(identity);
336
336
  try {
337
337
  if (effectiveObj.skills && config.configCenter) {
338
- await applyCenterSkills([], effectiveObj.skills, config.configCenter, logger, undefined, defaultSkillCacheDir());
338
+ await applyCenterSkills([], effectiveObj.skills, config.configCenter, logger, undefined, defaultSkillCacheDir(config));
339
339
  }
340
340
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
341
- const cacheDir = defaultSkillCacheDir();
341
+ const cacheDir = defaultSkillCacheDir(config);
342
342
  for (const m of enabledSkills) {
343
343
  const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
344
344
  if (!SHA256_HEX_RE.test(hex))
@@ -375,7 +375,7 @@ export async function createConfigCenterRuntime(ctx) {
375
375
  return null;
376
376
  }
377
377
  const lkgPath = lkgPathFor(identity);
378
- const legacy = await migrateV1Lkg({ legacyPath: process.env.CONFIG_LKG_PATH ?? legacyV1LkgPath(cc?.worker), binPath: lkgPath, worker: cc?.worker, identity });
378
+ const legacy = await migrateV1Lkg({ legacyPath: config.configLkgPath ?? legacyV1LkgPath(config, cc?.worker), binPath: lkgPath, worker: cc?.worker, identity });
379
379
  switch (legacy.outcome) {
380
380
  case "none":
381
381
  break;
@@ -622,9 +622,9 @@ export async function createConfigCenterRuntime(ctx) {
622
622
  },
623
623
  async applyCenterCapabilities(skills) {
624
624
  if (effective?.skills && config.configCenter) {
625
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
625
+ skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir(config) : undefined);
626
626
  }
627
- if (effective?.plugins) {
627
+ if (effective?.plugins !== undefined) {
628
628
  const pluginOut = await applyCenterPlugins(skills, effective, {
629
629
  cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
630
630
  allowHosts: config.pluginsAllowHosts,
@@ -1,6 +1,7 @@
1
1
  import { ElicitationCoordinator } from "../elicitation.js";
2
2
  import { QuestionCoordinator } from "../question.js";
3
3
  import { ToolApprovalCoordinator, resolveStreamApprovalGate } from "../tool-approval.js";
4
+ import { deploymentListAsks } from "../approval.js";
4
5
  import { FileApprovalAskAuditStore } from "../approval-ask-audit-store.js";
5
6
  import { SendUserFileEmitter, SEND_USER_FILE_MAX_BYTES, sendUserFileTool } from "../capabilities/send-user-file-tool.js";
6
7
  import { createSandboxFileSend, TaskEnvRegistry } from "../capabilities/sandbox-file-send.js";
@@ -58,7 +59,9 @@ export function createLiveCoordinators(ctx) {
58
59
  unattendedPolicy: config.unattendedApprovalPolicy,
59
60
  ...(getParkedRedeem !== undefined ? { parkedRedeem: getParkedRedeem } : {}),
60
61
  ...(approvalAskAudit !== undefined ? { askAudit: approvalAskAudit } : {}),
61
- ...(approvalExemptionStore !== undefined ? { sessionGrants: approvalExemptionStore } : {}),
62
+ ...(approvalExemptionStore !== undefined
63
+ ? { sessionGrants: approvalExemptionStore, deploymentListAsks: (call) => deploymentListAsks(config, call) }
64
+ : {}),
62
65
  ...(parkCommits !== undefined ? { parkCommits } : {}),
63
66
  })
64
67
  : undefined;
@@ -59,9 +59,9 @@ export declare function createDeviceLane(ctx: DeviceLaneCtx): DeviceLane | undef
59
59
  * `boot/leader.ts` 的 `assertLeaderDurableStore`)。
60
60
  *
61
61
  * config 层的门①判的是「运维配没配 SQL 坐标」——那句话回答的是**意图**。它回答不了「这次启动**真的
62
- * 拿到**了店吗」:`openStoreBackendWithFallback` 在 `SESSION_BACKEND=auto` 下(🔴 S-866 注改真:`auto` **不是**缺省 ——
63
- * 缺省是 `memory`,`DB_BACKEND` 显式 mysql|pg 时缺省 `mysql`;`auto` 要显式写),SQL 连不上 /
64
- * 建表失败 ⇒ `backend=undefined` + 一条 warn 就继续启动。那次启动里 device lane 的四表整个不在,
62
+ * 拿到**了店吗」:`openStoreBackendWithFallback` 曾在 `SESSION_BACKEND=auto` 下,SQL 连不上 /
63
+ * 建表失败 ⇒ `backend=undefined` + 一条 warn 就继续启动(那条降级臂已退役:配了 DSN 的库窗内重试、到窗拒启,见 `plugins/store-open-wait.ts`;
64
+ * 仍走得到本门的形:`DB_BACKEND=mysql|pg` 声明了 SQL 而 `SESSION_BACKEND=memory`,刻意不持久 ⇒ 不开店)。那次启动里 device lane 的四表整个不在,
65
65
  * 而 `boot/execution-env.ts` 的第七臂拿不到三件 ⇒ `executionEnvFactory` 落 `undefined` ⇒ core 拿
66
66
  * `StubExecutionEnv`:**一台自称在员工设备上执行的 worker 实际在自己进程里跑,而且回执同形**。
67
67
  * 那正是 [ref]「执行车道禁静默降级」点名的样板病,也正是 A-1 那条临时门当初要拦的东西 —— 换执法点,
@@ -40,8 +40,7 @@ export function assertDeviceLaneWired(isDeviceLane, lane, hasFactory) {
40
40
  return;
41
41
  if (!lane) {
42
42
  throw new Error("REMOTE_EXEC=device but the device lane could not be wired at boot [device-lane-unwired]: the SQL backend is " +
43
- "configured yet this replica did not get one (an unreachable DB / failed DDL degrades to the in-memory fallback " +
44
- "when SESSION_BACKEND=auto), so the device registry / session↔device binding / audit tables are absent. " +
43
+ "configured yet this replica did not get one, so the device registry / session↔device binding / audit tables are absent. " +
45
44
  "Refusing to serve a lane that would silently fall back to the in-process stub env — a worker claiming to " +
46
45
  "execute on the employee's device while actually running inside its own process (#157). Fix the database " +
47
46
  "(or unset REMOTE_EXEC) and restart (device-executor-lane-v2 §4.7)");
@@ -205,9 +205,10 @@ export function createExecutionEnv(ctx) {
205
205
  }
206
206
  else if (config.remoteExec?.provider === "host") {
207
207
  {
208
+ const schedulerStorePath = config.schedulerStorePath ?? defaultSchedulerStorePath(config);
208
209
  const hostScheduler = facts.hostScheduler(config).available
209
210
  ? new FileSchedulerBackend({
210
- ...(config.schedulerStorePath ? { storePath: config.schedulerStorePath } : {}),
211
+ storePath: schedulerStorePath,
211
212
  ...(config.schedulerSessionWakeup === false || config.schedulerSessionLifetime === false
212
213
  ? {
213
214
  caps: {
@@ -219,7 +220,7 @@ export function createExecutionEnv(ctx) {
219
220
  })
220
221
  : undefined;
221
222
  if (hostScheduler)
222
- logger.info(`R7: self-wake scheduler enabled (host lane), store=${config.schedulerStorePath ?? defaultSchedulerStorePath()}`);
223
+ logger.info(`R7: self-wake scheduler enabled (host lane), store=${schedulerStorePath}`);
223
224
  const hostCfg = {
224
225
  ...homeDirSeat,
225
226
  ...(config.remoteExec.workspaceBase ? { workspaceBase: config.remoteExec.workspaceBase } : {}),
@@ -1,4 +1,10 @@
1
- /** `err` 的 fatal 日志文本:`String(err)` 过形状律一次。纯函数,零副作用。 */
1
+ /**
2
+ * `err` 的 fatal 日志文本:标题过形状律一次。纯函数,零副作用。
3
+ *
4
+ * 标题 = `String(err)`;异常**自带机器码**(`err.code` 是串 —— 引擎的构造期拒因都带,如 `config.agent_config_dir_invalid`)⇒ 按 Node 给自家
5
+ * 带码异常的写法 `<name> [<code>]: <message>` 把码写进标题(`String(err)` 不带 `code`,修前拒启行里看不到引擎的码)。一条规则,不分谁铸的:
6
+ * 多数 Node 自己的 `ERR_*` 异常本来就印成这一形(逐字不变),系统调用异常(`EEXIST` 等)标题里多一次码。
7
+ */
2
8
  export declare function buildFatalErrorText(err: unknown): string;
3
9
  /**
4
10
  * 进程级 fatal 出口的**唯一一只腿**:同一只 {@link buildFatalErrorText} 产物写两个面,然后按崩溃码退。
@@ -1,7 +1,8 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
2
  import { createLogger } from "../observability/logger.js";
3
3
  export function buildFatalErrorText(err) {
4
- return redactSecrets(String(err));
4
+ const code = err instanceof Error ? err.code : undefined;
5
+ return redactSecrets(typeof code === "string" && err instanceof Error ? `${err.name} [${code}]: ${err.message}` : String(err));
5
6
  }
6
7
  export function fatalExit(err) {
7
8
  const text = buildFatalErrorText(err);
@@ -64,10 +64,10 @@ export declare function leaderK8sMinioGap(leaderEnabled: boolean, leaderProvider
64
64
  * [ref] 车4 件2 的**能力面**门(codex 交叉复审 R1-high①,验真后修)。
65
65
  *
66
66
  * config 层的门②判的是 `DB_BACKEND` 的字面值 —— 那句话回答的是「运维要没要 durable」。它回答不了
67
- * 「这次启动**真的拿到**了 durable 吗」:`openStoreBackendWithFallback` 在 `SESSION_BACKEND=auto`
68
- * 下(🔴 S-866 注改真:`auto` **不是**缺省 —— 缺省是 `memory`,`DB_BACKEND` 显式 mysql|pg 时缺省 `mysql`;`auto` 要显式写),SQL 连不上 /
69
- * 建表失败 ⇒ 返回 `backend=undefined` + 一条 warn 就继续启动。那次启动里
70
- * `leaderRun()` 缺席,leader 端点会静默落进单副本内存臂:多副本各自受理同一个 Idempotency-Key、
67
+ * 「这次启动**真的拿到**了 durable 吗」:`openStoreBackendWithFallback` 曾在 `SESSION_BACKEND=auto`
68
+ * 下,SQL 连不上 / 建表失败 ⇒ 返回 `backend=undefined` + 一条 warn 就继续启动(那条降级臂已退役:配了 DSN 的库窗内重试、到窗拒启,
69
+ * 见 `plugins/store-open-wait.ts`)。仍走得到本门的形:`DB_BACKEND=mysql|pg` 声明了 SQL 而 `SESSION_BACKEND=memory`(声明 pg、刻意不持久 ⇒ 不开店)。
70
+ * 那次启动里 `leaderRun()` 缺席,leader 端点会静默落进单副本内存臂:多副本各自受理同一个 Idempotency-Key、
71
71
  * 跨副本 GET 404、重启失忆 —— 而这条腿的产物是**真 git push**。
72
72
  *
73
73
  * ⇒ 判据锚**真拿到了没有**。纯谓词、只吃两个原语(与 {@link leaderK8sMinioGap} 同姿势),便于单测。
@@ -15,8 +15,7 @@ export function assertLeaderDurableStore(leaderEnabled, hasLeaderRunStore) {
15
15
  if (!leaderEnabled || hasLeaderRunStore)
16
16
  return;
17
17
  throw new Error("LEADER_ENABLED=true but no durable leader-run store is available at boot — the SQL backend is configured " +
18
- "yet this replica did not get one (an unreachable DB / failed DDL degrades to the in-memory fallback when " +
19
- "SESSION_BACKEND=auto). Refusing to serve the leader endpoint from a single-replica in-memory registry: " +
18
+ "yet this replica did not get one. Refusing to serve the leader endpoint from a single-replica in-memory registry: " +
20
19
  "the `leader_run` table is what makes a retried submit idempotent and a cross-replica GET answerable, and " +
21
20
  "without it a retry starts a SECOND real git push. Fix the database (or unset LEADER_ENABLED) and restart " +
22
21
  "(#193 车4 件2 capability gate; the DB_BACKEND literal check in config.ts is the intent half of the same rule)");
@@ -3,6 +3,7 @@ import type { ServiceConfig } from "../config.js";
3
3
  import type { RunnerDepsOnAsk } from "./runner-deps.js";
4
4
  import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView, type LiveQuestionFace } from "../deployment-governance.js";
5
5
  import { type SandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
6
+ import type { ApprovalExemptionStore } from "../plugins/approval-exemption-store.js";
6
7
  /** 本腿读的配置切面(`ServiceConfig` 结构满足)。两个构造口各自的切面 + lane 判别式读的 `remoteExec`。 */
7
8
  export interface ParkedReviveGateConfigView extends DeploymentGovernanceConfigView, ApprovalBaselineConfigView {
8
9
  readonly remoteExec?: ServiceConfig["remoteExec"];
@@ -43,6 +44,8 @@ export interface ParkedReviveGateDeps {
43
44
  * headless auto-deny,比「再问人一次」严且哑。交了之后最坏也是 park,方向 fail-safe。
44
45
  */
45
46
  readonly approverSeat: RunnerDepsOnAsk | undefined;
47
+ /** 会话豁免店(部署名单那一问的「本会话放行」读面;`StoreBackend.approvalExemption()` 结构满足)。缺席 = 部署没有 store backend ⇒ 名单照问。 */
48
+ readonly approvalExemptionStore: Pick<ApprovalExemptionStore, "has"> | undefined;
46
49
  /**
47
50
  * [ref] —— core `RunnerDeps.runtimeCapsResolver` 的**同一只**(main.ts 的 `runtimeCapsResolver`,
48
51
  * 单一属主同 `boot/runtime-caps.ts`)。重建条目的第三位决议链元数据 `autoModeArmed` 的唯一判据是
@@ -64,6 +67,7 @@ export interface ParkedReviveGateDeps {
64
67
  * 这一座可改读它 —— 接线不在本次提货单内,见 7.106.0 收车档「未闭环」。 */
65
68
  readonly parentAutoModeIntent?: ((row: BackgroundAgentRecord) => boolean) | undefined;
66
69
  readonly logger: {
70
+ info(event: string, fields?: Record<string, unknown>): void;
67
71
  warn(event: string, fields?: Record<string, unknown>): void;
68
72
  };
69
73
  /** host 腿 cwd 占位的落地根(main.ts 的 `localRoot`;`config.localDataRoot` 优先)。 */
@@ -90,7 +94,7 @@ export type RebuiltParentConstraint = NonNullable<RebuiltInheritedGate["parentCo
90
94
  * 赎回腿的父约束链重建([ref] 件二)。
91
95
  *
92
96
  * 语义:**同/跨副本一致地重建「部署 ⊇ 操作员」两层完整链**——审批基线(durable question 门 + F4 高危写
93
- * 审批门)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
97
+ * 审批门 + 部署名单那一问的豁免读面)作 base 的 `toolPolicy` 座,经**折叠属主** `applyRuntimeGovernance`
94
98
  * (→ core `tightenTaskSpec`)叠上部署治理段(autonomy / commandPolicy / MANUAL_MODE_SHELL_GATE /
95
99
  * 守卫集),取其 `toolPolicy` 装进单层 `parentConstraints`。
96
100
  *
@@ -6,11 +6,12 @@ import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from
6
6
  import { applyRuntimeGovernance } from "../runtime-governance.js";
7
7
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
8
8
  import { executionLaneHomeDir } from "../execution-lane-caps.js";
9
+ import { createDeploymentListExemption } from "../approval.js";
9
10
  function hostGateCwd(config, localRoot) {
10
11
  return join(config.localDataRoot ?? localRoot, "parked-revive-unrooted");
11
12
  }
12
13
  export function createParkedReviveInheritedGate(deps) {
13
- const { config, question, logger, localRoot, approverSeat, memoryEngineRoot } = deps;
14
+ const { config, question, approvalExemptionStore, logger, localRoot, approverSeat, memoryEngineRoot } = deps;
14
15
  const slots = deps.slots ?? sandboxPathEnvSlots;
15
16
  return async (row, identity) => {
16
17
  const metadata = await chainMetadataOf(deps, row, identity);
@@ -26,7 +27,12 @@ export function createParkedReviveInheritedGate(deps) {
26
27
  ...homeBase,
27
28
  }
28
29
  : { env: createHostAdjudicationEnv(config, localRoot), ...homeBase };
29
- const baseline = createApprovalBaselinePolicy(config, { kind: "durable", question });
30
+ const rootSessionId = row.rootSessionId;
31
+ const baseline = createApprovalBaselinePolicy(config, {
32
+ kind: "durable",
33
+ question,
34
+ ...(approvalExemptionStore !== undefined && rootSessionId ? { exemption: createDeploymentListExemption(approvalExemptionStore, rootSessionId, logger, "parked_revive") } : {}),
35
+ });
30
36
  const governed = applyRuntimeGovernance({ objective: "", toolPolicy: baseline }, createDeploymentGovernanceInputs(config, pathAdjudication, { engineDataRoot: memoryEngineRoot }));
31
37
  return {
32
38
  parentConstraints: [
@@ -10,7 +10,7 @@ import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.j
10
10
  import { buildNoticeLaneFacts, shouldDiscloseNoPersistentMemory, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
11
11
  import { applyLongtailDefer } from "../capabilities/tool-defer.js";
12
12
  import { assertGuardPatternsUsable, buildApprovalGrantNotice, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs } from "../deployment-governance.js";
13
- import { gateIntentUnserviceableReason, singleUserAutoAcceptBaselineApplies } from "../approval.js";
13
+ import { createDeploymentListExemption, gateIntentUnserviceableReason, singleUserAutoAcceptBaselineApplies } from "../approval.js";
14
14
  import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane, scratchpadCoveringTaskRoot } from "../env-facts.js";
15
15
  import { FleetEventBus } from "../fleet/fleet-bus.js";
16
16
  import { composeHooks, createTaskHooks } from "../hooks/hook-runner.js";
@@ -137,9 +137,8 @@ export function createResolveSpec(ctx) {
137
137
  return stamped;
138
138
  };
139
139
  const gateScenarioAndAppend = async (body, auth, opts) => {
140
- const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario, Object.keys(scenarios), logger);
140
+ const scenarioName = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario, Object.keys(scenarios), logger, auth?.principal);
141
141
  gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), executionLaneOf(config.remoteExec?.provider), logger);
142
- const scenarioName = scenarios[requested] ? requested : "default";
143
142
  const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal, auth?.sessionId !== undefined ? { sessionId: auth.sessionId } : {});
144
143
  const centerDecls = getCenterPrompts()?.declarations;
145
144
  const appendLessPack = centerDecls
@@ -460,7 +459,11 @@ export function createResolveSpec(ctx) {
460
459
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
461
460
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
462
461
  toolPolicy: toolPolicyForHands(hands, durableEnabled
463
- ? createApprovalBaselinePolicy(config, { kind: "durable", question: liveQuestionFace })
462
+ ? createApprovalBaselinePolicy(config, {
463
+ kind: "durable",
464
+ question: liveQuestionFace,
465
+ ...(approvalExemptionStore !== undefined && auth?.sessionId ? { exemption: createDeploymentListExemption(approvalExemptionStore, auth.sessionId, logger) } : {}),
466
+ })
464
467
  : singleUserAutoAcceptBaselineApplies(config)
465
468
  ? createApprovalBaselinePolicy(config, { kind: "absent" })
466
469
  : undefined),
@@ -311,9 +311,10 @@ export declare function createEngineNoticeSeat(logger: {
311
311
  * 「键为 undefined」与「键缺席」对引擎逐字等价)。**不复制引擎缺省**(`roots`/`static-face`/`carry`/`open`/
312
312
  * CC parity 20/200 一个都不抄):上游改缺省之日起本仓不会成为第二份真源。
313
313
  */
314
- export type DeploymentPostureSeats = Pick<RunnerDeps, "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "allowedTools">;
315
- /** 本族读的配置切面:席名与 config 键同名的那几席 + 运维 allow 表(席 `allowedTools` ← config `approvalGrant`,env 名 `APPROVAL_GRANT` 不改)。 */
316
- export type DeploymentPostureConfig = Pick<ServiceConfig, Exclude<keyof DeploymentPostureSeats, "allowedTools"> | "approvalGrant">;
314
+ export type DeploymentPostureSeats = Pick<RunnerDeps, "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "allowedTools" | "agentConfigDir">;
315
+ /** 本族读的配置切面:席名与 config 键同名的那几席 + 运维 allow 表(席 `allowedTools` ← config `approvalGrant`,env 名 `APPROVAL_GRANT` 不改)
316
+ * + LKG 覆写路径(`readDenyPatterns` 席并入它派生的读拒行,`lkgReadDenyRows`)。 */
317
+ export type DeploymentPostureConfig = Pick<ServiceConfig, Exclude<keyof DeploymentPostureSeats, "allowedTools"> | "approvalGrant" | "configLkgPath">;
317
318
  export declare function buildDeploymentPostureSeats(config: DeploymentPostureConfig): DeploymentPostureSeats;
318
319
  export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
319
320
  export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
@@ -1,6 +1,7 @@
1
1
  import { createFileWorkflowScriptStore } from "@sema-agent/core";
2
2
  import { join } from "node:path";
3
3
  import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
4
+ import { lkgReadDenyRows } from "../config-lkg.js";
4
5
  import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
5
6
  import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
6
7
  import { createMemorySessionIncarnationSeat, sessionFaceReaderOf } from "./session-incarnation-seat.js";
@@ -9,7 +10,7 @@ import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
9
10
  import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
10
11
  import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
11
12
  import { resolveServedSession } from "../orchestration/workflow-completion-inbox.js";
12
- import { defaultEngineNoticeRouter, presentNoticeDetail } from "../trace/engine-notice-wire.js";
13
+ import { defaultEngineNoticeRouter, redactNoticeRuleText } from "../trace/engine-notice-wire.js";
13
14
  import { WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
14
15
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "../project-memory.js";
15
16
  import { createInstructionSourceSeat } from "../instruction-source-notice.js";
@@ -41,7 +42,7 @@ const PROMPT_CACHE_BREAK_LEVELS = new Map(Object.entries(PROMPT_CACHE_BREAK_CAUS
41
42
  const CEILING_ORIGINS = new Set(["walltime", "turns"]);
42
43
  export function createEngineNoticeForwarder(logger, router = defaultEngineNoticeRouter, metrics) {
43
44
  return (notice) => {
44
- const detail = presentNoticeDetail(notice.code, notice.detail);
45
+ const detail = redactNoticeRuleText(notice.code, notice.detail);
45
46
  logger.warn("engine_notice", { code: notice.code, message: notice.message, ...(detail !== undefined ? { detail } : {}) });
46
47
  if (metrics !== undefined && notice.code === "task.interrupt_unconsumed") {
47
48
  const origin = notice.detail?.origin;
@@ -57,11 +58,14 @@ export function createEngineNoticeSeat(logger, router = defaultEngineNoticeRoute
57
58
  }
58
59
  export function buildDeploymentPostureSeats(config) {
59
60
  const crossSessionLayers = buildCrossSessionInboundLayers(config);
61
+ const lkgRows = lkgReadDenyRows(config.configLkgPath);
62
+ const readDenyAdditions = lkgRows.length === 0 ? config.readDenyPatterns : [...(config.readDenyPatterns ?? []), ...lkgRows];
60
63
  const seats = {
61
64
  readFace: config.readFace ? config.readFace : undefined,
62
- readDenyPatterns: config.readDenyPatterns ? config.readDenyPatterns : undefined,
65
+ readDenyPatterns: readDenyAdditions ? readDenyAdditions : undefined,
63
66
  readDenyBuiltinTiers: config.readDenyBuiltinTiers ?? undefined,
64
67
  readDenyBuiltinExclude: config.readDenyBuiltinExclude ?? undefined,
68
+ agentConfigDir: config.agentConfigDir ?? undefined,
65
69
  memoryDelegationEvidence: config.memoryDelegationEvidence ?? undefined,
66
70
  memoryProvenance: config.memoryProvenance ?? undefined,
67
71
  memoryCapturePolicy: config.memoryCapturePolicy ?? undefined,
@@ -13,6 +13,7 @@ import { installSecretEnvScrubRecorder } from "../observability/secret-env-scrub
13
13
  import { setRunTerminalLogObserver } from "../observability/run-terminal.js";
14
14
  import { setRedactionObserver } from "../trace/redact.js";
15
15
  import { installOutboundDispatcher } from "../outbound-dispatcher.js";
16
+ import { provisionSemaConfigDir } from "../sema-config-dir.js";
16
17
  export function runStage01() {
17
18
  const config = loadConfig();
18
19
  const runStaleFailure = crossInvariant("run-stale-heartbeat").check({ live: config, next: config });
@@ -44,6 +45,7 @@ export function runStage01() {
44
45
  else
45
46
  logger.info("run_failed", fields);
46
47
  });
48
+ provisionSemaConfigDir(config, logger);
47
49
  const localRoot = configLocalRoot(config);
48
50
  const rootsSplit = splitLocalRoots({
49
51
  localDataRoot: config.localDataRoot,
@@ -10,7 +10,7 @@ import { buildForceDurableGateInertNotice, buildNoResponderBootWarns, gateIntent
10
10
  import { executionLaneOf } from "../execution-lane-caps.js";
11
11
  import { loadDeploymentSkills } from "../capabilities/skills.js";
12
12
  import { createRepoClient } from "../capabilities/repo-tools.js";
13
- import { buildScenarios, builtinScenarioDetails } from "../capabilities/scenarios.js";
13
+ import { buildScenarios, builtinScenarioDetails, danglingScenarioRefusal } from "../capabilities/scenarios.js";
14
14
  import { RateLimiter } from "../observability/rate-limit.js";
15
15
  import { explicitOperatorOk } from "../http/server.js";
16
16
  import { selfOrchestrationDenial } from "../task-workflow.js";
@@ -169,6 +169,7 @@ export async function runStage07(ctx) {
169
169
  config, question, logger, localRoot,
170
170
  memoryEngineRoot: memoryEngine?.root,
171
171
  approverSeat: createRunnerDepsOnAsk(toolApproval),
172
+ approvalExemptionStore,
172
173
  ...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
173
174
  autoModeSeatMounted: runnerDeps.autoMode !== undefined,
174
175
  })
@@ -176,6 +177,9 @@ export async function runStage07(ctx) {
176
177
  const scenarioDetails = builtinScenarioDetails(scenarios, scenarioDeps);
177
178
  const builtinScenarioNames = Object.keys(scenarioDetails);
178
179
  configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
180
+ const defaultScenarioRefusal = danglingScenarioRefusal(config.defaultScenario, "deployment", Object.keys(scenarios));
181
+ if (defaultScenarioRefusal !== undefined)
182
+ throw new Error(defaultScenarioRefusal);
179
183
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
180
184
  return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, userInputLedger, userInputLedgerStore, webSearchProvider: webSearchCfg?.provider };
181
185
  }
@@ -12,6 +12,7 @@ import { backgroundAgentOutput, taskHandleOutput, taskHandleStop } from "../runs
12
12
  import { SQL_BLOB_DEFAULT_MAX_BYTES } from "../plugins/blob-backend.js";
13
13
  import { createStoreLiveProbe } from "../store-live-probe.js";
14
14
  import { counterStoreLabel } from "../plugins/store-backend.js";
15
+ import { sessionStoreLabelOf } from "../store-posture.js";
15
16
  import { startFleetClientFromEnv } from "../fleet-client.js";
16
17
  import { createCenterWiringReader } from "../capabilities/center-wiring.js";
17
18
  import { readinessOf } from "../http/readiness.js";
@@ -133,7 +134,7 @@ export async function runStage10(ctx) {
133
134
  ? createStoreLiveProbe({ probe: () => backend.dbNowMs(), intervalMs: config.storeProbeIntervalMs, logger })
134
135
  : undefined;
135
136
  const deployment = {
136
- sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
137
+ sessionStoreLabel: sessionStoreLabelOf(config.sessionBackend, backend?.kind),
137
138
  modelReady: () => configCenter.modelReady(),
138
139
  scenarioDetails,
139
140
  workflowsCapable,
@@ -269,7 +270,7 @@ export async function runStage10(ctx) {
269
270
  model: config.model.id,
270
271
  brain: brainSummary(config),
271
272
  breakerState: config.resilience.circuitBreaker ? (breakerState ? `shared(${backend.kind})` : "in-process") : "off",
272
- session: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
273
+ session: sessionStoreLabelOf(config.sessionBackend, backend?.kind),
273
274
  memory: memoryEngine ? `engine(${memoryEngine.root})` : "off",
274
275
  instanceId,
275
276
  rateLimitPerMin: config.rateLimitPerMin,
@@ -9,6 +9,7 @@ import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "../plugin
9
9
  import { PgMemoryHistoryStore, PgMemorySyncStore, ensurePgMemoryHistorySchema, ensurePgMemorySyncSchema } from "../plugins/memory-sync-store-pg.js";
10
10
  import { TiDBMemoryHistoryStore, TiDBMemorySyncStore, ensureTiDBMemoryHistorySchema, ensureTiDBMemorySyncSchema } from "../plugins/memory-sync-store-tidb.js";
11
11
  import { MinioBlobBackend } from "../plugins/blob-backend.js";
12
+ import { ensureSnapshotBucket } from "../plugins/snapshot-bucket-presence.js";
12
13
  import { PgBackgroundAgentStore, TiDBBackgroundAgentStore, ensurePgBackgroundAgentSchema, ensureTiDBBackgroundAgentSchema } from "../plugins/background-agent-store-sql.js";
13
14
  import { PgMailboxStore, TiDBMailboxStore, ensurePgMailboxSchema, ensureTiDBMailboxSchema } from "../plugins/mailbox-store-sql.js";
14
15
  import { createDropOnlyRecipientLifecycle } from "../plugins/mailbox-recipient-lifecycle.js";
@@ -48,9 +49,12 @@ export async function openStores(ctx) {
48
49
  const { config, logger, metrics, localRoot } = ctx;
49
50
  let backend;
50
51
  let storeDegradedReason;
52
+ const dsnConfigured = !!(config.tidb || config.pg);
53
+ if (config.snapshotBlobStore)
54
+ await ensureSnapshotBucket(config.snapshotBlobStore, config.storeLockWaitMs, logger);
51
55
  const wantDb = config.sessionBackend === "tidb" ||
52
56
  config.dbBackend === "local" ||
53
- (config.sessionBackend === "auto" && !!(config.tidb || config.pg));
57
+ (config.sessionBackend === "auto" && dsnConfigured);
54
58
  if (wantDb) {
55
59
  const opened = await openStoreBackendWithFallback(config, logger);
56
60
  backend = opened.backend;
@@ -111,7 +115,7 @@ export async function openStores(ctx) {
111
115
  metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
112
116
  }
113
117
  if (config.sessionBackend === "auto")
114
- config.sessionBackend = backend ? "tidb" : "memory";
118
+ config.sessionBackend = dsnConfigured ? "tidb" : "memory";
115
119
  if (config.dbBackend === "local" && backend)
116
120
  config.sessionBackend = "tidb";
117
121
  let memoryEngine;
@@ -414,11 +418,12 @@ export async function openStores(ctx) {
414
418
  }
415
419
  const sessionStore = createSessionStore(config, backend, metrics);
416
420
  if (config.requirePrincipal && backend?.kind === "local") {
417
- throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
418
- "durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
419
- "owners = P1), so it cannot durably enforce multi-tenant session ownership. Use a SQL backend " +
420
- "(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
421
- "still inject x-agent-principal per request for memory scoping and audit attribution).");
421
+ throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): the lane is single-user " +
422
+ "by construction — a session created before owners were recorded is claimed by the FIRST principal that submits " +
423
+ "to it during the upgrade window, and the lane's mutual-recognition arms assume one user — so it cannot enforce " +
424
+ "a multi-tenant session boundary. Use a SQL backend (DB_BACKEND=mysql|pg) for multi-tenant, or run local " +
425
+ "single-user with REQUIRE_PRINCIPAL=false (a BFF may still inject x-agent-principal per request for memory " +
426
+ "scoping and audit attribution).");
422
427
  }
423
428
  if (backend)
424
429
  assertCloudSnapshotBlobPosture(backend.kind, config);