@sema-agent/server 7.67.0 → 7.68.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.
@@ -367,7 +367,7 @@ export function createResolveSpec(ctx) {
367
367
  return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
368
368
  })(cap.tools),
369
369
  skills: mergeUserSkills(cap.skills, body.skills, logger),
370
- mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
370
+ mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
371
371
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
372
372
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
373
373
  toolPolicy: toolPolicyForHands(hands, durableEnabled
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -122,6 +122,10 @@ export interface PreparedTaskSubmission {
122
122
  cascade?: boolean;
123
123
  jobId?: string;
124
124
  body: TaskRequestBody;
125
+ /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天只有请求腿 MCP 注入被丢弃一族)。
126
+ * 装配跑在这条腿的通告口开张之前,所以铸点交不出去 —— 由腿在 `registerEngineNoticeLeg` 那一拍
127
+ * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
128
+ engineNotices?: readonly EngineNotice[];
125
129
  }
126
130
  /** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
127
131
  export interface DriveResumeArgs {
@@ -1609,7 +1609,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1609
1609
  windowMs: deps.config.streamApproval?.windowMs,
1610
1610
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1611
1611
  }
1612
- : undefined, deps.checkpointStore);
1612
+ : undefined, deps.checkpointStore, prepared.engineNotices);
1613
1613
  return { status: 202, body: { taskId, sessionId, status: "running" } };
1614
1614
  }, (r) => r.status === 202);
1615
1615
  return resp;
@@ -299,6 +299,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
299
299
  const noticeLedger = ledgerSink;
300
300
  const unregisterEngineNotice = registerEngineNoticeLeg({
301
301
  sessionId: prepared.spec.sessionId,
302
+ ...(prepared.engineNotices !== undefined ? { pending: prepared.engineNotices } : {}),
302
303
  live: (row) => {
303
304
  if (res.writableEnded || res.destroyed) {
304
305
  recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import type { IncomingMessage } from "node:http";
3
- import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain } from "@sema-agent/core";
3
+ import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
4
4
  import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
@@ -92,9 +92,14 @@ export interface ServiceCoreDeps {
92
92
  * `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture ——
93
93
  * bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。
94
94
  * ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */
95
+ /** `opts.onNotice` (L-167, core 7.9.1):装配期铸下的、**这条会话**的用户面通告(今天只有一族:请求腿
96
+ * MCP 注入被丢弃)。装配跑在通告腿开口**之前**(`prepareSpec` → 路由注册 `registerEngineNoticeLeg`),
97
+ * 所以它不能在铸点直接 `route()` ——收下来交给调用腿,由腿在开口那一拍连同 `pending` 一起投。
98
+ * 缺席(桩、resume 家族)⇒ 只走结构化日志终点,行为逐字不变。 */
95
99
  resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
96
100
  leg?: "fresh" | "resume";
97
101
  liveStream?: boolean;
102
+ onNotice?: (notice: EngineNotice) => void;
98
103
  }) => TaskSpec | Promise<TaskSpec>;
99
104
  /**
100
105
  * [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
@@ -2,7 +2,7 @@ import http from "node:http";
2
2
  import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
3
3
  import { once } from "node:events";
4
4
  import { createHash } from "node:crypto";
5
- import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, RUN_WORKFLOW_TOOL_NAME, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
5
+ import { uuidv7, isThinkingLevel, expandTiers, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, subscribeWorkflow, GLOBAL_USAGE_KEY, usageRetryAfterMs, terminalProjection, PAUSE_REGISTRY } from "@sema-agent/core";
6
6
  import { decideParkedAgent, findParkedAgentForCheckpoint } from "../parked-decide.js";
7
7
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
8
8
  import {} from "../config-center/facade.js";
@@ -24,7 +24,6 @@ import { publicStoreProbeError } from "../store-live-probe.js";
24
24
  import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
25
25
  import {} from "../orchestration/workflow-agent-steer.js";
26
26
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
27
- import { HANDS_BAND_TOOL_NAMES } from "../capabilities/hands-lane.js";
28
27
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
29
28
  import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
30
29
  import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
@@ -119,7 +118,6 @@ const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
119
118
  const MAX_BODY = 8 * 1024 * 1024;
120
119
  const MAX_IMAGES_PER_REQUEST = 20;
121
120
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
122
- const HAND_TOOL_NAMES = HANDS_BAND_TOOL_NAMES;
123
121
  function mentionableNames(config, catalog) {
124
122
  const allowlist = config.atModelAllowlist ?? [];
125
123
  return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
@@ -999,8 +997,9 @@ export function createHttpServer(rawDeps) {
999
997
  }
1000
998
  try {
1001
999
  const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
1002
- const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}) });
1003
- return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body };
1000
+ const engineNotices = [];
1001
+ const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
1002
+ return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body, ...(engineNotices.length > 0 ? { engineNotices } : {}) };
1004
1003
  }
1005
1004
  catch (err) {
1006
1005
  if (err instanceof HttpError) {
@@ -1224,22 +1223,6 @@ export function createHttpServer(rawDeps) {
1224
1223
  throw e;
1225
1224
  }
1226
1225
  const pendingTool = cp.pendingAction.toolName;
1227
- const pendingToolCanon = pendingTool;
1228
- const availableTools = new Set((spec.tools ?? []).map((t) => t.name));
1229
- if (deps.config.remoteExec)
1230
- for (const name of HAND_TOOL_NAMES)
1231
- availableTools.add(name);
1232
- if (deps.checkpointStore)
1233
- availableTools.add("AskUserQuestion");
1234
- if (spec.selfOrchestration === true && deps.config.selfOrchestrationEnabled) {
1235
- availableTools.add(RUN_WORKFLOW_TOOL_NAME);
1236
- }
1237
- if (decision === "approve" && pendingToolCanon && !availableTools.has(pendingToolCanon)) {
1238
- return {
1239
- status: 422,
1240
- body: { error: `pending action no longer satisfiable: tool "${pendingToolCanon}" is not in the current task config (scenario changed since suspend)`, errorCode: "conflict.pending_action_unsatisfiable" },
1241
- };
1242
- }
1243
1226
  if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
1244
1227
  return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
1245
1228
  }
@@ -1,4 +1,4 @@
1
- import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ACCESSES } from "@sema-agent/core";
1
+ import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES } from "@sema-agent/core";
2
2
  function isRecord(v) {
3
3
  return typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  }
@@ -26,17 +26,26 @@ function readOneFace(raw) {
26
26
  const pt = raw.pathTarget;
27
27
  if (!isRecord(pt) || typeof pt.param !== "string" || !memberOf(TOOL_PATH_ACCESSES, pt.access))
28
28
  return undefined;
29
- if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible"]))
29
+ if (!onlyKeys(pt, ["param", "access", "aliases", "skillScopeEligible", "base", "absent", "patternParam"]))
30
30
  return undefined;
31
31
  if (pt.aliases !== undefined && !isStringArray(pt.aliases))
32
32
  return undefined;
33
33
  if (pt.skillScopeEligible !== undefined && typeof pt.skillScopeEligible !== "boolean")
34
34
  return undefined;
35
+ if (pt.base !== undefined && !memberOf(TOOL_PATH_BASES, pt.base))
36
+ return undefined;
37
+ if (pt.absent !== undefined && !memberOf(TOOL_PATH_ABSENCES, pt.absent))
38
+ return undefined;
39
+ if (pt.patternParam !== undefined && typeof pt.patternParam !== "string")
40
+ return undefined;
35
41
  out.pathTarget = {
36
42
  param: pt.param,
37
43
  access: pt.access,
38
44
  ...(pt.aliases !== undefined ? { aliases: pt.aliases } : {}),
39
45
  ...(pt.skillScopeEligible === true ? { skillScopeEligible: true } : {}),
46
+ ...(pt.base !== undefined ? { base: pt.base } : {}),
47
+ ...(pt.absent !== undefined ? { absent: pt.absent } : {}),
48
+ ...(pt.patternParam !== undefined ? { patternParam: pt.patternParam } : {}),
40
49
  };
41
50
  }
42
51
  if (raw.renderHints !== undefined) {
package/dist/runs.d.ts CHANGED
@@ -324,7 +324,16 @@ parkCheckpointReader?: {
324
324
  toolCallId?: unknown;
325
325
  };
326
326
  } | null>;
327
- }): Promise<void>;
327
+ },
328
+ /** L-167:装配期(`prepareSpec` → `resolveSpec`)铸下的用户面通告 —— 那一刻本腿的通告口还没开,
329
+ * 所以由本腿在 `registerEngineNoticeLeg` 那一拍连同 `pending` 一起投(投递单点仍是 `route()`)。
330
+ * 缺席 ⇒ 一个字节不投(既有调用方逐字不变)。 */
331
+ pendingEngineNotices?: readonly {
332
+ code: string;
333
+ message: string;
334
+ detail?: Record<string, unknown>;
335
+ sessionId?: unknown;
336
+ }[]): Promise<void>;
328
337
  /** How often a running instance refreshes its run's updated_at (liveness, independent of events).
329
338
  * Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
330
339
  * reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
package/dist/runs.js CHANGED
@@ -153,7 +153,7 @@ export function evictIfConflict(runner, sessionId, result) {
153
153
  runner.sessions.evict?.(sessionId);
154
154
  }
155
155
  }
156
- export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader) {
156
+ export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices) {
157
157
  const startedAt = Date.now();
158
158
  metrics?.addGauge("runs_active", 1);
159
159
  fleetPublisher?.onStart();
@@ -248,6 +248,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
248
248
  };
249
249
  const unregisterEngineNotice = registerEngineNoticeLeg({
250
250
  sessionId: spec.sessionId,
251
+ ...(pendingEngineNotices !== undefined ? { pending: pendingEngineNotices } : {}),
251
252
  durable: (row) => void append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=bg code=${row.code} task=${taskId}`)),
252
253
  });
253
254
  try {
@@ -22,9 +22,25 @@
22
22
  * `runtimeCapsResolver`) is a DEFERRED v2.
23
23
  * When the gate is closed, a sent `mcpServers` is IGNORED (advertised via `capabilities.mcpInjection`).
24
24
  */
25
- import { type CompliancePosture, type LockedKey, type McpServerSpec } from "@sema-agent/core";
25
+ import { type CompliancePosture, type EngineNotice, type LockedKey, type McpInjectionDropReason, type McpServerSpec } from "@sema-agent/core";
26
26
  /** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
27
27
  export declare const MAX_REQUEST_MCP_SERVERS = 32;
28
+ /**
29
+ * 🔴 **一类丢弃至多点名多少条**(codex 对抗复审 r1 [high],验真后采纳)—— 与挂载上限**同一个数**,
30
+ * 刻意不新造一个可调旋钮:点名的量级本就该与挂载的量级同阶。
31
+ *
32
+ * 病(本批引入,不是既有形):`MAX_REQUEST_MCP_SERVERS` 管的是「挂几台」,管不住「你发了几台」——
33
+ * HTTP 体上限 8 MiB 装得下十万个名字,而 `gate_closed`(整只列表被忽略)与 `over_cap`(上限之后的尾项)
34
+ * 两臂原本逐条点名,于是一条已认证请求就能造出十万条事实、十万条通告,逐条排进本腿的**串行账本写链**
35
+ * ——同一条链上后续的正常事件全部排在它们后面。修前的名字数组把超上限折成**一条**聚合串,所以无界的
36
+ * 是本批新写的那两个循环。
37
+ *
38
+ * 为什么不改成「数组太长就 400」(codex 的另一条建议,**未采纳**):今天 33 台的请求是**照常受理、挂前
39
+ * 32 台**的,加一条新的拒绝面为了一个资源边界去改受理语义,爆炸半径比病本身大(而且 resume 腿重放存量
40
+ * body 会当场砖死一批已 park 的任务——本仓 `opts.leg` 三处先例逐字写着「砖死 resume 比降级糟」)。
41
+ * 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
42
+ */
43
+ export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
28
44
  /**
29
45
  * Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
30
46
  * (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
@@ -37,19 +53,42 @@ export declare function mcpInjectionHonored(config: {
37
53
  lockedConfigKeys?: readonly LockedKey[];
38
54
  compliancePosture?: CompliancePosture;
39
55
  }): boolean;
56
+ /**
57
+ * L-167(core 7.9.1)—— 一条**没有被挂上**的请求腿 MCP 条目的**事实**。宿主报事实,core 铸词:
58
+ * `reason` 的词表属主是 core(`McpInjectionDropReason` = malformed_entry / name_reserved_by_deployment /
59
+ * gate_closed / over_cap),本仓一个词都不枚举,通告的码/受众/句子一个字都不拼。
60
+ *
61
+ * `sessionId` **不在**这个形上:它是通告的**路由半场**,在铸点(`mintInjectionDropNotices`)才补 ——
62
+ * 一条丢弃事实在没有会话语境的调用方(桩、单测、未来的非 HTTP 腿)那里同样成立,而通告不成立。
63
+ */
64
+ export interface McpInjectionDrop {
65
+ readonly server: string;
66
+ readonly reason: McpInjectionDropReason;
67
+ /** `malformed_entry` 专用:**哪个键**判不出形。结构性给出(读器在拒的那一行就知道),不从文案里 parse。 */
68
+ readonly field?: string;
69
+ }
70
+ /** L-167:本条腿的用户面通告席。`sessionId` = 路由半场(`auth.sessionId`,与 `spec.sessionId` 同源);
71
+ * `emit` = 收下 core 铸好的 `EngineNotice`。**本模块不知道**它最后落在 SSE、账本还是日志上 —— 那是
72
+ * 投递腿的事,本模块只负责「事实 → core 工厂 → 一条通告」。 */
73
+ export interface McpInjectionDropSeat {
74
+ readonly sessionId: string | undefined;
75
+ readonly emit: (notice: EngineNotice) => void;
76
+ }
40
77
  /**
41
78
  * Shape-check the raw `body.mcpServers`. `null` → a non-array (caller error; the route may 400). Otherwise the
42
79
  * valid subset + the names of dropped (malformed/over-cap) entries + [ref]'s `badContentOrigin`
43
80
  * (**降级**掉的内容出处声明,与 `dropped` 是两回事:那条 server **留着**,丢的只是它的声明)。
44
81
  */
45
- export declare function validateRequestMcp(raw: unknown): {
82
+ export declare function validateRequestMcp(raw: unknown): ValidatedRequestMcp | null;
83
+ type ValidatedRequestMcp = {
46
84
  ok: McpServerSpec[];
47
- dropped: string[];
85
+ dropped: McpInjectionDrop[];
86
+ droppedTotal: number;
48
87
  badContentOrigin: Array<{
49
88
  server: string;
50
89
  value: string;
51
90
  }>;
52
- } | null;
91
+ };
53
92
  /**
54
93
  * [ref] 件C §4.3 —— **同步**拒面:部署锁了 `mcp` 而请求仍带 `mcpServers` ⇒ 400 `config.locked_key`。
55
94
  *
@@ -128,7 +167,7 @@ export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, g
128
167
  /**
129
168
  * The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
130
169
  * caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
131
- * unchanged. Logs an `mcp_injection_ignored` when a caller SENT servers a closed gate dropped (so the shell never
170
+ * unchanged. Logs an `mcp_injection_dropped` (reason `gate_closed`) when a caller SENT servers a closed gate dropped (so the shell never
132
171
  * silently believes its `.mcp.json` took effect — it also reads `capabilities.mcpInjection`).
133
172
  */
134
173
  export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined, bodyMcp: unknown, config: {
@@ -144,5 +183,10 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
144
183
  }, logger?: {
145
184
  info?(m: string, meta?: unknown): void;
146
185
  warn(m: string, meta?: unknown): void;
147
- }): McpServerSpec[] | undefined;
186
+ },
187
+ /** L-167:本条腿的**用户面**投递口。`sessionId` = 通告的路由半场(缺席 ⇒ 一条都不铸,见
188
+ * {@link mintInjectionDropNotices});`emit` 收下 core 铸好的通告,由调用腿决定投到哪(见 `boot/resolve-spec.ts`)。
189
+ * 整段缺席(桩、单测、非 HTTP 腿)⇒ 只走日志终点,行为逐字与本件之前相同。 */
190
+ notice?: McpInjectionDropSeat): McpServerSpec[] | undefined;
191
+ export {};
148
192
  //# sourceMappingURL=task-mcp.d.ts.map
package/dist/task-mcp.js CHANGED
@@ -1,9 +1,10 @@
1
- import { resolveComplianceDenies } from "@sema-agent/core";
1
+ import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
4
4
  import { readMcpSourceLabel } from "./mcp-source-label.js";
5
5
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
6
6
  export const MAX_REQUEST_MCP_SERVERS = 32;
7
+ export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
7
8
  export function mcpInjectionHonored(config) {
8
9
  if (config.lockedConfigKeys?.includes("mcp") === true)
9
10
  return false;
@@ -14,20 +15,27 @@ export function mcpInjectionHonored(config) {
14
15
  function isStringRecord(v) {
15
16
  return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
16
17
  }
18
+ const bad = (field) => ({ ok: false, field });
19
+ const MALFORMED_NAME = "<malformed>";
20
+ function declaredName(e) {
21
+ return e && typeof e === "object" && typeof e.name === "string" && e.name !== ""
22
+ ? String(e.name)
23
+ : MALFORMED_NAME;
24
+ }
17
25
  function normalizeServer(e, noteBadOrigin) {
18
26
  if (!e || typeof e !== "object")
19
- return null;
27
+ return bad("entry");
20
28
  const o = e;
21
29
  if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
22
- return null;
30
+ return bad("name");
23
31
  const t = o.transport;
24
32
  if (!t || typeof t !== "object")
25
- return null;
33
+ return bad("transport");
26
34
  const tr = t;
27
35
  let transport;
28
36
  if (tr.kind === "stdio") {
29
37
  if (typeof tr.command !== "string" || tr.command.length === 0)
30
- return null;
38
+ return bad("transport.command");
31
39
  transport = {
32
40
  kind: "stdio",
33
41
  command: tr.command,
@@ -37,7 +45,7 @@ function normalizeServer(e, noteBadOrigin) {
37
45
  }
38
46
  else if (tr.kind === "http") {
39
47
  if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
40
- return null;
48
+ return bad("transport.url");
41
49
  transport = {
42
50
  kind: "http",
43
51
  url: tr.url,
@@ -46,12 +54,12 @@ function normalizeServer(e, noteBadOrigin) {
46
54
  };
47
55
  }
48
56
  else {
49
- return null;
57
+ return bad("transport.kind");
50
58
  }
51
59
  const spec = { name: o.name, transport };
52
60
  if (o.allowTools !== undefined) {
53
61
  if (!Array.isArray(o.allowTools) || !o.allowTools.every((a) => typeof a === "string"))
54
- return null;
62
+ return bad("allowTools");
55
63
  spec.allowTools = o.allowTools;
56
64
  }
57
65
  if (typeof o.elicitation === "boolean")
@@ -82,29 +90,32 @@ function normalizeServer(e, noteBadOrigin) {
82
90
  Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
83
91
  const faces = readMcpToolFaces(o.toolFaces);
84
92
  if (!faces.ok)
85
- return null;
93
+ return bad("toolFaces");
86
94
  if (faces.faces !== undefined)
87
95
  spec.toolFaces = faces.faces;
88
- return spec;
96
+ return { ok: true, spec };
89
97
  }
90
98
  export function validateRequestMcp(raw) {
91
99
  if (raw === undefined || raw === null)
92
- return { ok: [], dropped: [], badContentOrigin: [] };
100
+ return validateRequestMcpEntries([]);
93
101
  if (!Array.isArray(raw))
94
102
  return null;
103
+ return validateRequestMcpEntries(raw);
104
+ }
105
+ function validateRequestMcpEntries(raw) {
95
106
  const ok = [];
96
107
  const dropped = [];
97
108
  const badContentOrigin = [];
98
109
  for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
99
110
  const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }));
100
- if (v)
101
- ok.push(v);
111
+ if (v.ok)
112
+ ok.push(v.spec);
102
113
  else
103
- dropped.push(e && typeof e === "object" && typeof e.name === "string" ? String(e.name) : "<malformed>");
114
+ dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
104
115
  }
105
- if (raw.length > MAX_REQUEST_MCP_SERVERS)
106
- dropped.push(`(+${raw.length - MAX_REQUEST_MCP_SERVERS} over cap of ${MAX_REQUEST_MCP_SERVERS})`);
107
- return { ok, dropped, badContentOrigin };
116
+ for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
117
+ dropped.push({ server: declaredName(e), reason: "over_cap" });
118
+ return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin };
108
119
  }
109
120
  export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
110
121
  if (!lockedKeys.has("mcp"))
@@ -140,21 +151,33 @@ export function mergeRequestMcp(baseline, gated, reservedNames) {
140
151
  const add = gated.filter((s) => !closed.has(s.name));
141
152
  return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
142
153
  }
143
- export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
154
+ export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice) {
144
155
  if (bodyMcp === undefined || bodyMcp === null)
145
156
  return baseline;
146
- const honored = mcpInjectionHonored(config);
147
- if (!honored) {
148
- logger?.warn("mcp_injection_ignored", { reason: "gate_closed (per-request MCP honored only on single-user host lane)" });
149
- return baseline;
150
- }
151
- const v = validateRequestMcp(bodyMcp);
152
- if (v === null) {
157
+ const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger);
158
+ mintInjectionDropNotices(drops, notice);
159
+ return mcp;
160
+ }
161
+ function classifyRequestMcp(baseline, bodyMcp, config, logger) {
162
+ if (bodyMcp !== undefined && bodyMcp !== null && !Array.isArray(bodyMcp)) {
153
163
  logger?.warn("mcp_injection_ignored", { reason: "mcpServers not an array" });
154
- return baseline;
164
+ return { mcp: baseline, drops: [] };
155
165
  }
156
- if (v.dropped.length > 0)
157
- logger?.warn("mcp_injection_dropped", { dropped: v.dropped });
166
+ const entries = Array.isArray(bodyMcp) ? bodyMcp : [];
167
+ const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger) : classifyGateClosed(baseline, entries);
168
+ if (outcome.drops.length > 0)
169
+ logger?.warn("mcp_injection_dropped", { dropped: outcome.drops, droppedTotal: outcome.droppedTotal });
170
+ return { mcp: outcome.mcp, drops: outcome.drops };
171
+ }
172
+ function classifyGateClosed(baseline, entries) {
173
+ return {
174
+ mcp: baseline,
175
+ drops: entries.slice(0, MAX_REQUEST_MCP_DROP_NAMES).map((e) => ({ server: declaredName(e), reason: "gate_closed" })),
176
+ droppedTotal: entries.length,
177
+ };
178
+ }
179
+ function classifyHonored(baseline, entries, config, logger) {
180
+ const v = validateRequestMcpEntries(entries);
158
181
  if (v.badContentOrigin.length > 0) {
159
182
  logger?.warn("mcp_content_class_dropped", {
160
183
  entries: v.badContentOrigin,
@@ -165,13 +188,26 @@ export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
165
188
  const reservedNames = config.mcpServers?.reservedNames ?? [];
166
189
  const closed = deploymentClosedMcpNames(baseline, reservedNames);
167
190
  const blocked = v.ok.filter((s) => closed.has(s.name)).map((s) => s.name);
168
- if (blocked.length > 0) {
169
- logger?.warn("mcp_injection_dropped", {
170
- reason: "name_reserved_by_deployment",
171
- dropped: blocked,
172
- note: "these names are declared by the deployment (center/config) — mounted or skipped, a caller can neither shadow nor refill them; rename the request-side server",
173
- });
191
+ return {
192
+ mcp: mergeRequestMcp(baseline, v.ok, reservedNames),
193
+ drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
194
+ droppedTotal: v.droppedTotal + blocked.length,
195
+ };
196
+ }
197
+ function mintInjectionDropNotices(drops, notice) {
198
+ if (notice === undefined || drops.length === 0)
199
+ return;
200
+ const sessionId = notice.sessionId;
201
+ if (typeof sessionId !== "string" || sessionId.length === 0)
202
+ return;
203
+ const said = new Set();
204
+ for (const d of drops) {
205
+ if (d.server !== MALFORMED_NAME) {
206
+ if (said.has(d.server))
207
+ continue;
208
+ said.add(d.server);
209
+ }
210
+ notice.emit(mcpInjectionDroppedNotice({ sessionId, server: d.server, reason: d.reason, ...(d.field !== undefined ? { field: d.field } : {}) }));
174
211
  }
175
- return mergeRequestMcp(baseline, v.ok, reservedNames);
176
212
  }
177
213
  //# sourceMappingURL=task-mcp.js.map
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
22
22
  export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
@@ -136,5 +136,21 @@ export declare function registerEngineNoticeLeg(opts: {
136
136
  live?: EngineNoticeWireSink;
137
137
  durable?: EngineNoticeWireSink;
138
138
  router?: EngineNoticeRouter;
139
+ /**
140
+ * L-167 —— **腿开张之前**就已经铸好的通告(装配期 `resolveSpec` 的产物,经
141
+ * `PreparedTaskSubmission.engineNotices` 递过来)。注册完口**立刻**经同一只 {@link EngineNoticeRouter.route}
142
+ * 投出:白名单、归属键、消毒、隔离四件判据一个都不绕,**不是**第二条投递机制 —— 只是把 `route()` 挪到
143
+ * 「这条会话已经有口在听」的那一拍。
144
+ *
145
+ * 🔴 为什么不让铸点自己 `route()`:装配(`prepareSpec`)跑在本函数**之前**,那一刻这条会话一只口都没有,
146
+ * `route()` 恒返 0 —— 一条结构上永远投不出去的用户面披露,就是本仓 `task.user_steer_undrained` 陈腐明拒
147
+ * 那一族的病。缺席/空数组 ⇒ 一个字节不投(既有腿逐字不变)。
148
+ */
149
+ pending?: readonly {
150
+ code: string;
151
+ message: string;
152
+ detail?: Record<string, unknown>;
153
+ sessionId?: unknown;
154
+ }[];
139
155
  }): () => void;
140
156
  //# sourceMappingURL=engine-notice-wire.d.ts.map
@@ -20,6 +20,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
20
20
  "peer.held_settled",
21
21
  "peer.idle_subscription",
22
22
  "classifier.denial_limit",
23
+ "mcp.injection_dropped",
23
24
  ];
24
25
  const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
25
26
  export function isEngineNoticeWireCode(code) {
@@ -150,6 +151,10 @@ export function registerEngineNoticeLeg(opts) {
150
151
  const sinks = [...(live ? [live] : []), ...(durable ? [durable] : [])];
151
152
  if (sinks.length === 0)
152
153
  return () => undefined;
153
- return (opts.router ?? defaultEngineNoticeRouter).registerLeg(sessionId, sinks);
154
+ const router = opts.router ?? defaultEngineNoticeRouter;
155
+ const unregister = router.registerLeg(sessionId, sinks);
156
+ for (const notice of opts.pending ?? [])
157
+ router.route(notice);
158
+ return unregister;
154
159
  }
155
160
  //# sourceMappingURL=engine-notice-wire.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.67.0",
3
+ "version": "7.68.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -54,7 +54,7 @@
54
54
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
55
55
  },
56
56
  "dependencies": {
57
- "@sema-agent/core": "7.9.0",
57
+ "@sema-agent/core": "7.9.2",
58
58
  "@sema-agent/settings-schema": "1.9.0",
59
59
  "e2b": "^2.28.0",
60
60
  "libsodium-wrappers": "^0.8.4",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "8.6.0",
72
+ "@sema-agent/sdk": "8.8.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",