@sema-agent/server 7.118.0 → 7.119.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 (101) hide show
  1. package/MIGRATION.md +85 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +24 -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 +1 -1
  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/leader.d.ts +4 -4
  35. package/dist/boot/leader.js +1 -2
  36. package/dist/boot/parked-revive-gate.d.ts +5 -1
  37. package/dist/boot/parked-revive-gate.js +8 -2
  38. package/dist/boot/resolve-spec.js +7 -4
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-10-http-server.js +3 -2
  41. package/dist/boot/stores.js +12 -7
  42. package/dist/capabilities/center-plugins.d.ts +2 -0
  43. package/dist/capabilities/center-plugins.js +24 -6
  44. package/dist/capabilities/code-review-council.js +8 -11
  45. package/dist/capabilities/scenarios.d.ts +34 -7
  46. package/dist/capabilities/scenarios.js +33 -6
  47. package/dist/config-catalog.js +3 -3
  48. package/dist/config.js +2 -2
  49. package/dist/deployment-governance.d.ts +5 -2
  50. package/dist/deployment-governance.js +4 -3
  51. package/dist/host-decision.d.ts +52 -27
  52. package/dist/host-decision.js +14 -7
  53. package/dist/http/admission.js +6 -6
  54. package/dist/http/resume-legs.js +2 -1
  55. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  56. package/dist/http/routes/approvals-assistant.js +9 -5
  57. package/dist/http/routes/capabilities.js +4 -3
  58. package/dist/http/routes/memory-origin.d.ts +1 -1
  59. package/dist/http/routes/runs.js +11 -5
  60. package/dist/http/routes/sessions.js +20 -16
  61. package/dist/http/wire-types.d.ts +12 -13
  62. package/dist/images/manifest.d.ts +7 -1
  63. package/dist/images/manifest.js +18 -16
  64. package/dist/memory-store-refusal.d.ts +7 -12
  65. package/dist/memory-store-refusal.js +4 -5
  66. package/dist/namespaced-rule-name.d.ts +4 -3
  67. package/dist/observability/fail-open.d.ts +5 -1
  68. package/dist/observability/fail-open.js +5 -1
  69. package/dist/operator-ask.d.ts +6 -5
  70. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  71. package/dist/plugins/approval-exemption-store.js +1 -0
  72. package/dist/plugins/blob-backend.d.ts +13 -0
  73. package/dist/plugins/blob-backend.js +26 -0
  74. package/dist/plugins/local-session-store.d.ts +78 -47
  75. package/dist/plugins/local-session-store.js +152 -122
  76. package/dist/plugins/one-time-migrations.d.ts +6 -0
  77. package/dist/plugins/one-time-migrations.js +19 -1
  78. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  79. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  80. package/dist/plugins/store-backend.d.ts +17 -6
  81. package/dist/plugins/store-backend.js +46 -16
  82. package/dist/plugins/store-open-wait.d.ts +33 -0
  83. package/dist/plugins/store-open-wait.js +73 -0
  84. package/dist/rule-entry-list.js +1 -1
  85. package/dist/run-local.js +4 -3
  86. package/dist/security.d.ts +5 -3
  87. package/dist/security.js +4 -0
  88. package/dist/session-policy-wire.js +7 -12
  89. package/dist/spec-fields.d.ts +6 -2
  90. package/dist/store-posture.d.ts +14 -6
  91. package/dist/store-posture.js +7 -4
  92. package/dist/task-settings.d.ts +27 -55
  93. package/dist/task-settings.js +6 -18
  94. package/dist/tool-approval.d.ts +97 -43
  95. package/dist/tool-approval.js +120 -31
  96. package/dist/trace/redact.d.ts +6 -0
  97. package/dist/trace/redact.js +9 -9
  98. package/dist/trace/self-minted-wire.d.ts +1 -1
  99. package/dist/user-input-ledger.d.ts +19 -15
  100. package/dist/user-input-ledger.js +12 -20
  101. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventForRead, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { z } from "zod";
4
- import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
4
+ import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
5
5
  import { RUN_HOLDER_DEATH_CODES, isTerminalRunStatus, isParkedRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
6
6
  import { LIVE_RUN_GATE_KINDS, TERMINAL_PARK_GATE_KINDS } from "../../plugins/checkpoint-store-sql.js";
7
7
  import { redactSecrets } from "../../trace/redact.js";
@@ -15,14 +15,14 @@ import { scopedIdempotencyKey } from "../idempotency.js";
15
15
  import { streamSseLog } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
18
- import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
18
+ import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
19
19
  import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
20
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
21
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
22
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
23
23
  import { readTurnActivityMs } from "../../turn-activity.js";
24
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
- import { userInputLaneOf } from "../../user-input-ledger.js";
25
+ import { acceptedInputKindOf } from "../../user-input-ledger.js";
26
26
  import { INJECTION_TIERS } from "../../trace/injection-tier.js";
27
27
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
28
28
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
@@ -150,8 +150,9 @@ function sendSteerReplayRefusal(res, refusal) {
150
150
  async function acceptLiveUserInput(deps, res, at) {
151
151
  if (!deps.userInputLedger)
152
152
  return undefined;
153
+ const input = acceptedInputKindOf(at.tier);
153
154
  try {
154
- return await deps.userInputLedger.accept({ input: userInputLaneOf(at.tier), messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
155
+ return await deps.userInputLedger.accept({ input, messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
155
156
  }
156
157
  catch (e) {
157
158
  deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
@@ -1372,6 +1373,11 @@ export async function handleRunVerbs(req, res, match, ctx) {
1372
1373
  }
1373
1374
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1374
1375
  if (!parsedBody.success) {
1376
+ const retired = retiredDecisionKeyFacts(rawBody);
1377
+ if (retired !== undefined) {
1378
+ sendError(res, 400, "request.body_shape", retired.error, retired.extra);
1379
+ return;
1380
+ }
1375
1381
  sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1376
1382
  return;
1377
1383
  }
@@ -1517,7 +1523,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1517
1523
  sendTerminal(row0);
1518
1524
  return;
1519
1525
  }
1520
- if (policyCannotAllow(decidedBy, body.decision === "approve", storedCardDemandsAPerson(row0.cardJson))) {
1526
+ if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
1521
1527
  sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
1522
1528
  return;
1523
1529
  }
@@ -1,7 +1,7 @@
1
- import { materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
1
+ import { SessionError, materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
2
2
  import { a2aForScenario, mcpForScenario } from "../../config-center/facade.js";
3
3
  import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
4
- import { windowMessages, truncateMessageBlobs } from "../../audit.js";
4
+ import { windowMessages, truncateMessageBlobs, redactSessionViewForRead } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
@@ -596,9 +596,13 @@ export async function handleSessions(req, res, match, ctx) {
596
596
  sendError(res, 501, "capability.session_store_required", "the session audit view requires a durable session store (DB_BACKEND=mysql|pg|local)");
597
597
  return;
598
598
  }
599
- const audit = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
600
- if (audit === undefined)
599
+ const owned = await readOwnedSessionView(auditFact.sessionAudit, match.params.sessionId);
600
+ if (owned === undefined)
601
601
  return;
602
+ if (!Array.isArray(owned.messages)) {
603
+ throw new SessionError("corrupt_session", `session ${match.params.sessionId} is unreadable: its archive projection carries "messages" as ${owned.messages === null ? "null" : typeof owned.messages}, not an array`);
604
+ }
605
+ const ownedMessages = owned.messages;
602
606
  const q = new URLSearchParams(match.search);
603
607
  const qi = (k) => {
604
608
  const v = q.get(k);
@@ -609,12 +613,12 @@ export async function handleSessions(req, res, match, ctx) {
609
613
  };
610
614
  const msgIndex = qi("message");
611
615
  if (msgIndex !== undefined) {
612
- const messages = audit.messages;
613
- if (msgIndex >= messages.length) {
614
- sendError(res, 404, "not_found.message_index", "message index out of range", { total: messages.length });
616
+ if (msgIndex >= ownedMessages.length) {
617
+ sendError(res, 404, "not_found.message_index", "message index out of range", { total: ownedMessages.length });
615
618
  return;
616
619
  }
617
- sendJson(res, 200, { sessionId: audit.sessionId, floorEntryId: audit.floorEntryId, index: msgIndex, message: messages[msgIndex] });
620
+ const one = redactSessionViewForRead({ ...owned, messages: [ownedMessages[msgIndex]] });
621
+ sendJson(res, 200, { sessionId: owned.sessionId, floorEntryId: owned.floorEntryId, index: msgIndex, message: one.messages[0] });
618
622
  return;
619
623
  }
620
624
  const tail = qi("tail");
@@ -631,19 +635,19 @@ export async function handleSessions(req, res, match, ctx) {
631
635
  }
632
636
  const undrainedKey = undrained !== undefined ? { userInputUndrained: undrained } : {};
633
637
  if (tail === undefined && before === undefined && blobCap === undefined) {
634
- sendJson(res, 200, { ...audit, ...undrainedKey });
638
+ sendJson(res, 200, { ...redactSessionViewForRead(owned), ...undrainedKey });
635
639
  return;
636
640
  }
637
- let messages = audit.messages;
641
+ let selected = ownedMessages;
638
642
  let window;
639
643
  if (tail !== undefined || before !== undefined) {
640
- const w = windowMessages(messages, { ...(tail !== undefined ? { tail } : {}), ...(before !== undefined ? { before } : {}), ...(qi("limit") !== undefined ? { limit: qi("limit") } : {}) });
641
- messages = w.messages;
644
+ const w = windowMessages(ownedMessages, { ...(tail !== undefined ? { tail } : {}), ...(before !== undefined ? { before } : {}), ...(qi("limit") !== undefined ? { limit: qi("limit") } : {}) });
645
+ selected = w.messages;
642
646
  window = w.window;
643
647
  }
644
- if (blobCap !== undefined && blobCap > 0)
645
- messages = truncateMessageBlobs(messages, blobCap);
646
- sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}), ...undrainedKey });
648
+ const view = redactSessionViewForRead({ ...owned, messages: selected });
649
+ const messages = blobCap !== undefined && blobCap > 0 ? truncateMessageBlobs(view.messages, blobCap) : view.messages;
650
+ sendJson(res, 200, { ...view, messages, ...(window ? { window } : {}), ...undrainedKey });
647
651
  return;
648
652
  }
649
653
  }
@@ -756,7 +760,7 @@ export async function handleSessions(req, res, match, ctx) {
756
760
  return;
757
761
  }
758
762
  if (owner === null) {
759
- sendError(res, 409, "conflict.session_ownerless", "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless");
763
+ sendError(res, 409, "conflict.session_ownerless", "this session has no recorded owner, so its rules can be neither read nor changed through this route; rules already stored for it keep applying to the runs of the principal they were stored under. Ways out: on the local backend, during the upgrade window, the first task submitted with a principal on a session written by an earlier version claims it for that principal (this route then serves its owner); or delete the session (DELETE /v1/sessions/:id), which removes its rules with it");
760
764
  return;
761
765
  }
762
766
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -6,8 +6,7 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
- import type { SettingsPermissionMode } from "../task-settings.js";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, PermissionMode, SkillSpec } from "@sema-agent/core";
11
10
  import type { wiringManifestFaceEventData } from "../trace/project.js";
12
11
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
13
12
  export interface TaskRequestBody {
@@ -17,7 +16,9 @@ export interface TaskRequestBody {
17
16
  * table — e.g. "default"/"code-review"/"scan"/"discuss"). An INTENT bounded by the deployment's own table AND,
18
17
  * on a governed worker, by the principal's center-resolved allowlist: an unknown name is a typed 400
19
18
  * `scenario_unknown`, a known-but-not-allowed one a typed 400 `scenario_not_allowed`; absent ⇒ the ruling's
20
- * assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`).
19
+ * assigned scenario, else `DEFAULT_SCENARIO` (consumed at `boot/resolve-spec.ts` via `gateScenarioRequest`). A name
20
+ * on that default chain this deployment does not have is NEVER folded to `default`: an assigned one is a typed 503
21
+ * `state.scenario_assignment_unknown`, a dangling `DEFAULT_SCENARIO` refuses to boot.
21
22
  * ⚠️ scenario-SPECIFIC keys (`repo`/`lenses`/`rounds`, read through `ScenarioRequest`'s index signature) are
22
23
  * deliberately NOT declared here — they belong to one scenario each, not to the shared submit surface. */
23
24
  scenario?: string;
@@ -333,18 +334,16 @@ export interface TaskRequestBody {
333
334
  toolMaterializeStrategy?: "static" | "swap";
334
335
  /** R4 (CC parity): the LIGHT top-level per-turn permission-mode intent (CC `permissionMode`). The shell sends
335
336
  * the RAW mode (axis-agnostic, no client-side interpretation); the SERVICE interprets it tighten-only vs the
336
- * deployment baseline. Post-[ref] all FIVE modes are honored as gate-SHAPE choices (`plan` ⇒ read-only hands +
337
- * `present_plan`; `acceptEdits`/`bypassPermissions`/`auto` select how much the mode-derived fs-write ask gate
338
- * asks — they can never subtract from deployment/operator policy, tightenTaskSpec deny-wins; the old "LOOSEN →
339
- * coerced to default" note was pre-[ref] doc-rot). Folds onto the SAME tighten-only governance as
340
- * `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path.
337
+ * deployment baseline. All SIX core modes are honored (`plan` ⇒ read-only hands + `present_plan`;
338
+ * `acceptEdits` selects how much the mode-derived fs-write ask gate asks; `bypassPermissions`/`auto`/`dontAsk` ride the
339
+ * engine's own mode table verbatim — `dontAsk` refuses every ask by the mode, zero cards, the gate records
340
+ * `deniedBy:"mode"`) — they can never subtract from deployment/operator policy, tightenTaskSpec deny-wins). Folds onto the SAME
341
+ * tighten-only governance as `settings.permissions.defaultMode` (the heavier bundle), so it's a lighter alias, not a second path.
341
342
  * [ref]-②: CLOSED enum on the wire — an unknown word 400s at submit (`request.field_invalid`, sibling of
342
343
  * promptProfile) instead of silently coercing to `default`; resume replay of a stored body stays lenient
343
- * (coercePermissionMode). */
344
- /** 🔴 型**从值表派生**(S-496 U1 / [ref]):闭集的属主是 `task-settings.ts` 的
345
- * `SETTINGS_PERMISSION_MODES`(受理腿 `isSettingsPermissionMode` 与两句 400 文案同读那一份)。
346
- * 改前这里是第 6 份手抄的五词联合 —— 加/退一个模式时 wire 型面与受理腿会各走各的。 */
347
- permissionMode?: SettingsPermissionMode;
344
+ * (coercePermissionMode).
345
+ * 🔴 型与闭集的属主是 core 的 `PermissionMode` / `PERMISSION_MODES`(受理腿 `isPermissionMode` 与三句 400 文案同读那一份)—— 本仓不持词表。 */
346
+ permissionMode?: PermissionMode;
348
347
  /** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
349
348
  * read-only over long-term memory (`TaskSpec.memory.writeScope:null` — the [ref] memory ENGINE materializes/
350
349
  * reads but its harvest commits nothing). Absent/`true` ⇒ normal read+write. The shell's `/memory` pause carries
@@ -6,7 +6,13 @@
6
6
  * one-time seed and the P3 atomic-publish pipeline — kept side-effect-free so it is unit-tested without a DB.
7
7
  */
8
8
  import type { ImageIndexUpsert, ImageNestedBuildMode, ImageStatus } from "../plugins/store-contracts.js";
9
- /** Project the manifest's `nestedBuild` (string posture OR bool) → the enum (center contract). */
9
+ /**
10
+ * Project the manifest's `nestedBuild` (string posture OR bool) → the enum (center contract).
11
+ *
12
+ * 声明型 = 闭集词 | `true`(老写法的裸布尔,含义 = `rootless-buildkit`)| `false` / 缺席(= `none`)。**其余一律抛** —— 修前「未知的非空词」
13
+ * 被上调成 `rootless-buildkit`(一条没人声明过的 nested-build 能力写进 index),运行期的非声明型真值(数字 / 对象 …)走 `v ? … : …` 同样被上调。
14
+ * 抛出的句子沿用该面既有的拒法:`done` 帧的 auto-register 把它收成「built-but-unregistered」终局 FAILED,错误句带这一句(见 `http/routes/images.ts`)。
15
+ */
10
16
  export declare function nestedBuildModeOf(v: string | boolean | undefined): ImageNestedBuildMode;
11
17
  /** The fields of build.sh's `sandbox-manifest.json` this projection reads (a superset is fine; extras ignored). */
12
18
  export interface BuildManifest {
@@ -1,18 +1,19 @@
1
- const NESTED_BUILD_MODES = [
2
- "none",
3
- "docker-cli-only",
4
- "rootless-buildkit",
5
- "privileged-dind",
6
- ];
1
+ const NESTED_BUILD_MODE_WORDS = {
2
+ none: true,
3
+ "docker-cli-only": true,
4
+ "rootless-buildkit": true,
5
+ "privileged-dind": true,
6
+ };
7
+ const isNestedBuildMode = (v) => Object.prototype.hasOwnProperty.call(NESTED_BUILD_MODE_WORDS, v);
7
8
  export function nestedBuildModeOf(v) {
8
- if (typeof v === "string") {
9
- return NESTED_BUILD_MODES.includes(v)
10
- ? v
11
- : v
12
- ? "rootless-buildkit"
13
- : "none";
14
- }
15
- return v ? "rootless-buildkit" : "none";
9
+ if (v === undefined || v === false)
10
+ return "none";
11
+ if (v === true)
12
+ return "rootless-buildkit";
13
+ if (typeof v === "string" && isNestedBuildMode(v))
14
+ return v;
15
+ const got = typeof v === "string" ? `the string ${JSON.stringify(v.slice(0, 64))}` : v === null ? "null" : Array.isArray(v) ? "an array" : `type ${typeof v}`;
16
+ throw new Error(`manifest capabilities.nestedBuild must be one of ${Object.keys(NESTED_BUILD_MODE_WORDS).join(" | ")} (or a bare boolean); got ${got}`);
16
17
  }
17
18
  export function shmToMB(v) {
18
19
  if (!v)
@@ -32,11 +33,12 @@ export function profileFromManifest(m) {
32
33
  export function manifestToIndexEntry(m, opts) {
33
34
  const profile = profileFromManifest(m);
34
35
  const tag = (m.tag ?? "").includes(":") ? (m.tag ?? "").split(":")[1] : (m.tag ?? "");
36
+ const nestedBuildMode = nestedBuildModeOf(m.capabilities?.nestedBuild);
35
37
  const capabilities = {
36
38
  browser: !!m.capabilities?.browser,
37
39
  db: !!m.capabilities?.db,
38
- nestedBuild: !!m.capabilities?.nestedBuild,
39
- nestedBuildMode: nestedBuildModeOf(m.capabilities?.nestedBuild),
40
+ nestedBuild: nestedBuildMode !== "none",
41
+ nestedBuildMode,
40
42
  };
41
43
  const pc = m.capabilities?.podContract;
42
44
  const devShmMB = shmToMB(pc?.devShm);
@@ -34,27 +34,22 @@
34
34
  * 路径不上 wire,只进这一条运维 warn —— 它是**日志行**不是 wire 错误体,所以铸点跟读法属主住在一起(不住 `http/`:`http/` 全树门
35
35
  * `test/error-code-key-gate.test.ts` 咬的是错误体里的 `code:` 键位,日志字段名不该借 wire 层的目录去过它)。
36
36
  */
37
+ import { type ScopeHomeForm, type StoreFormRefusalCode } from "@sema-agent/core";
37
38
  import type { Logger } from "./observability/logger.js";
38
- /** core 的两码(`dist/core/memory-engine/layout.js` `TARGET_OUTSIDE_STORE` / `SCOPE_HOME_NOT_DIRECTORY`;core 不在包根导出它们,这两个常量是它们在本仓的唯一写点)。 */
39
- export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
40
- export declare const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
41
- /** 店形拒族(闭集;core 加第三码 ⇒ 它落未分类 500 + 运维 warn,由人裁归不归这一族)。 */
42
- export declare const STORE_FORM_REFUSAL_CODES: readonly ["memory.target_outside_store", "memory.scope_home_not_directory"];
43
- export type StoreFormRefusalCode = (typeof STORE_FORM_REFUSAL_CODES)[number];
44
39
  /** 一次店形拒的读数(纯数据)。`path` / `form` 取自 core 的 `detail`(读不出 ⇒ `undefined`,不猜、不从句子里抠)。 */
45
- export interface StoreFormRefusal {
40
+ export interface StoreFormRefusalReading {
46
41
  readonly code: StoreFormRefusalCode;
47
42
  /** core 原句(以码领头;7.44.0 起不含宿主路径)。 */
48
43
  readonly message: string;
49
44
  /** `detail.path`:落点 / 被改指的根 / 不是目录的 scope home —— **只给运维行**。 */
50
45
  readonly path: string | undefined;
51
- /** `detail.form`(只在 home 那一码上):原样字符串,不镜像 core 的闭集(本仓只把它写进运维行,不按它分支)。 */
52
- readonly form: string | undefined;
46
+ /** `detail.form`(只在 home 那一码上),经 core 闭集读;本仓只把它写进运维行,不按它分支。 */
47
+ readonly form: ScopeHomeForm | undefined;
53
48
  }
54
- /** 抛出面:`err` 是店形拒 ⇒ 读数;否则 `undefined`。形 = core 的 `Error` + `.code` + `.detail`。 */
55
- export declare function storeFormRefusalOf(err: unknown): StoreFormRefusal | undefined;
49
+ /** 抛出面:`err` 是店形拒(core `StoreFormRefusal` 的形:码在族里)⇒ 读数;否则 `undefined`。`detail` 读不全不改判(码定族,`detail` 只供运维行)。 */
50
+ export declare function storeFormRefusalOf(err: unknown): StoreFormRefusalReading | undefined;
56
51
  /** 两受众共用的运维行(一处铸点):码 + core `detail` 里的宿主路径与 home 形。`logger` 缺席(只有不接 logger 的测试装配)⇒ 不落。 */
57
- export declare function logStoreFormRefusal(logger: Pick<Logger, "warn"> | undefined, refusal: StoreFormRefusal): void;
52
+ export declare function logStoreFormRefusal(logger: Pick<Logger, "warn"> | undefined, refusal: StoreFormRefusalReading): void;
58
53
  /** 报告面:harvest `incident` 是这一族拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成店形拒)。 */
59
54
  export declare function storeFormRefusalInIncident(incident: {
60
55
  readonly kind: string;
@@ -1,7 +1,6 @@
1
- export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
2
- export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
3
- export const STORE_FORM_REFUSAL_CODES = [TARGET_OUTSIDE_STORE, SCOPE_HOME_NOT_DIRECTORY];
4
- const isStoreFormRefusalCode = (code) => typeof code === "string" && STORE_FORM_REFUSAL_CODES.includes(code);
1
+ import { SCOPE_HOME_FORMS, STORE_FORM_REFUSAL_CODES } from "@sema-agent/core";
2
+ const isStoreFormRefusalCode = (code) => STORE_FORM_REFUSAL_CODES.includes(code);
3
+ const isScopeHomeForm = (form) => SCOPE_HOME_FORMS.includes(form);
5
4
  export function storeFormRefusalOf(err) {
6
5
  if (!(err instanceof Error))
7
6
  return undefined;
@@ -9,7 +8,7 @@ export function storeFormRefusalOf(err) {
9
8
  if (!isStoreFormRefusalCode(code))
10
9
  return undefined;
11
10
  const d = typeof detail === "object" && detail !== null ? detail : {};
12
- return { code, message: err.message, path: typeof d.path === "string" ? d.path : undefined, form: typeof d.form === "string" ? d.form : undefined };
11
+ return { code, message: err.message, path: typeof d.path === "string" ? d.path : undefined, form: isScopeHomeForm(d.form) ? d.form : undefined };
13
12
  }
14
13
  export function logStoreFormRefusal(logger, refusal) {
15
14
  logger?.warn("memory_store_form_refused", {
@@ -14,9 +14,10 @@
14
14
  * · 会话规则写口(`session-policy-wire.ts`)与 SQL 会话规则店的写口(`assertSessionRuleNamesReachable`);
15
15
  * · 引擎 allow 层的条目表:请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT`、技能的两只清单 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代
16
16
  * `agents[].skills[]` 两处受理口)—— 经 `rule-entry-list.ts` 的 `emptyToolSegmentEntryOf`(本仓这一问;退役名 / 铸不出的判词那一半由引擎 `compileAllowList` 给)。
17
- * **不调它的面**(如实,未接):`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)、子代的工具名表
18
- * `agents[].allowTools` / `denyTools`(`spec-fields.ts` 只判形与条数)。在这两处写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中。把这些面接上判官 = 另一件事
19
- * (本版不做,也不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
17
+ * **不调它的面**:`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)—— 如实未接,
18
+ * 在那里写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中;把它接上判官 = 另一件事(不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
19
+ * 子代的工具名表 `agents[].allowTools` / `denyTools` 不是本文件的面:core 7.45.0 #1377 / #1385 起名字归引擎在开跑前一只判(三来源同判,`spec-fields.ts`
20
+ * 受理口只判形与条数),本仓不对它另做对照。
20
21
  * **精确名的工具段归一不在这里判**:包根没有导出工具段的铸法,不手抄 —— 精确名的工具段按字面收,本仓每张表上都不判它。
21
22
  *
22
23
  * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表与技能清单同读)。
@@ -356,7 +356,7 @@ export declare const FAIL_OPEN_TAGS: {
356
356
  };
357
357
  readonly "server.session-repo.corrupt-record-skipped": {
358
358
  readonly cls: "F";
359
- readonly note: "S-990(`plugins/store-backend.ts` 的 `FileSessionRepo` 座,local 车道的会话存档):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。";
359
+ readonly note: "S-990(`plugins/local-session-store.ts` `createLocalSessionRepo` 的 `FileSessionRepo` 座,local 车道的会话存档;`LocalSessionStore` 与会话视图口 `audit.ts` 共用,S-879):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。";
360
360
  };
361
361
  readonly "server.workflow-journal.corrupt-record-skipped": {
362
362
  readonly cls: "F";
@@ -390,6 +390,10 @@ export declare const FAIL_OPEN_TAGS: {
390
390
  readonly cls: "F";
391
391
  readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";
392
392
  };
393
+ readonly "server.snapshot-bucket.presence-unproven": {
394
+ readonly cls: "F";
395
+ readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
396
+ };
393
397
  };
394
398
  /** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
395
399
  export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
@@ -329,7 +329,7 @@ export const FAIL_OPEN_TAGS = {
329
329
  },
330
330
  "server.session-repo.corrupt-record-skipped": {
331
331
  cls: "F",
332
- note: "S-990(`plugins/store-backend.ts` 的 `FileSessionRepo` 座,local 车道的会话存档):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。",
332
+ note: "S-990(`plugins/local-session-store.ts` `createLocalSessionRepo` 的 `FileSessionRepo` 座,local 车道的会话存档;`LocalSessionStore` 与会话视图口 `audit.ts` 共用,S-879):一份会话存档里有记录读不出(整行毁 / 半截 JSON / 形不对,core 记成 `kind:\"record\"` 逐条披露)或存档文件 / 目录整体读不出(`kind:\"file\"`)⇒ core 的成文处置是跳过 + 披露,会话照常打开、列表少一行。最坏后果 = 模型读到的历史少了一截 / 列表静默少一份会话 —— 都是**可用性**损失,不开任何门;发起这一轮的人另有 wire 面披露(打开自己存档时的 `engine_notice` 码 `session.records_skipped`,与本 tag 无关、不动)⇒ F。**计数单位 = 一条被跳过的记录 = 一次命中**(`replay` 级是同一批记录的汇总,不计,S-616 同教训);**每命中一行探针**(`SEMA_FAILOPEN_PROBE`)带 `path` / `sessionId` / `line` / `form` / `reason` 逐条明细 —— 运维要看「哪份存档的哪一行坏了」读探针,不读本条 warn:本条是每进程每 tag 一次的告警(`detail` 是首条明细,不是全部)。修前:每命中一行 warn,一份 113 行坏记录的存档做一次列表 + 一次查看打出 342 行,存档没修好每读再发。",
333
333
  },
334
334
  "server.workflow-journal.corrupt-record-skipped": {
335
335
  cls: "F",
@@ -363,6 +363,10 @@ export const FAIL_OPEN_TAGS = {
363
363
  cls: "F",
364
364
  note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。",
365
365
  },
366
+ "server.snapshot-bucket.presence-unproven": {
367
+ cls: "F",
368
+ note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。",
369
+ },
366
370
  };
367
371
  export function failOpenTagForDroppedFrame(frameType) {
368
372
  switch (frameType) {
@@ -54,15 +54,16 @@ export declare function operatorAsk(rule: string, message: string): OperatorAskR
54
54
  * 任何 scope)退不掉,只有**这一次**的应答能答它。
55
55
  *
56
56
  * **为什么现在需要**:修前这两类 ask 挡住会话授权靠的是策略层的**次序**(`createDurableAskPolicy` 先判 neverAuto / 预算耗尽,再查
57
- * 豁免探针)。候裁 16 把会话授权的兑现位收到**审批席**(协调器 `standingGrantSettles`,判据 = `standingAnswerMayRetire`),而审批席
57
+ * 豁免探针)。候裁 16 把会话授权的兑现位收到**审批席**(协调器 `standingGrantSettles`,判据 = `standingAnswerHome`),而审批席
58
58
  * 读不到 `matchedAskRule`(core 刻意不上卡)—— 不盖出身,一条 `remember:"session"` 就会在审批席上退役「恒问人」。盖了之后
59
59
  * 席的同一只判据按 `mandated` 挡住它,耐久 `/decide` 腿的可记性(park 行 `mandated` 孪生位)同判 ⇒ 回执 `rememberApplied:false`。
60
60
  * live 腿修前这里本就是个洞(进程内 `allow_session` 授权可退役 neverAuto 卡),随类收掉。
61
61
  *
62
- * ⚠️ **只**这两类:普通 `APPROVAL_REQUIRE` 仍只经 {@link operatorAsk}(不盖出身)。它的问带 `matchedAskRule` ⇒ 引擎类 `ask_rule`;站着的答案
63
- * (「本会话放行」、耐久豁免行)清不清得掉它,出处是引擎类表 `foldArbitrationRow(classes).ruleMayClear` —— `ask_rule` 那一行说清不掉
64
- * (`tool-approval.ts` 的 `standingAnswerMayRetire` 与它同读)。所以在「站着的答案清不掉」这一问上它与「恒问人」两类同答;不同在出身位
65
- * (只这两类盖部署出身 ⇒ core 读 `operator_forced`、盖 `mandated`),其余各列按引擎类表各读各的行。
62
+ * ⚠️ **只**这两类:普通 `APPROVAL_REQUIRE` 仍只经 {@link operatorAsk}(不盖出身)。它的问带 `matchedAskRule` ⇒ 引擎类 `ask_rule`:bypass 放不掉、
63
+ * auto 分类器答不了(引擎眼里的免疫只靠这一位 —— core 7.44.0 的类表里 `operator_forced` 分类器**可答**,所以不拿出身位替它)。它是 server 自己的问,
64
+ * 它的站着的答案(「本会话放行」、耐久豁免行)由名单门在出问之前读 server 的会话豁免店(`approval.ts`),引擎的类表只裁引擎铸的问
65
+ * (`tool-approval.ts` 的 `standingAnswerHome` 按问的属主分读)。不盖出身还有第二个理由:出身位让引擎盖 `mandated`,卡面就成了「强制卡」——
66
+ * 名单卡要带「本会话放行」,而强制卡上没有。「恒问人」两类盖出身 ⇒ core 读 `operator_forced`、盖 `mandated`,站着的答案在哪一层都退不掉它们。
66
67
  */
67
68
  export declare function operatorAlwaysHumanAsk(rule: string, message: string): OperatorAskResult;
68
69
  /**
@@ -1,6 +1,10 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import type { Logger } from "../observability/logger.js";
4
+ /** The server's own audit event for a question a row answered without asking anyone — one name for both reading positions
5
+ * (the list gate: field `seat: "deployment_list"`; the approval seat: `seat: "approval_seat"`). Not a settlement record: nobody
6
+ * was asked, so it names no human / person / policy and carries no `remember`. */
7
+ export declare const APPROVAL_EXEMPTED_EVENT = "approval_exempted";
4
8
  export interface ApprovalExemptionRow {
5
9
  toolName: string;
6
10
  grantedBy: string | null;
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
3
  import { AppendLog, ensureDir, sanitizePathComponent } from "@sema-agent/core";
4
4
  import { replayJsonlGuarded } from "../observability/corrupt-read-seat.js";
5
+ export const APPROVAL_EXEMPTED_EVENT = "approval_exempted";
5
6
  export class TiDBApprovalExemptionStore {
6
7
  pool;
7
8
  constructor(pool) {
@@ -108,6 +108,15 @@ export interface MinioBlobBackendConfig {
108
108
  * 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec
109
109
  * 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */
110
110
  export declare const OBJECT_STORE_IO_TIMEOUT_MS = 120000;
111
+ /** 桶存在性探针的结果(闭集):`present` = 列举 2xx;`missing` = 404 且体里是 `NoSuchBucket`(**唯一**能证明缺桶的形);其余一切 = `unknown`(带一句原因,不回显体)。 */
112
+ export type BucketProbe = {
113
+ readonly state: "present";
114
+ } | {
115
+ readonly state: "missing";
116
+ } | {
117
+ readonly state: "unknown";
118
+ readonly detail: string;
119
+ };
111
120
  export declare class MinioBlobBackend implements BlobBackend {
112
121
  private readonly cfg;
113
122
  private readonly keyPrefix;
@@ -160,6 +169,10 @@ export declare class MinioBlobBackend implements BlobBackend {
160
169
  headObject(hash: string): Promise<{
161
170
  lastModifiedMs: number;
162
171
  } | undefined>;
172
+ /** 桶存在性探针(boot 期核**一次**,`plugins/snapshot-bucket-presence.ts`):presigned ListObjectsV2 `max-keys=1`(与 sweep 的列举同一条签名路径,零新依赖)。
173
+ * 🔴 只有「404 且体里是 `NoSuchBucket`」才算缺桶;网络不通 / 403(凭据不能列举 ≠ 桶不在)/ 5xx / 没有 NoSuchBucket 的 404(代理 / 路径错)
174
+ * 都**证明不了**缺桶 ⇒ `unknown`,调用方不拒启、只报一次。总函数:不抛。`timeoutMs`:boot 期调用方给的单次上界(黑洞端点不许把起服拖到 120s)。 */
175
+ probeBucket(timeoutMs?: number): Promise<BucketProbe>;
163
176
  /** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge
164
177
  * path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index
165
178
  * rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp
@@ -253,6 +253,32 @@ export class MinioBlobBackend {
253
253
  const ms = lm ? Date.parse(lm) : NaN;
254
254
  return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
255
255
  }
256
+ async probeBucket(timeoutMs = OBJECT_STORE_IO_TIMEOUT_MS) {
257
+ const f = this.cfg.fetchImpl ?? fetch;
258
+ const url = presignS3ListUrl({
259
+ endpoint: this.cfg.endpoint,
260
+ bucket: this.cfg.bucket,
261
+ prefix: this.keyPrefix,
262
+ maxKeys: 1,
263
+ accessKey: this.cfg.accessKey,
264
+ secretKey: this.cfg.secretKey,
265
+ ...(this.cfg.region ? { region: this.cfg.region } : {}),
266
+ expiresSec: this.ttl,
267
+ });
268
+ let res;
269
+ try {
270
+ res = await f(url, { method: "GET", signal: AbortSignal.timeout(timeoutMs) });
271
+ }
272
+ catch (err) {
273
+ return { state: "unknown", detail: `request failed: ${err instanceof Error ? err.message : String(err)}` };
274
+ }
275
+ if (res.ok)
276
+ return { state: "present" };
277
+ const body = await res.text().catch(() => "");
278
+ if (res.status === 404 && /<Code>NoSuchBucket<\/Code>/.test(body))
279
+ return { state: "missing" };
280
+ return { state: "unknown", detail: `HTTP ${res.status}` };
281
+ }
256
282
  async deleteBlobs(hashes) {
257
283
  if (hashes.length === 0)
258
284
  return 0;