@sema-agent/server 7.117.0 → 7.118.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 (98) hide show
  1. package/MIGRATION.md +97 -0
  2. package/USAGE.md +37 -18
  3. package/dist/approval-card.d.ts +40 -10
  4. package/dist/approval-card.js +11 -1
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/parked-revive-gate.d.ts +1 -7
  8. package/dist/boot/parked-revive-gate.js +2 -12
  9. package/dist/boot/resolve-spec.js +8 -13
  10. package/dist/boot/runner-deps.js +2 -2
  11. package/dist/boot/stage-07-capability-layer.js +2 -2
  12. package/dist/capabilities/hands-lane.d.ts +25 -0
  13. package/dist/capabilities/hands-lane.js +1 -1
  14. package/dist/capabilities/mcp-probe.d.ts +2 -2
  15. package/dist/config.d.ts +14 -18
  16. package/dist/config.js +13 -24
  17. package/dist/deployment-governance.d.ts +2 -7
  18. package/dist/deployment-governance.js +3 -5
  19. package/dist/host-decision.d.ts +83 -25
  20. package/dist/host-decision.js +31 -1
  21. package/dist/http/admission.js +7 -1
  22. package/dist/http/approval-sweeps.js +1 -1
  23. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  24. package/dist/http/mcp-dial-deadline.js +21 -0
  25. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  26. package/dist/http/memory-compliance-failure-reply.js +17 -8
  27. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  28. package/dist/http/memory-store-refusal-reply.js +16 -5
  29. package/dist/http/resume-legs.js +7 -3
  30. package/dist/http/routes/approvals-assistant.js +21 -24
  31. package/dist/http/routes/capabilities.js +2 -0
  32. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  33. package/dist/http/routes/mcp-reconnect.js +6 -1
  34. package/dist/http/routes/memory-bundle.js +1 -1
  35. package/dist/http/routes/memory-compliance.js +1 -1
  36. package/dist/http/routes/memory-origin.js +1 -1
  37. package/dist/http/routes/runs.js +41 -33
  38. package/dist/http/routes/sessions.d.ts +1 -1
  39. package/dist/http/routes/sessions.js +3 -6
  40. package/dist/http/routes/workflows.js +2 -0
  41. package/dist/http/wire-types.d.ts +6 -1
  42. package/dist/memory-store-refusal.d.ts +42 -17
  43. package/dist/memory-store-refusal.js +25 -6
  44. package/dist/model-unreachable-notice.d.ts +1 -1
  45. package/dist/namespaced-rule-name.d.ts +45 -41
  46. package/dist/namespaced-rule-name.js +36 -22
  47. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  48. package/dist/observability/corrupt-read-seat.js +1 -0
  49. package/dist/observability/fail-open.d.ts +14 -2
  50. package/dist/observability/fail-open.js +14 -2
  51. package/dist/operator-ask.d.ts +4 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  53. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  54. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  55. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  56. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  57. package/dist/orchestration/workflow-notify-journal.js +7 -2
  58. package/dist/outbound-dispatcher.d.ts +29 -20
  59. package/dist/outbound-dispatcher.js +105 -16
  60. package/dist/parked-decide.d.ts +43 -48
  61. package/dist/parked-decide.js +54 -16
  62. package/dist/permission-rule-vocab.d.ts +4 -3
  63. package/dist/permission-rule-vocab.js +1 -1
  64. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  65. package/dist/plugins/approval-ask-store-sql.js +0 -3
  66. package/dist/plugins/one-time-migrations.d.ts +5 -0
  67. package/dist/plugins/one-time-migrations.js +110 -4
  68. package/dist/plugins/pg-pool.js +1 -1
  69. package/dist/plugins/tidb-pool.js +1 -1
  70. package/dist/plugins/web-search.d.ts +20 -1
  71. package/dist/plugins/web-search.js +39 -10
  72. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  73. package/dist/plugins/workflow-run-store-sql.js +5 -7
  74. package/dist/request-key-closure.js +1 -1
  75. package/dist/request-value-closure.d.ts +1 -1
  76. package/dist/rule-entry-list.d.ts +35 -46
  77. package/dist/rule-entry-list.js +17 -25
  78. package/dist/rules-consent.js +3 -0
  79. package/dist/run-local.js +1 -0
  80. package/dist/runs.d.ts +1 -1
  81. package/dist/session-policy-wire.d.ts +13 -12
  82. package/dist/session-policy-wire.js +11 -6
  83. package/dist/spec-fields.d.ts +7 -9
  84. package/dist/spec-fields.js +4 -7
  85. package/dist/standing-answer-grant.d.ts +14 -2
  86. package/dist/standing-answer-grant.js +8 -1
  87. package/dist/task-settings.js +8 -4
  88. package/dist/tool-approval.d.ts +53 -112
  89. package/dist/tool-approval.js +75 -55
  90. package/dist/trace/core-keyset-guard.d.ts +5 -5
  91. package/dist/trace/self-minted-wire.d.ts +16 -2
  92. package/dist/trace/self-minted-wire.js +7 -0
  93. package/dist/user-input-ledger.d.ts +24 -12
  94. package/dist/user-input-ledger.js +33 -14
  95. package/package.json +2 -2
  96. package/scripts/drain-check-local.mjs +74 -3
  97. package/scripts/drain-check.mysql.sql +43 -17
  98. package/scripts/drain-check.pg.sql +46 -20
@@ -36,6 +36,23 @@
36
36
  * (`isParkedRunStatus` / `isTerminalRunStatus`)与 `runProvenElsewhere`(手抄已收编成一只谓词,S-430 起同住 `plugins/store-contracts.ts`)。
37
37
  * 显式 operator 可重拨任何租户的会话(监督动作,与 notify / steer 同臂;**不是** bare `isOperator`)。
38
38
  *
39
+ * ── 应答期限(S-431)────────────────────────────────────────────────────────────────────────────
40
+ * 修前这一口对 `live.reconnectMcpServer(server)` 是裸 `await`:server 侧没有期限,上界只来自 core 自己的启动期钟
41
+ * (env `MCP_TIMEOUT`,未设落 SDK 缺省 60s;拨号之前还有一次不在那条钟里的旧连接 `close()`),一台拨不通 / 半开的远端能把这一口
42
+ * HTTP 请求挂很久。现在与 `GET /v1/sessions/:id/mcp` 面板读**同一只**数 `MCP_DIAL_DEADLINE_MS`(`../mcp-dial-deadline.ts`),
43
+ * 到点答 **503 `state.mcp_reconnect_unsettled`**。
44
+ * 🔴 **只界应答,不取消重拨**:core 的 `TaskStream.reconnectMcpServer(server)` 不收 `AbortSignal`(d.ts 逐字:单参),
45
+ * 本面没有取消把手可递,也不伪造一个。到点之后重拨**继续跑**,落定时照常把名册写上这条 run(成功)或撤走(拨号失败)—— 所以
46
+ * 503 说的是「这一口没等到结果」,**不是**「失败」,文案与契约 §G.7c 都按这个读法写。重发**不作保证**:core 的在途守卫
47
+ * (`redialsInFlight`,`dist/core/mcp.js`)只罩**拨号本身**(关旧 + 连 + 列),拨号一落定就放开,而 `redialMcpServer`
48
+ * (`dist/core/runner/mcp-redial.js`)随后还要 `await commitMcpRosterChanges`(重建工具面)—— 那一段里守卫已不在,
49
+ * 此刻再发会开第二次「先关后拨」。所以文案只说「拨号还在跑时 core 会拒第二次」,不说「重发一定得 refused」,也不把重发
50
+ * 推荐成探针(对抗复审 [medium],验真后采纳;守卫延到名册提交之后是 core 的活,本仓不自造第二份在途判)。
51
+ * 为什么是 503 而不是 200 + 第四个 `outcome` 词:`outcome` 三词是 core 结果的投影,这里**没有结果**,编一个词就是替
52
+ * core 说话(与 `unsupported` 那一行「没有 core 结果就不铸 `prefix`」同一条纪律);503 是 `state.*` 族的唯一状态
53
+ * (附录 A 判据 3),与 `state.mcp_probe_incomplete` 同族同档,但**不复用**它的码 —— 那一码的语义钉在 probe 面上。
54
+ * 本面**不**带 `Retry-After`:重发不是无代价的重试(见上),一个会被通用重试臂自动执行的头在一次交易动词上是错的方向。
55
+ *
39
56
  * ── 计费/门 ─────────────────────────────────────────────────────────────────────────────────────
40
57
  * 零模型工作(不进 billable;不吃 drain / roster-pending);mutating ⇒ `rateLimited`。能力位 `mcpReconnect`
41
58
  * = 持久 run 账面 ∧ 会话属主面(两道 501 的同一批 deps 推导出来,「说 yes 但 501」结构上不可能)。
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { isParkedRunStatus, isTerminalRunStatus, runProvenElsewhere } from "../../plugins/store-contracts.js";
3
3
  import { redactSecrets } from "../../trace/redact.js";
4
+ import { DeadlineExceeded, MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
4
5
  import { sendJson, sendError } from "../send.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import {} from "../route-ctx.js";
@@ -99,11 +100,15 @@ export async function handleMcpReconnect(req, res, match, ctx) {
99
100
  const live = activeTaskId === undefined ? undefined : ctx.registry.steerableRuns.get(activeTaskId);
100
101
  if (live) {
101
102
  try {
102
- const result = await live.reconnectMcpServer(server);
103
+ const result = await raceDeadline(live.reconnectMcpServer(server), MCP_DIAL_DEADLINE_MS);
103
104
  sendJson(res, 200, { taskId: activeTaskId, sessionId, ...projectResult(result) });
104
105
  return;
105
106
  }
106
107
  catch (e) {
108
+ if (e instanceof DeadlineExceeded) {
109
+ sendError(res, 503, "state.mcp_reconnect_unsettled", "the MCP re-dial did not settle within the time this endpoint waits, so there is no outcome to report — the re-dial itself was NOT cancelled (the engine takes no cancellation for it) and may still finish, so do not assume it failed; do not resend blindly either: the engine refuses a second re-dial of the same server only while the dial itself is still running, and a request sent after that point starts another close-then-dial on a server that may now be healthy, so check the server's state before re-sending");
110
+ return;
111
+ }
107
112
  const code = e.code;
108
113
  const msg = e instanceof Error ? e.message : String(e);
109
114
  switch (code) {
@@ -37,7 +37,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
37
37
  const code = codeOf(err);
38
38
  if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
39
39
  return true;
40
- if (sentMemoryStoreRefusal(res, err))
40
+ if (sentMemoryStoreRefusal(res, err, ctx.deps.logger))
41
41
  return true;
42
42
  ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
43
43
  return false;
@@ -12,7 +12,7 @@ const EraseRequestSchema = z.object({
12
12
  allowUnevidenced: z.boolean().optional(),
13
13
  });
14
14
  function sentClassifiedFailure(res, ctx, face, err) {
15
- if (sentMemoryComplianceFailure(res, err, { kind: "operator" }))
15
+ if (sentMemoryComplianceFailure(res, err, { kind: "operator" }, ctx.deps.logger))
16
16
  return true;
17
17
  ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
18
18
  return false;
@@ -54,7 +54,7 @@ function sentClassifiedFailure(res, ctx, face, err) {
54
54
  if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
55
55
  return true;
56
56
  }
57
- if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
57
+ if (face !== "clearances" && sentMemoryStoreRefusal(res, err, ctx.deps.logger))
58
58
  return true;
59
59
  if (err instanceof ControlPlaneCorruptError) {
60
60
  sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
@@ -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 } from "@sema-agent/core";
4
+ import { uuidv7, mintCheckpointToken, CheckpointError, 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";
@@ -14,13 +14,16 @@ import { createHash } from "node:crypto";
14
14
  import { scopedIdempotencyKey } from "../idempotency.js";
15
15
  import { streamSseLog } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
- import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
18
- import { ASK_DECISION_CONSUMER_ABSENT, SETTLED_BY_DENY_ONLY, SETTLED_BY_MUST_BE_STRING, SETTLED_BY_TOO_WIDE, judgeSettledByBody, outcomeOfDecidedRow, resolveStreamApprovalGate, respondSettledBy } from "../../tool-approval.js";
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";
19
+ import { ASK_DECISION_CONSUMER_ABSENT, outcomeOfDecidedRow, resolveStreamApprovalGate } from "../../tool-approval.js";
19
20
  import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
20
21
  import { normalizeRunEventType, screenedStoredEventData } from "../../trace/project.js";
21
22
  import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
22
23
  import { readTurnActivityMs } from "../../turn-activity.js";
23
24
  import { mintCancelledResult } from "../../run-cancel-context.js";
25
+ import { userInputLaneOf } from "../../user-input-ledger.js";
26
+ import { INJECTION_TIERS } from "../../trace/injection-tier.js";
24
27
  import { buildCrossSliceUsage, readResourceWindow } from "../../resource-window.js";
25
28
  import { headerStr, gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
26
29
  import { ActorAssertionWire } from "@sema-agent/settings-schema";
@@ -31,6 +34,9 @@ import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
31
34
  import { withPrincipal } from "../../observability/principal-context.js";
32
35
  import { readResumeCtx } from "../resume-ctx-read.js";
33
36
  import { storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "../checkpoint-ctx.js";
37
+ const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
38
+ const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
39
+ const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
34
40
  const ASK_PARKING_RETRY_AFTER_SEC = 2;
35
41
  const ASK_ACTOR_LABEL_MAX = 256;
36
42
  const AskDecisionActorSchema = ActorAssertionWire.strip();
@@ -46,7 +52,7 @@ const AskDecisionBodySchema = z
46
52
  .refine((k) => k.trim() === k, { message: "idempotency key must not have leading/trailing whitespace" })
47
53
  .optional(),
48
54
  actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
49
- settledBy: z.unknown().optional(),
55
+ decidedBy: z.unknown().optional(),
50
56
  })
51
57
  .strict();
52
58
  const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
@@ -136,11 +142,16 @@ async function streamRunEvents(req, res, deps, runStore, taskId, staleMs) {
136
142
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
137
143
  }, taskId, staleMs);
138
144
  }
145
+ function sendSteerReplayRefusal(res, refusal) {
146
+ const detail = refusal?.detail;
147
+ const differs = detail !== null && typeof detail === "object" ? readClosedWordSeat(detail, "differs") : undefined;
148
+ sendError(res, 409, "steering.duplicate_input_id", refusal instanceof Error ? refusal.message : "this steering input id was already accepted with different values", differs !== undefined ? { detail: { differs } } : undefined);
149
+ }
139
150
  async function acceptLiveUserInput(deps, res, at) {
140
151
  if (!deps.userInputLedger)
141
152
  return undefined;
142
153
  try {
143
- return await deps.userInputLedger.accept({ input: "steer", messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
154
+ return await deps.userInputLedger.accept({ input: userInputLaneOf(at.tier), messageId: at.messageId, sessionId: at.sessionId, taskId: at.taskId, leg: at.leg });
144
155
  }
145
156
  catch (e) {
146
157
  deps.logger?.warn?.("user_input_ledger_accept_failed", { taskId: at.taskId, sessionId: at.sessionId, err: e instanceof Error ? e.message : String(e) });
@@ -429,15 +440,15 @@ export async function handleRuns(req, res, match, ctx) {
429
440
  return;
430
441
  }
431
442
  if (typeof body.text !== "string" || body.text.length === 0) {
432
- sendError(res, 400, "request.body_shape", "body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: 'now' | 'next' | 'later' }");
443
+ sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
433
444
  return;
434
445
  }
435
446
  if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
436
447
  sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
437
448
  return;
438
449
  }
439
- if (body.priority !== undefined && body.priority !== "now" && body.priority !== "next" && body.priority !== "later") {
440
- sendError(res, 400, "request.field_invalid", "priority must be 'now', 'next', or 'later' when present");
450
+ if (body.priority !== undefined && !isSystemInjectionPriority(body.priority)) {
451
+ sendError(res, 400, "request.field_invalid", `priority must be ${STEER_PRIORITY_CHOICES} when present`);
441
452
  return;
442
453
  }
443
454
  const text = body.text;
@@ -495,19 +506,19 @@ export async function handleRuns(req, res, match, ctx) {
495
506
  return "queue-full";
496
507
  }
497
508
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
498
- parkDuplicateKey = e.message;
509
+ parkDuplicateKey = e;
499
510
  return "duplicate-key";
500
511
  }
501
512
  throw e;
502
513
  }
503
514
  };
504
515
  const sendQueueFull = () => sendError(res, 409, "steering.queue_full", parkQueueFull ?? "the parked steering queue on this checkpoint is full");
505
- const sendDuplicateKey = () => sendError(res, 409, "steering.duplicate_input_id", parkDuplicateKey ?? "this Idempotency-Key is already parked with different steering content — reissue with a fresh key");
516
+ const sendDuplicateKey = () => sendSteerReplayRefusal(res, parkDuplicateKey);
506
517
  const sendParked = (status) => sendJson(res, 202, { taskId, status, delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
507
518
  const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
508
519
  const live = steerableRuns.get(taskId);
509
520
  if (live) {
510
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live });
521
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId, leg: live, tier: priority });
511
522
  if (acceptance === "refused")
512
523
  return;
513
524
  try {
@@ -526,7 +537,7 @@ export async function handleRuns(req, res, match, ctx) {
526
537
  return;
527
538
  }
528
539
  if (code === "steering.duplicate_input_id") {
529
- parkDuplicateKey = e instanceof Error ? e.message : undefined;
540
+ parkDuplicateKey = e;
530
541
  sendDuplicateKey();
531
542
  return;
532
543
  }
@@ -601,7 +612,7 @@ export async function handleRuns(req, res, match, ctx) {
601
612
  return;
602
613
  }
603
614
  if (e instanceof CheckpointError && e.code === "steering.duplicate_input_id") {
604
- parkDuplicateKey = e.message;
615
+ parkDuplicateKey = e;
605
616
  mintResult.duplicateKey = true;
606
617
  return;
607
618
  }
@@ -831,7 +842,7 @@ export async function handleRuns(req, res, match, ctx) {
831
842
  });
832
843
  return;
833
844
  }
834
- const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream });
845
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId: interruptMessageId, sessionId: run.sessionId, taskId, leg: liveStream, tier: "now" });
835
846
  if (acceptance === "refused")
836
847
  return;
837
848
  try {
@@ -848,7 +859,7 @@ export async function handleRuns(req, res, match, ctx) {
848
859
  return;
849
860
  }
850
861
  if (code === "steering.duplicate_input_id") {
851
- sendError(res, 409, "steering.duplicate_input_id", e instanceof Error ? e.message : "this Idempotency-Key was already accepted with different content or at a different tier — reissue with a fresh key");
862
+ sendSteerReplayRefusal(res, e);
852
863
  return;
853
864
  }
854
865
  if (code !== "steering.not_running")
@@ -1221,7 +1232,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1221
1232
  return;
1222
1233
  }
1223
1234
  const messageId = uuidv7();
1224
- const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle });
1235
+ const acceptance = await acceptLiveUserInput(deps, res, { messageId, sessionId: run.sessionId, taskId: runId, leg: handle, tier: undefined });
1225
1236
  if (acceptance === "refused")
1226
1237
  return;
1227
1238
  const marker = await relayAcceptedInput(deps, acceptance, () => handle.steer(redacted));
@@ -1361,33 +1372,26 @@ export async function handleRunVerbs(req, res, match, ctx) {
1361
1372
  }
1362
1373
  const parsedBody = AskDecisionBodySchema.safeParse(rawBody);
1363
1374
  if (!parsedBody.success) {
1364
- sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / settledBy");
1375
+ sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
1365
1376
  return;
1366
1377
  }
1367
1378
  const body = parsedBody.data;
1368
- const settledByVerdict = judgeSettledByBody(body.settledBy, body.decision, deps.config.directDoorActive);
1369
- switch (settledByVerdict.kind) {
1379
+ const decidedByReading = readDecidedBy(body.decidedBy, deps.config.directDoorActive);
1380
+ switch (decidedByReading.kind) {
1370
1381
  case "not_in_proof":
1371
- sendError(res, 400, "settled_by_not_in_proof", "settledBy is not supported on a direct-door worker (not covered by the decision proof)");
1372
- return;
1373
- case "deny_only":
1374
- sendError(res, 400, "request.field_conflict", SETTLED_BY_DENY_ONLY);
1375
- return;
1376
- case "not_string":
1377
- sendError(res, 400, "request.field_invalid", SETTLED_BY_MUST_BE_STRING);
1382
+ sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
1378
1383
  return;
1379
- case "too_wide":
1380
- sendError(res, 400, "request.field_invalid", SETTLED_BY_TOO_WIDE);
1384
+ case "outside_closed_set":
1385
+ sendError(res, 400, "request.field_invalid", DECIDED_BY_OUTSIDE_CLOSED_SET);
1381
1386
  return;
1382
- case "absent":
1383
1387
  case "accepted":
1384
1388
  break;
1385
1389
  default: {
1386
- const unreachable = settledByVerdict;
1387
- throw new Error(`unhandled settledBy verdict: ${JSON.stringify(unreachable)}`);
1390
+ const unreachable = decidedByReading;
1391
+ throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
1388
1392
  }
1389
1393
  }
1390
- const settledBy = settledByVerdict.kind === "accepted" ? settledByVerdict.settledBy : undefined;
1394
+ const decidedBy = decidedByReading.decidedBy;
1391
1395
  const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
1392
1396
  const taskId = match.params.taskId;
1393
1397
  const askId = match.params.askId;
@@ -1513,6 +1517,10 @@ export async function handleRunVerbs(req, res, match, ctx) {
1513
1517
  sendTerminal(row0);
1514
1518
  return;
1515
1519
  }
1520
+ if (policyCannotAllow(decidedBy, body.decision === "approve", storedCardDemandsAPerson(row0.cardJson))) {
1521
+ sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK);
1522
+ return;
1523
+ }
1516
1524
  let outcome;
1517
1525
  const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
1518
1526
  try {
@@ -1521,7 +1529,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
1521
1529
  decisionActor: actor,
1522
1530
  decisionNote: body.note ?? null,
1523
1531
  idempotencyKey: body.idempotencyKey ?? null,
1524
- ...(settledBy !== undefined ? { settledBy: respondSettledBy({ settledBy }) } : {}),
1532
+ ...((sb) => (sb !== undefined ? { settledBy: sb } : {}))(liveSettledBy(decidedBy)),
1525
1533
  ...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
1526
1534
  });
1527
1535
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 单域独占状态(lens1 §C2 的 12 条里的 5 条)全部收进 `SessionsLocal`,每个 server 实例一份:
8
8
  * SSE 连接坑计数 `sseConnections`、owner 查探许可 `sseOwnerLookups`、`/mcp` 面板的短 TTL single-flight
9
- * 缓存 `mcpStatusCache` 与它的两个时限常量。
9
+ * 缓存 `mcpStatusCache` 与它的时限常量(应答期限 `MCP_DIAL_DEADLINE_MS` 与 `POST …/mcp/reconnect` 共读,见 `../mcp-dial-deadline.ts`)。
10
10
  *
11
11
  * ⚠️ 搬运例外(仅此一处,语义等价):两个原为闭包 `let` 的计数器进了 local 盒子,于是 6 个使用点写成
12
12
  * `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。
@@ -4,6 +4,7 @@ import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js
4
4
  import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
+ import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
7
8
  import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
8
9
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
9
10
  import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
@@ -13,7 +14,6 @@ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
13
14
  import * as facts from "../../capability-facts.js";
14
15
  import { rosterAgentId } from "./agents-roster.js";
15
16
  const MCP_STATUS_TTL_MS = 5_000;
16
- const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
17
17
  const A2A_STATUS_TTL_MS = 5_000;
18
18
  const A2A_MATERIALIZE_TIMEOUT_MS = 10_000;
19
19
  const MEMORY_STATUS_TTL_MS = 5_000;
@@ -154,10 +154,7 @@ export async function handleSessions(req, res, match, ctx) {
154
154
  mcpStatusCache.clear();
155
155
  const panel = (async () => {
156
156
  const matP = materializeMcpTools(specs, principal, undefined, undefined, undefined, deps.mcpRevocations);
157
- let timer;
158
- const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), MCP_MATERIALIZE_TIMEOUT_MS); })]);
159
- if (timer)
160
- clearTimeout(timer);
157
+ const mat = await raceDeadline(matP, MCP_DIAL_DEADLINE_MS).catch(() => null);
161
158
  if (!mat) {
162
159
  void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
163
160
  return { servers: [], asOf: new Date().toISOString(), degraded: true };
@@ -530,7 +527,7 @@ export async function handleSessions(req, res, match, ctx) {
530
527
  }
531
528
  catch (err) {
532
529
  const withheld = (coreMessage) => deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: coreMessage });
533
- if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }))
530
+ if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }, deps.logger))
534
531
  return;
535
532
  deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
536
533
  sendError(res, 500, "internal.error", "session memory erasure failed");
@@ -254,6 +254,8 @@ function projectParkRow(p) {
254
254
  sessionId: p.sessionId,
255
255
  originRunId: p.originRunId,
256
256
  ...(p.originUnconfirmed === true ? { originUnconfirmed: true } : {}),
257
+ ...(p.parkedBy !== undefined ? { parkedBy: p.parkedBy } : {}),
258
+ ...(p.generation !== undefined ? { generation: p.generation } : {}),
257
259
  };
258
260
  }
259
261
  function summarizeWorkflowDetail(run) {
@@ -6,7 +6,7 @@
6
6
  * cut: ONLY TaskRequestBody (its field types are inline or core imports — no other server.ts-local satellites);
7
7
  * sibling request/response shapes can migrate here later on the same pattern.
8
8
  */
9
- import type { A2aServerSpec, AgentDefinition, McpServerSpec, SkillSpec } from "@sema-agent/core";
9
+ import type { A2aServerSpec, AgentDefinition, ApprovalReach, McpServerSpec, SkillSpec } from "@sema-agent/core";
10
10
  import type { SettingsPermissionMode } from "../task-settings.js";
11
11
  import type { wiringManifestFaceEventData } from "../trace/project.js";
12
12
  import type { buildSyncSubmitBody } from "./sync-submit-body.js";
@@ -466,6 +466,11 @@ export interface TaskRequestBody {
466
466
  * typeof read. Declared per [ref] 件1 ([ref] boundary-must-schema): the submit chain reads this key, and an
467
467
  * undeclared read is a bare cast the exported type cannot audit (embedders could not even write it). */
468
468
  interactiveTools?: boolean;
469
+ /** core 7.43.0 `TaskSpec.approvalReach`: the run declares that NOBODY on it can be asked to approve (the only word: `"none"`; closed set and
470
+ * predicate are core's `APPROVAL_REACH` / `isApprovalReach`). Passed to the spec untranslated (boot/resolve-spec.ts) and to the per-leg
471
+ * interactive-posture judgment (an interactive posture beside it is refused by the engine); absent ⇒ key omitted (byte-identical); an
472
+ * unreadable value is a 400 on submit (admission.ts). Not checkpointed by the engine ⇒ resume legs re-supply it from the persisted body. */
473
+ approvalReach?: ApprovalReach;
469
474
  /** The caller's per-request settings stamp (the client-side `SemaSettings` shape). The KEY SET is deliberately
470
475
  * NOT mirrored here — a hand-written second shape on this interface would be the classic mirror-drift form.
471
476
  * Per-key ownership is split, not single: `parseTaskSettings` (src/task-settings.ts) defensively owns the
@@ -1,18 +1,21 @@
1
1
  /**
2
- * core 7.31.1 #1094 —— **`memory.target_outside_store`** 在本仓的唯一读法属主(纯叶:零 http / 零 boot 依赖)。
3
- *
4
- * ## 这个码说的是什么(core `docs/wire-consumption/7.31.1.md` 逐 face 表,tag v7.31.1 亲读)
5
- * 记忆 store 里的一个目录经软链通到了 store **外**(或 store 根在 backend 打开 / 引擎首次挂载之后被改指)⇒ 引擎对落点在外的
6
- * 删 / 写 / 重放 / 自身写一律**不做**,抛普通 `Error` + `.code`(文案以码领头,点名动作、路径、解析到哪)。core 的读法原话:
7
- * operator 要处置的拒(去掉那只目录链),**不是**可重试的 IO 故障,也**不是**对调用方的裁决;链去掉之前重发恒拒。
2
+ * **店形拒**(store-form refusal)在本仓的唯一读法属主(纯叶:零 http / 零 boot 依赖)—— core 一只铸点(`layout.ts` `storeFormRefusal`)的**两码一族**:
3
+ * · `memory.target_outside_store`(core 7.31.1 #1094):记忆 store 里的一个目录经软链通到了 store **外**(或 store 根在 backend 打开 / 引擎首次
4
+ * 挂载之后被改指)⇒ 引擎对落点在外的删 / 写 / 重放 / 自身写一律**不做**;
5
+ * · `memory.scope_home_not_directory`(core 7.44.0 #1368):一只 scope home(`<memoryDir>/<label>`)不是真目录 —— 被换成软链 / 普通文件 / 特殊文件
6
+ * (`detail.form` ∈ `symlink` | `file` | `other`,`lstat` 判、不跟链)。修前 core 对这一形抛不带码的 `ControlPlaneCorruptError`(与真控制面损坏同类同形),
7
+ * 平铺布局(条目直接住在 home 里)于是答 500,而同一换法在嵌套布局下一层答 409 —— 现在两布局同答这一码。
8
+ * 两码的形相同:普通 `Error` + `.code` + `.detail`(`{ path }`;home 那一码多 `form`),**句子以码领头、不含任何宿主路径**(core 7.44.0 起,
9
+ * 路径只在 `detail.path`)。core 的读法:operator 要处置的盘形(去掉那只链 / 放回真目录),**不是**可重试的 IO 故障,也**不是**对调用方的裁决;
10
+ * 盘形修好之前重发恒拒。本仓据此一条规则:两码同族、409、码原样;路径只进运维行(HTTP 映射一处,见 `http/memory-store-refusal-reply.ts`)。
8
11
  *
9
12
  * ## 两条到达车道,两个读者(本模块各给一只,别处不许再写第二份判据)
10
13
  * ① **抛出面**(抹除 / 出处 / 外源列表 / 清标 / bundle 导出导入 —— 凡触 store 树的 backend 面):`.code` 就是码
11
- * ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,operator 面一处字面量)与
14
+ * ⇒ {@link storeFormRefusalOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,operator 面一处字面量)与
12
15
  * `http/memory-compliance-failure-reply.ts`(合规面两受众的那一只;会话所有者受众换固定句,S-930)。
13
16
  * ② **报告面**(harvest `incident`):core 契约写明 report 车道上码**只在文本里** —— `sidecar_corrupt` 的 detail 恰两种形
14
- * `memory commit refused: memory.target_outside_store: …` / `memory control plane refused: memory.target_outside_store: …`
15
- * (core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字)⇒ {@link targetOutsideStoreInIncident} 只认**这两个 lead 紧跟码**
17
+ * `memory commit refused: <码>: …` / `memory control plane refused: <码>: …`(core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字;
18
+ * 7.44.0 安装树 `engine.js` 两处 incident 铸点同形,home 那一码经同一只 `writeFailureText` 到这里)⇒ {@link storeFormRefusalInIncident} 只认**这两个 lead 紧跟码**
16
19
  * 的位置:lead 与码都是 core 铸的字面,路径与引用数据全在码之后。🔴 不做全文子串找(codex 一审 C2:一条真控制面损坏若在
17
20
  * 它引用的坏数据里恰好带着这串字,子串找会把它标成目录链 —— 两者运维动作相反)。不读散文、不猜语义;结构化码位
18
21
  * (`HarvestReport.incident` / `.warnings` 带码)是向 core 索取的欠账。
@@ -20,19 +23,41 @@
20
23
  * 那几行的前缀是**条目路径**(模型可写)—— 本仓今天不读 warnings 车道(既有面,不是本批回归),也不在模型可写的文本上
21
24
  * 找码;这一半候 core 的结构化码位(登记在案,codex 一审 C3)。
22
25
  *
23
- * ## 不是这个码的三形(core 同表「different surface」,本仓**不**自拼码,如实走各自既有臂)
24
- * · journal 重放目标目录成链 ⇒ `ControlPlaneCorruptError`(无 `.code`,KL-1094-b)⇒ 路由 typed 500 `internal.memory_control_plane_corrupt`;
26
+ * ## 不是这一族的三形(core 同表「different surface」,本仓**不**自拼码,如实走各自既有臂)
27
+ * · journal 重放目标目录成链 ⇒ `ControlPlaneCorruptError`(无 `.code`,KL-1094-b)⇒ 路由 typed 500 `internal.memory_control_plane_corrupt`
28
+ * (core 7.44.0 起 scope home 非目录**不再**走这一类 —— 它有了自己的码,见上;`ControlPlaneCorruptError` 只剩真控制面损坏);
25
29
  * · 引擎原地写撞硬链(nlink>1)⇒ 文本 `has N names (a hard link)`、无码(KL-1094-g / #1105)⇒ 走该写点既有的失败车道(harvest
26
30
  * warnings / 挂载失败),抛出面够不着;
27
31
  * · 解析故障(`EACCES` / `ELOOP` / `ENOTDIR`)⇒ 以 errno 本身抛 ⇒ 路由未分类 500 + 运维 warn(`String(err)` 带 core 补救句)。
32
+ *
33
+ * ## 运维行也在这里铸(两受众同一只,{@link logStoreFormRefusal})
34
+ * 路径不上 wire,只进这一条运维 warn —— 它是**日志行**不是 wire 错误体,所以铸点跟读法属主住在一起(不住 `http/`:`http/` 全树门
35
+ * `test/error-code-key-gate.test.ts` 咬的是错误体里的 `code:` 键位,日志字段名不该借 wire 层的目录去过它)。
28
36
  */
29
- /** core 的码(`src/core/memory-engine/layout.ts` `TARGET_OUTSIDE_STORE`;core 不在包根导出它,本常量是它在本仓的唯一写点)。 */
37
+ import type { Logger } from "./observability/logger.js";
38
+ /** core 的两码(`dist/core/memory-engine/layout.js` `TARGET_OUTSIDE_STORE` / `SCOPE_HOME_NOT_DIRECTORY`;core 不在包根导出它们,这两个常量是它们在本仓的唯一写点)。 */
30
39
  export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
31
- /** 抛出面:`err` 是这一拒 ⇒ 原样交回(已窄化成 `Error`,调用方要它的 `message`);否则 `undefined`。形 = core 的 `Error` + `.code`。 */
32
- export declare function targetOutsideStoreOf(err: unknown): Error | undefined;
33
- /** 报告面:harvest `incident` 是这一拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成链)。 */
34
- export declare function targetOutsideStoreInIncident(incident: {
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
+ /** 一次店形拒的读数(纯数据)。`path` / `form` 取自 core 的 `detail`(读不出 ⇒ `undefined`,不猜、不从句子里抠)。 */
45
+ export interface StoreFormRefusal {
46
+ readonly code: StoreFormRefusalCode;
47
+ /** core 原句(以码领头;7.44.0 起不含宿主路径)。 */
48
+ readonly message: string;
49
+ /** `detail.path`:落点 / 被改指的根 / 不是目录的 scope home —— **只给运维行**。 */
50
+ readonly path: string | undefined;
51
+ /** `detail.form`(只在 home 那一码上):原样字符串,不镜像 core 的闭集(本仓只把它写进运维行,不按它分支)。 */
52
+ readonly form: string | undefined;
53
+ }
54
+ /** 抛出面:`err` 是店形拒 ⇒ 读数;否则 `undefined`。形 = core 的 `Error` + `.code` + `.detail`。 */
55
+ export declare function storeFormRefusalOf(err: unknown): StoreFormRefusal | undefined;
56
+ /** 两受众共用的运维行(一处铸点):码 + core `detail` 里的宿主路径与 home 形。`logger` 缺席(只有不接 logger 的测试装配)⇒ 不落。 */
57
+ export declare function logStoreFormRefusal(logger: Pick<Logger, "warn"> | undefined, refusal: StoreFormRefusal): void;
58
+ /** 报告面:harvest `incident` 是这一族拒 ⇒ 码;否则 `undefined`(真控制面损坏的 `sidecar_corrupt` 走这一臂 —— 不被说成店形拒)。 */
59
+ export declare function storeFormRefusalInIncident(incident: {
35
60
  readonly kind: string;
36
61
  readonly detail: string;
37
- }): typeof TARGET_OUTSIDE_STORE | undefined;
62
+ }): StoreFormRefusalCode | undefined;
38
63
  //# sourceMappingURL=memory-store-refusal.d.ts.map
@@ -1,14 +1,33 @@
1
1
  export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
2
- export function targetOutsideStoreOf(err) {
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);
5
+ export function storeFormRefusalOf(err) {
3
6
  if (!(err instanceof Error))
4
7
  return undefined;
5
- const { code } = err;
6
- return code === TARGET_OUTSIDE_STORE ? err : undefined;
8
+ const { code, detail } = err;
9
+ if (!isStoreFormRefusalCode(code))
10
+ return undefined;
11
+ 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 };
13
+ }
14
+ export function logStoreFormRefusal(logger, refusal) {
15
+ logger?.warn("memory_store_form_refused", {
16
+ code: refusal.code,
17
+ ...(refusal.path !== undefined ? { path: refusal.path } : {}),
18
+ ...(refusal.form !== undefined ? { form: refusal.form } : {}),
19
+ });
7
20
  }
8
- const INCIDENT_PREFIXES = [`memory commit refused: ${TARGET_OUTSIDE_STORE}: `, `memory control plane refused: ${TARGET_OUTSIDE_STORE}: `];
9
- export function targetOutsideStoreInIncident(incident) {
21
+ const INCIDENT_LEADS = ["memory commit refused: ", "memory control plane refused: "];
22
+ export function storeFormRefusalInIncident(incident) {
10
23
  if (incident.kind !== "sidecar_corrupt")
11
24
  return undefined;
12
- return INCIDENT_PREFIXES.some((p) => incident.detail.startsWith(p)) ? TARGET_OUTSIDE_STORE : undefined;
25
+ for (const lead of INCIDENT_LEADS) {
26
+ if (!incident.detail.startsWith(lead))
27
+ continue;
28
+ const rest = incident.detail.slice(lead.length);
29
+ return STORE_FORM_REFUSAL_CODES.find((code) => rest.startsWith(`${code}: `));
30
+ }
31
+ return undefined;
13
32
  }
14
33
  //# sourceMappingURL=memory-store-refusal.js.map
@@ -23,7 +23,7 @@
23
23
  * 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId }` ——
24
24
  * `proxyInUse` 只在这一跳走了代理时在场,且只是**变量名**(代理地址是基础设施细节,不上租户 wire,与 `OutboundProxyError` 的
25
25
  * F3 裁定同一条)。`targetHost`(模型端点的 `host:port`)对 user 受众是**新披露的部署事实**(F7 裁定:它是部署自己声明的模型端点,
26
- * 不是基础设施秘密,可以上;代理地址仍不上)—— 不要读成「终局句里本就有」:代理形下 core 终局句展开到代理根因、不含模型主机,
26
+ * 不是基础设施秘密,可以上;代理地址仍不上)—— 不要读成「终局句里本就有」:代理形下终局句只带码与 `via outbound proxy (from <变量>)`、不含任何主机地址,
27
27
  * 辅助调用形下 run 可 completed、租户面本无主机句。
28
28
  */
29
29
  import type { Brain } from "@sema-agent/core";