@sema-agent/server 7.88.2 → 7.90.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 (44) hide show
  1. package/dist/boot/fatal-exit.d.ts +3 -0
  2. package/dist/boot/fatal-exit.js +5 -0
  3. package/dist/boot/resolve-spec.js +2 -0
  4. package/dist/boot/stage-06-runners.js +7 -3
  5. package/dist/brain.js +32 -19
  6. package/dist/capabilities/center-plugins.d.ts +10 -0
  7. package/dist/capabilities/center-plugins.js +21 -5
  8. package/dist/capabilities/memory-notice.d.ts +7 -1
  9. package/dist/capabilities/memory-notice.js +6 -6
  10. package/dist/capabilities/team.d.ts +37 -0
  11. package/dist/config-catalog.js +7 -19
  12. package/dist/config-center/apply-effective.js +1 -1
  13. package/dist/config.d.ts +1 -1
  14. package/dist/config.js +11 -5
  15. package/dist/hooks/hook-llm.d.ts +25 -0
  16. package/dist/hooks/hook-llm.js +27 -9
  17. package/dist/hooks/hook-runner.js +21 -4
  18. package/dist/http/admission.js +9 -5
  19. package/dist/http/routes/capabilities.js +2 -2
  20. package/dist/http/routes/session-sync.js +10 -3
  21. package/dist/http/routes/sessions.js +16 -14
  22. package/dist/http/wire-types.d.ts +12 -0
  23. package/dist/main.js +2 -1
  24. package/dist/model-route-url.d.ts +27 -3
  25. package/dist/model-route-url.js +28 -14
  26. package/dist/plugins/memory-embedder.d.ts +0 -6
  27. package/dist/plugins/memory-embedder.js +10 -16
  28. package/dist/plugins/remote-env-adb.d.ts +13 -0
  29. package/dist/plugins/remote-env-adb.js +42 -9
  30. package/dist/plugins/remote-env-device.d.ts +39 -3
  31. package/dist/plugins/remote-env-device.js +107 -31
  32. package/dist/request-key-closure.js +2 -1
  33. package/dist/runtime-governance.d.ts +16 -7
  34. package/dist/runtime-governance.js +27 -7
  35. package/dist/session-policy-wire.d.ts +103 -0
  36. package/dist/session-policy-wire.js +66 -0
  37. package/dist/task-settings.d.ts +12 -2
  38. package/dist/task-settings.js +7 -5
  39. package/dist/trace/core-keyset-guard.d.ts +2 -2
  40. package/dist/trace/project.d.ts +7 -0
  41. package/dist/trace/project.js +1 -0
  42. package/dist/trace/redact.d.ts +110 -5
  43. package/dist/trace/redact.js +71 -12
  44. package/package.json +2 -2
@@ -3,6 +3,11 @@ import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.j
3
3
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
4
4
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
5
5
  import { parseHooksConfig } from "./hooks/hook-runner.js";
6
+ export const SETTINGS_PERMISSION_MODES = ["default", "acceptEdits", "plan", "bypassPermissions", "auto"];
7
+ export const SETTINGS_PERMISSION_MODE_WORDS = SETTINGS_PERMISSION_MODES.map((m) => `"${m}"`).join(" | ");
8
+ export function isSettingsPermissionMode(raw) {
9
+ return typeof raw === "string" && SETTINGS_PERMISSION_MODES.includes(raw);
10
+ }
6
11
  export function coercePermissionMode(raw) {
7
12
  const strict = coercePermissionModeStrict(raw);
8
13
  if (strict !== undefined)
@@ -12,9 +17,7 @@ export function coercePermissionMode(raw) {
12
17
  return undefined;
13
18
  }
14
19
  export function coercePermissionModeStrict(raw) {
15
- if (raw === "plan" || raw === "acceptEdits" || raw === "default" || raw === "bypassPermissions" || raw === "auto")
16
- return raw;
17
- return undefined;
20
+ return isSettingsPermissionMode(raw) ? raw : undefined;
18
21
  }
19
22
  export const PLAN_APPROVE_PERMISSION_MODES_AFTER = ["default", "acceptEdits"];
20
23
  export function isPlanApprovePermissionModeAfter(raw) {
@@ -154,8 +157,7 @@ export function parseTaskSettings(raw) {
154
157
  const allow = cleanStringArray(p.allow);
155
158
  const deny = cleanStringArray(p.deny);
156
159
  const ask = cleanStringArray(p.ask);
157
- const mode = p.defaultMode;
158
- const defaultMode = mode === "default" || mode === "acceptEdits" || mode === "plan" || mode === "bypassPermissions" || mode === "auto" ? mode : undefined;
160
+ const defaultMode = coercePermissionModeStrict(p.defaultMode);
159
161
  const rawKill = p.disableAutoMode;
160
162
  const disableAutoMode = rawKill === "disable" || rawKill === true || rawKill === false ? rawKill : undefined;
161
163
  if (allow || deny || ask || defaultMode || disableAutoMode !== undefined) {
@@ -130,8 +130,8 @@ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "seve
130
130
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
131
131
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
132
132
  type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
133
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars";
134
- type TaskSpecUnwired = "excludeAllTools" | "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
133
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "taskId" | "signal" | "preemptSignal" | "modelRole" | "enableBlockedReport" | "maxSuspends" | "autoModeRequested" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools";
134
+ type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
135
135
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
136
136
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
137
137
  type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles";
@@ -769,6 +769,13 @@ export declare function compactedEventData(ev: {
769
769
  clampReason?: "budget" | "tolerance";
770
770
  tokensAfter?: number;
771
771
  triggerTokensBefore?: number;
772
+ /** core 7.24.0 [ref]:引擎**自己**算的结构性净减(pre-messages 的结构和 减去 post-messages 的同一个和,
773
+ * 铸点处 clamp ≥0),三条压缩车道(turn 边界 / prompt-too-long 恢复 / 任务末 settle)同一只
774
+ * `mintCompactedEvent` 出。🔴 **缺席 = post 测量失败,不是 0**:上面那两个旧键(`tokensAfter` /
775
+ * `triggerTokensBefore`)自本版起在 core 的类型上写明是**诊断位、不可相减**(触发闸读的是 usage 锚定
776
+ * 的数、post 恒是结构和 + 固定提示词开销,两者的差混坐标,实测在普通 run 上是负数)。本仓的处置因此
777
+ * 是**三条都逐字透传、一条都不合成**:谁要画上下文表就读 `freedTokens`,读不到就渲染 unknown。 */
778
+ freedTokens?: number;
772
779
  durationMs?: number;
773
780
  phaseDurations?: {
774
781
  prepareMs?: number;
@@ -656,6 +656,7 @@ export function compactedEventData(ev) {
656
656
  ...(ev.clampReason !== undefined ? { clampReason: ev.clampReason } : {}),
657
657
  ...(num(ev.tokensAfter) !== undefined ? { tokensAfter: num(ev.tokensAfter) } : {}),
658
658
  ...(num(ev.triggerTokensBefore) !== undefined ? { triggerTokensBefore: num(ev.triggerTokensBefore) } : {}),
659
+ ...(num(ev.freedTokens) !== undefined ? { freedTokens: num(ev.freedTokens) } : {}),
659
660
  ...(num(ev.durationMs) !== undefined ? { durationMs: num(ev.durationMs) } : {}),
660
661
  ...(phaseDurations !== undefined ? { phaseDurations } : {}),
661
662
  };
@@ -136,6 +136,27 @@ export declare function redactDeep(value: unknown): unknown;
136
136
  * owner-scoped, so this preview is only ever shown to the ask's own principal (or an operator inspecting it).
137
137
  */
138
138
  export declare function redactedPreview(value: unknown, cap?: number): string | null;
139
+ /**
140
+ * **这一位是不是一个层级式绝对 URL**(有 authority 的那一族)—— 本铸点与**模型路由三键拒启**
141
+ * (`model-route-url.ts` 第一臂)共用的**同一只**判据:两处曾各判各的,而 `URL.canParse` 单独为真的
142
+ * **不透明** URL 恰好是「凭据位看不见」的那一形。
143
+ *
144
+ * ⚠️ **射程如实**(车JC 复审去幻觉轮:此处原文写的是「本仓唯一判据」,亲读证伪)—— 仓里**另有一处**仍用
145
+ * 裸 `URL.canParse` 回答同一个问题,且**刻意不在射程内**(改它是一次新的行为面拒启,按三问另开车):
146
+ * `config.ts` 的 `GIT_API_BASEURL`(`parseGitApiKind`):无协议闸 ⇒ 不透明形今天过门(拒因经本铸点,
147
+ * 所以不回显凭据;可用性由 `fetch` 那侧决定)。
148
+ * ✅ 原先列在这里的第二处(`config-catalog.ts` 的 `stripUrlUserinfo`:不透明形 `href === 原串` ⇒ 原样回显)
149
+ * 已在 7.90.0 随 core `displayUrlOf` 到货**闭环** —— 那只函数整只退役,url 型行的回显改走本铸点,
150
+ * 于是这道闸对它自动成立(catalog wire 行为面变更,CHANGELOG 已对外写明并点名 @web-admin)。
151
+ *
152
+ * 🔴 谓词写成 `URL.canParse` 而不是 try/catch:这一臂**不是兜底**(兜底才归静默降级那张表管),它是判据本身 ——
153
+ * 「不是一个层级式 URL」是一个可判的状态,用异常当控制流只会让它看起来像一次意外。
154
+ * 🔴 「解析得动」**不够**:`new URL("alice:hunter2@gw.internal:8000")` 是合法的**不透明** URL(`alice:` 当 scheme,
155
+ * 与 `mailto:` 同族)—— 它没有 authority,URL 律的锚(`scheme://`)对它不成立,`username`/`password` 恒空,
156
+ * 直接回原串就是密码明文(红先实测)。闸取「**层级式** URL」:`href` 以 `<protocol>//` 起头 ⇒ 有 authority
157
+ * ⇒ 三个凭据位都在律的射程内。
158
+ */
159
+ export declare function isHierarchicalUrl(raw: string): boolean;
139
160
  /**
140
161
  * 🔴 **配置型 URL 上日志 / 诊断面的唯一 display-safe 铸点**(S-475 件B;`build*` = 返回纯数据的一只纯函数)。
141
162
  *
@@ -144,16 +165,100 @@ export declare function redactedPreview(value: unknown, cap?: number): string |
144
165
  * 网关 URL 的 userinfo / query 里,密码明文落 `engine-*.log`(日志的读者面比 run 错误文本更宽:它落盘、
145
166
  * 被采集、进工单)。同类出口不止这两处,类级机器门在 `test/config-url-display-safe.test.ts`。
146
167
  *
147
- * **内部就调 {@link redactSecrets} —— 同一条 URL 律、同一张名表、同一批记号,零第二份实现**:
148
- * 本函数自己只加两件事,两件都是**配置型 URL 特有**的:
168
+ * 🔴 **S-483 展示半场(7.90.0 / core 7.24.0):两条正交的律合成一次,名表的射程缩到不可约的那一半**。
169
+ * 改前内部只有一条:`redactSecrets(new URL(raw).href)` —— 自由文本那条**名单制**的律,query 的值只在键名
170
+ * 落在 `CREDENTIAL_NAME_WORDS`(或值本身像个密钥)时才遮,于是**没人列过的那个键名**就是它的盲点
171
+ * (`?sig=…` / `?auth=…` / 部署自定义的网关参数名)。现在前面先跑 core 的 `displayUrlOf`,它是**位置制**的:
172
+ * · userinfo **落地**(不是遮成记号 —— 它从**解析**组合 `scheme://host + path + query`,userinfo 结构上
173
+ * 进不了产物);凭据在 userinfo 这件事本身仍然响亮,只不过是在**声明关口**响
174
+ * (`modelRouteUrlRefusal` / `parseA2aServe` / core 的 `assertServableRequestRoot`),不靠展示串暗示;
175
+ * · query 的**每一个值**成记号、**键名留着**(键名必须留:两条只在 query 上不同的根若都渲染成同一个串,
176
+ * 「A 配给了 B」这句拒因会写成「targets X … paired with X」);
177
+ * · 无 `=` 的**裸段**整片成一枚记号、连名字都不给(`URLSearchParams` 把它读成「空值的键」,那个读法
178
+ * 正好把 payload 本身放上了屏);
179
+ * · fragment 丢掉(implicit-OAuth 把 bearer 放那儿),尾斜杠与默认端口按配对键的同一把尺折。
180
+ * 位置律跑完**再跑一次形状律**({@link redactSecrets}):它管的是「一段字面量**长得像**凭据」,而位置律
181
+ * 对这件事结构性失明 —— `pathname` 被逐字保留,「这个路径段是不是一枚令牌」只能按形认。两条律正交、
182
+ * 各有一个属主、在铸点里合成一次;因为位置律先跑,名表在 query 上无事可做,它的射程被**缩小**到
183
+ * host + path 这一不可约的半边。
184
+ * 换尺的**代价成文、不是意外**:非凭据参数的值(`?model=x`)现在也成记号 —— 排障读不到它了。方向一致:
185
+ * 一个没人列过的凭据参数名泄出去是不可逆的,少读一个模型名不是。本仓原先**三份**各判各的 URL 脱敏实现
186
+ * (本铸点 / catalog 的 `stripUrlUserinfo` / embedder 的 `redactEndpointForLog`)合成这一只,记号闭形仍归
187
+ * 本模块({@link REDACTION_TOKENS};委托边界的闭集校验在下面的加载期检查)。
188
+ *
189
+ * 本函数自己只剩两件事,两件都是**配置型 URL 特有**的(core 的渲染器对这两形有自己的答案,而配置面要更严):
149
190
  * ① **有效性闸**:配置里的那一位应当是一个**绝对 URL**。`new URL(raw)` 解析不动 ⇒ 这一位根本不是 URL
150
191
  * (裸 `host:port`、相对路径、手滑的散文),自由文本的 URL 律对它没有定界依据 ⇒ 返回**占位**
151
192
  * {@link DISPLAY_SAFE_URL_TOKEN},**不回落原值**([ref]:安全轴上的兜底必须 fail-closed 或响亮)。
152
193
  * 成文代价:`MODEL_GATEWAY_BASEURL=127.0.0.1:8000` 这种缺 scheme 的配置在日志上变成 `«redacted:url»`
153
194
  * —— 而它在 `fetch` 那一侧本来就是必然失败的形(WHATWG 要求绝对 URL),日志上响亮一点方向一致。
154
195
  * ② **空串 = 未设**:里面没有凭据,原样回空串(不铸占位 —— 占位会让「没配」读成「配了但看不见」)。
155
- * 解析成功后**不重建 URL**(`new URL(...).toString()` 会归一化路径/端口/大小写):保形是这条律的一半,
156
- * 运维要靠 host / port / path / 非凭据参数逐字排障,所以回的是**原串经脱敏器**的结果。
196
+ * ⚠️ **保形只到 host / port / path 这一层**(S-483 起如实改口;`test/config-url-display-safe.test.ts`
197
+ * 的 S-483-o 是这句话的机器读数,别在散文里再抄一遍):scheme / host / port / path 的字节逐字(模 WHATWG
198
+ * 的 HTTP 等价归一:默认端口省、host 小写、IDN→punycode、点段折叠,再加**整串末尾**的斜杠去掉 ——
199
+ * 是**整串末尾**不是「路径末尾」:`…/p/` ⇒ `…/p`,而 `…/p/?k=v` 的 `/p/` 保留),query 只留**键名**。
200
+ */
201
+ /** 位置律的**形** —— 生产路径恒是 core 的 {@link displayUrlOf}。参数化是**为可测性开的唯一一个缝**
202
+ * (S-497 的「违反格」要造一个「位置律漏了一段」的世界,而拿上游的现成缺陷当触发器会在它修好那天失效);
203
+ * 生产调用方**不传**这个参数。 */
204
+ export type PositionalUrlLaw = (url: string) => string;
205
+ /** 测试口(与 `observability/fail-open.ts` 的 `resetFailOpenRecorderForTest` 同姿势):一次性 warn 的闸是
206
+ * 模块级的,而「只 warn 一次」本身要被钉,所以给测试一个复位口。生产零调用。 */
207
+ export declare function resetDisplaySafeFailClosedWarnForTest(): void;
208
+ /**
209
+ * 🔴 **S-497 的不变量 —— 形状律在 query / fragment 区间上有一条凭据跨度 ⇒ 那一整段不出门**。
210
+ *
211
+ * 为什么需要它(差分实测 650 形里 26 个反例,本车亲跑复现):两条律**串起来**的时候,前一条会把后一条
212
+ * 赖以认字的**结构**改掉。实测的病形是 `https://h/?next=https://u:&pw@h=` —— 位置律先按 `&` 切段、把
213
+ * `next=https://u:` 整段换成记号,于是 `&` 之后那半截密码落到了**下一段的键名位**上被保留,而形状律
214
+ * 此刻已经看不到 `://<userinfo>@` 这个结构、无从下手(`redactSecrets(原串)` 是对的,可它拿不到原串)。
215
+ *
216
+ * 🔴 **判据落在「切分之前」,而不是在产物文本里找证据**(这一条是本不变量的全部)。三次尝试都被同一件事
217
+ * 击穿:①凭据**整段**不得是产物的子串 —— 位置律把它切开了,整段自然不是子串;②切成**片** + 长度下限 ——
218
+ * 材料能被切成全是单字符(`&a!b!c!d@`)甚至纯标点(`&!$()*+,;@`),任何下限都漏;③**字符集**包含 ——
219
+ * 密码的每个字符都能在同一条 URL 的 path / host / 别的键名里另外出现一次,包含关系成立而明文骑在产物上
220
+ * (`https://h/abcdefghijklmnopqrstuvwxyz0123456789?ok=1&next=https://u:&secret123=x@h`,S-497-k)。
221
+ * 共同的根:**切开之后,「这个字节来自凭据」在文本上不可判**。⇒ 判据搬到切分之前,用形状律自己报的
222
+ * **跨度坐标**当出处证明({@link shapeLawCredentialInQueryOrFragment})—— 不需要长度下限、字符类白名单
223
+ * 或任何集合运算,**规则比前三版都少**,而且对「将来出现的新形」天然封闭(新形状照样落在跨度里)。
224
+ *
225
+ * 两步(一条规则,不按形状分臂):
226
+ * ① 形状律在**归一化视图**上取跨度;凡有一条跨度落进 `?`(或 `#`)之后的区间 ⇒ 整段 query + fragment
227
+ * 换一枚 {@link REDACTION_TOKENS} 内的占位,`scheme://host[:port]/path` 留着并照旧过一遍形状律;
228
+ * ② 否则照旧合成:位置律 → 形状律。
229
+ * **过度遮的射程如实**:一条 query 里**真的**带凭据的配置型 URL,自本版起读不到它的**其它**参数名了
230
+ * (改前是逐参数遮)。方向一致,而且干净的根**零影响** —— Azure 式 `?api-version=…&deployment=…` 上
231
+ * 形状律一条跨度都没有,结构上走不到这一步(S-497-e 单独钉住)。path 区间的凭据**不**走这条路:位置律
232
+ * 逐字保留 `pathname`,形状律在产物上照常认得它(S-497-f 的并集格)。
233
+ * 🔴 这是 fail-**closed**(扣下信息),**不**登记 `recordFailOpen` —— 那张表记的是「放行了本该拒的」。
234
+ * 只打一条一次性 warn(**不含原文**:原文就是那段凭据)。
235
+ */
236
+ export declare function buildDisplaySafeUrl(raw: string, positionalLaw?: PositionalUrlLaw): string;
237
+ /**
238
+ * {@link buildDisplaySafeUrl} 的**带判据**出口:除了展示串,还说这一次有没有落到 fail-closed
239
+ * (= 「query / fragment 因为里面有一条凭据跨度而整段被扣下」)。
240
+ *
241
+ * 🔴 **为什么要这个出口**(发前复审):core 的**路由拒因**是一句**自由文本**,里面嵌着 core **已经用
242
+ * 位置律改写过**的那条 URL —— 到我们手上时 `://<userinfo>@` 的结构早没了,形状律无从下手,于是那条出口
243
+ * 整个在不变量之外(实测:降级拒因把密码写进 `listening` 行)。这里不去猜怎么把上游那句话重新脱敏
244
+ * (那会变成第二个写者),而是用**同一条判决**:铸点对这条 URL 都已经守不住了,任何**引用它**的自由文本
245
+ * 一律扣下(见 {@link buildDisplaySafeRefusal})。判决属主还是这一只函数。
246
+ */
247
+ export declare function buildDisplaySafeUrlChecked(raw: string, positionalLaw?: PositionalUrlLaw): {
248
+ text: string;
249
+ failedClosed: boolean;
250
+ };
251
+ /**
252
+ * 🔴 **引用了一条「铸点都守不住」的 URL 的自由文本,一律扣下**(S-497 的第二个出口;codex 第三轮 [high] 二)。
253
+ *
254
+ * 料 = 上游(core)的路由拒因:一句自由文本,里面嵌着 core **已经用位置律改写过**的那条 URL。到本仓手上时
255
+ * `://<userinfo>@` 的结构已经没了 ⇒ 形状律认不出那一段 ⇒ 光过一遍 `redactSecrets` 兜不住(实测:降级拒因
256
+ * 把密码写进 `listening` 行)。
257
+ *
258
+ * 判据**不新造**:问 {@link buildDisplaySafeUrlChecked} —— 如果铸点对这几条源 URL 里的任何一条都已经落到
259
+ * fail-closed,那么**任何引用它的句子**都不许出门,换一句固定文案(零原文)。机器可读的那一半不丢:调用方
260
+ * 手里有 `verdict.code`(拒因的码),它本来就在同一个投影的**另一个字段**上。
261
+ * 都守得住 ⇒ 照旧只过一遍形状律(上游的位置律已经把 URL 的凭据位处理过,这一层管的是**形状**那一半)。
157
262
  */
158
- export declare function buildDisplaySafeUrl(raw: string): string;
263
+ export declare function buildDisplaySafeRefusal(text: string, sourceUrls: readonly string[]): string;
159
264
  //# sourceMappingURL=redact.d.ts.map
@@ -1,4 +1,4 @@
1
- import { stripFormatCharacters } from "@sema-agent/core";
1
+ import { displayUrlOf, stripFormatCharacters } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
3
  export const normalizeForRedaction = stripFormatCharacters;
4
4
  const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
@@ -1085,6 +1085,7 @@ const FRAGMENT_SECRET_PATTERNS = [
1085
1085
  [valueArm("xapp-\\d-", "[A-Za-z0-9-]", 4, "needs-left-boundary"), "«redacted:slack-token:partial»"],
1086
1086
  ];
1087
1087
  const DISPLAY_SAFE_URL_TOKEN = "«redacted:url»";
1088
+ const WITHHELD_VALUE_TOKEN = "«redacted»";
1088
1089
  const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FRAGMENT_TOKEN, DISPLAY_SAFE_URL_TOKEN, ...FRAGMENT_SECRET_PATTERNS.map(([, r]) => r)]
1089
1090
  .flatMap((r) => r.match(/«[^»]*»/g) ?? []));
1090
1091
  {
@@ -1092,6 +1093,22 @@ const REDACTION_TOKENS = new Set([...FULL_ARM_PATTERNS.map(([, r]) => r), PEM_FR
1092
1093
  for (const t of REDACTION_TOKENS)
1093
1094
  if (!closed.test(t))
1094
1095
  throw new Error(`redact: token ${t} is outside the closed form ${REDACTION_TOKEN_RE_SRC}`);
1096
+ if (!REDACTION_TOKENS.has(WITHHELD_VALUE_TOKEN)) {
1097
+ throw new Error(`redact: ${WITHHELD_VALUE_TOKEN} is no longer produced by the arm tables — the fail-closed filler must stay a closed-set member`);
1098
+ }
1099
+ }
1100
+ {
1101
+ for (const probe of ["https://probe.invalid/p?k=v", "https://probe.invalid/p?bare"]) {
1102
+ const toks = displayUrlOf(probe).match(/«[^»]*»/g) ?? [];
1103
+ if (toks.length === 0) {
1104
+ throw new Error(`redact: core displayUrlOf(${probe}) minted no «…» token at all — upstream changed the placeholder spelling; map it at the delegation boundary (never add a second token table)`);
1105
+ }
1106
+ for (const tok of toks) {
1107
+ if (!REDACTION_TOKENS.has(tok)) {
1108
+ throw new Error(`redact: core displayUrlOf minted ${tok}, which is outside this module's token closed set — map it at the delegation boundary (never add a second token table)`);
1109
+ }
1110
+ }
1111
+ }
1095
1112
  }
1096
1113
  const labelOfToken = (token) => token.replace(/[«»]/g, "").replace(/^redacted:/, "");
1097
1114
  const FRAGMENT_ARM_TOKENS = new Set(FRAGMENT_SECRET_PATTERNS.map(([, repl]) => repl));
@@ -1598,7 +1615,7 @@ const armPool = (() => {
1598
1615
  const kept = resolveSpanOverlaps(spans);
1599
1616
  const out = applySpans(text, kept).out;
1600
1617
  if (!wantCount || kept.length === 0)
1601
- return { out, hits: NO_HITS };
1618
+ return { out, hits: NO_HITS, kept };
1602
1619
  const tokenOf = (sp) => members[sp.order].token;
1603
1620
  const strong = spans.filter((sp) => armOfToken(tokenOf(sp)) === "full").sort((a, b) => a.start - b.start);
1604
1621
  const byArmedToken = new Map();
@@ -1618,7 +1635,7 @@ const armPool = (() => {
1618
1635
  else
1619
1636
  cur.count += 1;
1620
1637
  }
1621
- return { out, hits: [...byArmedToken.values()].map(({ token, arm, count }) => ({ label: labelOfToken(token), count, arm })) };
1638
+ return { out, hits: [...byArmedToken.values()].map(({ token, arm, count }) => ({ label: labelOfToken(token), count, arm })), kept };
1622
1639
  };
1623
1640
  })();
1624
1641
  let onRedaction;
@@ -1641,11 +1658,12 @@ function reportHits(hits) {
1641
1658
  }
1642
1659
  function runRedaction(text, observe, json, wantHits) {
1643
1660
  const wantCount = wantHits || (onRedaction !== undefined && observe);
1644
- const r = applyRules(normalizeForRedaction(text), wantCount, json, observe);
1661
+ const normalized = normalizeForRedaction(text);
1662
+ const r = applyRules(normalized, wantCount, json, observe);
1645
1663
  forgetEscapedSeparatorTails();
1646
1664
  if (observe)
1647
1665
  reportHits(r.hits);
1648
- return { text: r.out, hits: r.hits };
1666
+ return { text: r.out, hits: r.hits, spans: r.kept };
1649
1667
  }
1650
1668
  export function redactSecrets(text, opts) {
1651
1669
  return runRedaction(text, opts?.observe ?? true, opts?.json === true, false).text;
@@ -1712,14 +1730,55 @@ export function redactedPreview(value, cap = 2000) {
1712
1730
  s = redactSecrets(s, { json: true });
1713
1731
  return s.length > cap ? `${s.slice(0, cap)}…[truncated]` : s;
1714
1732
  }
1715
- export function buildDisplaySafeUrl(raw) {
1716
- if (raw === "")
1717
- return "";
1733
+ export function isHierarchicalUrl(raw) {
1718
1734
  if (!URL.canParse(raw))
1719
- return DISPLAY_SAFE_URL_TOKEN;
1735
+ return false;
1720
1736
  const u = new URL(raw);
1721
- if (!u.href.startsWith(`${u.protocol}//`))
1722
- return DISPLAY_SAFE_URL_TOKEN;
1723
- return redactSecrets(u.href);
1737
+ return u.href.startsWith(`${u.protocol}//`);
1738
+ }
1739
+ function shapeLawCredentialInQueryOrFragment(canonical) {
1740
+ const normalized = normalizeForRedaction(canonical);
1741
+ const boundary = normalized.indexOf("?");
1742
+ if (boundary < 0)
1743
+ return false;
1744
+ return runRedaction(canonical, false, false, false).spans.some((sp) => sp.end > boundary);
1745
+ }
1746
+ let displaySafeFailClosedWarned = false;
1747
+ export function resetDisplaySafeFailClosedWarnForTest() {
1748
+ displaySafeFailClosedWarned = false;
1749
+ }
1750
+ export function buildDisplaySafeUrl(raw, positionalLaw = displayUrlOf) {
1751
+ return buildDisplaySafeUrlChecked(raw, positionalLaw).text;
1752
+ }
1753
+ export function buildDisplaySafeUrlChecked(raw, positionalLaw = displayUrlOf) {
1754
+ if (raw === "")
1755
+ return { text: "", failedClosed: false };
1756
+ if (!isHierarchicalUrl(raw))
1757
+ return { text: DISPLAY_SAFE_URL_TOKEN, failedClosed: false };
1758
+ const parsed = new URL(raw);
1759
+ if (shapeLawCredentialInQueryOrFragment(parsed.href)) {
1760
+ noteDisplaySafeFailClosed("query-and-fragment-withheld");
1761
+ const root = redactSecrets(`${parsed.protocol}//${parsed.host}${parsed.pathname}`).replace(/\/+$/, "");
1762
+ return { text: `${root}?${WITHHELD_VALUE_TOKEN}`, failedClosed: true };
1763
+ }
1764
+ return { text: redactSecrets(positionalLaw(raw)), failedClosed: false };
1765
+ }
1766
+ export function buildDisplaySafeRefusal(text, sourceUrls) {
1767
+ if (text === "")
1768
+ return text;
1769
+ if (sourceUrls.some((url) => url !== "" && buildDisplaySafeUrlChecked(url).failedClosed)) {
1770
+ noteDisplaySafeFailClosed("refusal-text-withheld");
1771
+ return REFUSAL_WITHHELD_TEXT;
1772
+ }
1773
+ return redactSecrets(text);
1774
+ }
1775
+ const REFUSAL_WITHHELD_TEXT = "[route refusal text withheld: it quotes a configured URL that carries a credential, and the upstream wording cannot be re-redacted safely once the URL has been rewritten for display. " +
1776
+ "The machine-readable verdict is on the pairing field beside this one; fix the configuration so the credential is not in the URL.]";
1777
+ function noteDisplaySafeFailClosed(level) {
1778
+ if (displaySafeFailClosedWarned)
1779
+ return;
1780
+ displaySafeFailClosedWarned = true;
1781
+ process.emitWarning(`display-safe URL composition fell back to fail-closed (${level}): the positional law reshaped the input so that a fragment the shape law had judged a credential survived into the product. ` +
1782
+ `The product is safe (the offending part is withheld), but this deployment has an input hitting the composition seam — report it upstream. No original bytes are logged.`, "SemaRedactionFailClosed");
1724
1783
  }
1725
1784
  //# sourceMappingURL=redact.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.88.2",
3
+ "version": "7.90.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",
@@ -59,7 +59,7 @@
59
59
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
60
60
  },
61
61
  "dependencies": {
62
- "@sema-agent/core": "7.23.5",
62
+ "@sema-agent/core": "7.24.2",
63
63
  "@sema-agent/settings-schema": "3.0.0",
64
64
  "e2b": "^2.28.0",
65
65
  "libsodium-wrappers": "^0.8.4",