@sema-agent/server 7.65.0 → 7.67.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 (59) hide show
  1. package/USAGE.md +6 -3
  2. package/dist/approval-card.d.ts +8 -0
  3. package/dist/approval.d.ts +3 -0
  4. package/dist/approval.js +2 -1
  5. package/dist/boot/governance-seams.js +1 -1
  6. package/dist/boot/resolve-spec.js +12 -5
  7. package/dist/boot/stores.js +3 -3
  8. package/dist/capabilities/hands-lane.d.ts +3 -1
  9. package/dist/capabilities/hands-lane.js +3 -3
  10. package/dist/config-catalog.js +2 -2
  11. package/dist/config-center/apply-effective.js +1 -1
  12. package/dist/config-center/skills-mcp.d.ts +3 -3
  13. package/dist/config-center/skills-mcp.js +17 -4
  14. package/dist/config-center/types.d.ts +11 -0
  15. package/dist/config-provider.js +1 -0
  16. package/dist/config-types.d.ts +27 -4
  17. package/dist/config.d.ts +25 -3
  18. package/dist/config.js +58 -30
  19. package/dist/deployment-governance.d.ts +8 -0
  20. package/dist/http/routes/approvals-assistant.js +18 -0
  21. package/dist/http/routes/diagnostics.d.ts +27 -2
  22. package/dist/http/routes/diagnostics.js +21 -1
  23. package/dist/http/routes/rules.js +15 -10
  24. package/dist/http/routes/tasks.js +9 -9
  25. package/dist/http/server.js +4 -1
  26. package/dist/mcp-tool-faces.d.ts +64 -0
  27. package/dist/mcp-tool-faces.js +93 -0
  28. package/dist/observability/fail-open.d.ts +8 -0
  29. package/dist/observability/fail-open.js +8 -0
  30. package/dist/observability/metrics.js +1 -1
  31. package/dist/permission-rule-vocab.d.ts +13 -21
  32. package/dist/permission-rule-vocab.js +6 -5
  33. package/dist/plugins/checkpoint-store-sql.d.ts +68 -7
  34. package/dist/plugins/checkpoint-store-sql.js +90 -25
  35. package/dist/plugins/local-checkpoint-store.d.ts +19 -1
  36. package/dist/plugins/local-checkpoint-store.js +6 -1
  37. package/dist/plugins/permission-rule-store-file.d.ts +3 -5
  38. package/dist/plugins/permission-rule-store-file.js +2 -1
  39. package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
  40. package/dist/plugins/permission-rule-store-sql.js +35 -20
  41. package/dist/plugins/pg-pool.js +5 -0
  42. package/dist/plugins/tidb-pool.js +11 -0
  43. package/dist/posture-source.d.ts +57 -0
  44. package/dist/posture-source.js +27 -0
  45. package/dist/read-face-posture.d.ts +9 -4
  46. package/dist/read-face-posture.js +7 -11
  47. package/dist/rules-consent.d.ts +15 -10
  48. package/dist/rules-consent.js +41 -21
  49. package/dist/session-titler.js +3 -12
  50. package/dist/task-mcp.d.ts +18 -4
  51. package/dist/task-mcp.js +23 -4
  52. package/dist/task-settings.d.ts +30 -0
  53. package/dist/task-settings.js +39 -18
  54. package/dist/tool-approval.d.ts +5 -4
  55. package/dist/tool-approval.js +3 -3
  56. package/dist/trace/core-keyset-guard.d.ts +2 -2
  57. package/dist/trace/project.d.ts +22 -0
  58. package/dist/trace/project.js +21 -1
  59. package/package.json +4 -4
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable } from "@sema-agent/core";
5
+ import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
6
6
  import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
7
7
  import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
8
8
  import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
@@ -13,6 +13,7 @@ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
15
  import { readFaceForPosture } from "./read-face-posture.js";
16
+ import {} from "./posture-source.js";
16
17
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
17
18
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
18
19
  function csv(name) {
@@ -321,6 +322,15 @@ export function configKnobTable() {
321
322
  function registerKnob(rec) {
322
323
  CONFIG_KNOBS.set(rec.name, rec);
323
324
  }
325
+ function postureSourceOfKnob(name) {
326
+ const source = CONFIG_KNOBS.get(name)?.source;
327
+ return source === "env" ? "env" : source === "posture" ? "posture" : "engine-default";
328
+ }
329
+ function postureSourceOfNumEnv(raw, singleUserTurnkey) {
330
+ if (raw !== undefined && raw !== "")
331
+ return "env";
332
+ return singleUserTurnkey ? "posture" : "engine-default";
333
+ }
324
334
  const BOOL_TRUE_WORDS = new Set(["true", "1", "yes", "on"]);
325
335
  const BOOL_FALSE_WORDS = new Set(["false", "0", "no", "off"]);
326
336
  function parseBoolWord(name, raw) {
@@ -918,49 +928,48 @@ function ccRuleFormGuidance(name) {
918
928
  `to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
919
929
  `to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
920
930
  }
921
- export function findCcRuleFormNames(names) {
922
- const out = [];
923
- for (const name of names) {
924
- const guidance = ccRuleFormGuidance(name);
925
- if (guidance !== undefined)
926
- out.push({ name, guidance });
927
- }
928
- return out;
929
- }
930
- function uncompilableRuleGuidance(name) {
931
- const issues = validatePermissionRules([{ rule: name, behavior: "deny" }]);
931
+ function uncompilableRuleGuidance(name, behavior, pathBases) {
932
+ const issues = validatePermissionRules([{ rule: name, behavior }], pathBases !== undefined ? { pathBases } : undefined);
932
933
  const issue = issues[0];
933
934
  if (issue === undefined)
934
935
  return undefined;
936
+ const ccGuidance = issue.code === "unsupported.bash_prefix" || issue.code === "unsupported.file_glob" ? ccRuleFormGuidance(name) : undefined;
937
+ if (ccGuidance !== undefined)
938
+ return ccGuidance;
935
939
  return (`${issue.code} — core's rule grammar cannot compile this name (${issue.message}), so a table keyed by it matches nothing. ` +
936
940
  `Tool names carry no backslashes and are far shorter than the 512-character rule cap; check the spelling, and use ` +
937
941
  `mcp__<server>__* / a2a__<peer>__* for "every tool on that peer"`);
938
942
  }
939
- export function findUnmatchableToolNames(names) {
943
+ export function findUnmatchableToolNames(names, opts) {
940
944
  const out = [];
941
945
  for (const name of names) {
942
- const ccGuidance = ccRuleFormGuidance(name);
943
- if (ccGuidance !== undefined) {
944
- out.push({ name, guidance: ccGuidance });
945
- continue;
946
+ if (opts.consumer === "tool-name-table") {
947
+ const ccGuidance = ccRuleFormGuidance(name);
948
+ if (ccGuidance !== undefined) {
949
+ out.push({ name, guidance: ccGuidance });
950
+ continue;
951
+ }
946
952
  }
953
+ if (opts.behavior === "allow")
954
+ continue;
947
955
  const retired = RETIRED_TOOL_NAMES.get(name);
948
956
  if (retired !== undefined) {
949
957
  out.push({ name, guidance: retired });
950
958
  continue;
951
959
  }
952
- const uncompilable = uncompilableRuleGuidance(name);
960
+ const uncompilable = uncompilableRuleGuidance(name, opts.behavior, opts.pathBases);
961
+ const subject = opts.consumer === "tool-name-table" ? name : (() => { const parsed = parseRuleText(name, opts.behavior); return "rule" in parsed ? parsed.rule.tool : name; })();
953
962
  if (uncompilable !== undefined) {
954
963
  out.push({ name, guidance: uncompilable });
955
964
  continue;
956
965
  }
957
- const ns = protocolOf(name);
966
+ const ns = protocolOf(subject);
958
967
  if (ns === undefined) {
959
- if (name.includes("__"))
968
+ if (subject.includes("__"))
960
969
  out.push({ name, guidance: `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => `${e.prefix}${presentToolName(name)}`).join(" or ")}` });
961
970
  continue;
962
971
  }
963
- if (ns.parse(name) === undefined) {
972
+ if (ns.parse(subject) === undefined) {
964
973
  out.push({ name, guidance: `incomplete ${ns.id} name — the live form is ${ns.prefix}<server>__<tool>, or ${ns.prefix}<server>__* to cover every tool on that server` });
965
974
  }
966
975
  }
@@ -974,7 +983,7 @@ const MAX_TIMER_DELAY_MS = 2_147_483_647;
974
983
  function streamApprovalConfig(gates) {
975
984
  const cfg = {
976
985
  enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
977
- windowMs: numEnv("STREAM_ASK_WINDOW_MS", "300000"),
986
+ windowMs: numEnv("STREAM_ASK_WINDOW_MS", gates.windowMsSource === "posture" ? "86400000" : "300000"),
978
987
  replayMax: numEnv("STREAM_APPROVAL_REPLAY_MAX", "50"),
979
988
  admitMaxPerTask: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_TASK", "32"),
980
989
  admitMaxPerOwner: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "256"),
@@ -990,6 +999,10 @@ function streamApprovalConfig(gates) {
990
999
  if (cfg.windowMs > MAX_TIMER_DELAY_MS) {
991
1000
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} exceeds the Node timer ceiling ${MAX_TIMER_DELAY_MS} (2^31-1 ms ≈ 24.8 days) — setTimeout SILENTLY collapses a larger delay to 1ms, so the window you meant to lengthen would expire immediately and every gated ask would take the unattended route on the next tick. Pick a window at or below the ceiling (and remember STREAM_ASK_WINDOW_MS + STREAM_APPROVAL_ADHOC_GRACE_MS must stay strictly below STREAM_APPROVAL_ORPHAN_TTL_MS).`);
992
1001
  }
1002
+ if (gates.windowMsSource === "posture" && cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
1003
+ gates.onPostureYield?.({ windowMs: cfg.windowMs, adhocGraceMs: cfg.adhocGraceMs, orphanTtlMs: cfg.orphanTtlMs });
1004
+ cfg.windowMs = 300_000;
1005
+ }
993
1006
  if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
994
1007
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
995
1008
  }
@@ -1014,14 +1027,25 @@ function unattendedApprovalPolicyEnv() {
1014
1027
  `Refusing to boot rather than treating a misspelled value as "unset": that runs the deployment on a policy the operator did not choose, with no trace anywhere`);
1015
1028
  }
1016
1029
  function parseApprovalDomain(ctx) {
1017
- const { postureOn } = ctx;
1030
+ const { postureOn, singleUserTurnkey, infraReady } = ctx;
1018
1031
  const unattendedPolicy = unattendedApprovalPolicyEnv();
1032
+ const streamAskWindowMsSource = postureSourceOfNumEnv(process.env.STREAM_ASK_WINDOW_MS, singleUserTurnkey);
1019
1033
  const toolApprovalEnabled = postureOn("TOOL_APPROVAL_ENABLED");
1034
+ let streamAskWindowMsEffectiveSource = streamAskWindowMsSource;
1035
+ const streamApproval = streamApprovalConfig({
1036
+ unattendedPolicy,
1037
+ toolApprovalEnabled,
1038
+ windowMsSource: streamAskWindowMsSource,
1039
+ onPostureYield: (facts) => {
1040
+ streamAskWindowMsEffectiveSource = "engine-default";
1041
+ process.stderr.write(`[config] stream_ask_window_posture_yielded: the single-user posture default STREAM_ASK_WINDOW_MS=${facts.windowMs} would violate the explicit STREAM_APPROVAL_ORPHAN_TTL_MS=${facts.orphanTtlMs} (window + ADHOC_GRACE ${facts.adhocGraceMs} must be strictly less) — falling back to 300000; set STREAM_ASK_WINDOW_MS explicitly to choose\n`);
1042
+ },
1043
+ });
1020
1044
  const directApprovalDoor = boolEnv("DIRECT_APPROVAL_DOOR", false);
1021
1045
  const dgHmacKeys = parseApprovalHmacKeys(process.env.APPROVAL_HMAC_KEYS, announceKeySetRejection("APPROVAL_HMAC_KEYS"));
1022
- const durableApproval = boolEnv("DURABLE_APPROVAL", false);
1046
+ const durableApproval = postureOn("DURABLE_APPROVAL", infraReady);
1023
1047
  const rejectUnmatchableToolNames = (envName, names) => {
1024
- const bad = findUnmatchableToolNames(names);
1048
+ const bad = findUnmatchableToolNames(names, { consumer: "tool-name-table", behavior: "deny" });
1025
1049
  if (bad.length > 0)
1026
1050
  throw new Error(formatUnmatchableToolNames(envName, bad));
1027
1051
  return names;
@@ -1092,7 +1116,9 @@ function parseApprovalDomain(ctx) {
1092
1116
  permissionRulesEnabled: boolEnv("PERMISSION_RULES_ENABLED", true),
1093
1117
  permissionRulesEnabledExplicit: parseBoolWord("PERMISSION_RULES_ENABLED", process.env["PERMISSION_RULES_ENABLED"]) !== undefined,
1094
1118
  streamAskWindowMarginMs: numEnv("STREAM_ASK_WINDOW_MARGIN_MS", "10000"),
1095
- streamApproval: streamApprovalConfig({ unattendedPolicy, toolApprovalEnabled }),
1119
+ streamApproval,
1120
+ streamAskWindowMsSource: streamAskWindowMsEffectiveSource,
1121
+ durableApprovalSource: postureSourceOfKnob("DURABLE_APPROVAL"),
1096
1122
  unattendedApprovalPolicy: unattendedPolicy,
1097
1123
  approvalTimeoutSec: numEnv("APPROVAL_TIMEOUT_SEC", "0"),
1098
1124
  };
@@ -1659,6 +1685,7 @@ function parseOrchestrationDomain(ctx) {
1659
1685
  ? { sandboxPkgSource: pkgSourceEnv() }
1660
1686
  : {}),
1661
1687
  sessionAutoTitle: boolEnv("SESSION_AUTO_TITLE", true),
1688
+ sessionAutoTitleSource: postureSourceOfKnob("SESSION_AUTO_TITLE"),
1662
1689
  selectEnvironmentTool: boolEnv("SELECT_ENVIRONMENT_TOOL", true),
1663
1690
  envFactsEnabled: boolEnv("SANDBOX_ENV_FACTS", true),
1664
1691
  toolDeferLongtail: boolEnv("TOOL_DEFER_LONGTAIL", false),
@@ -1885,7 +1912,7 @@ const MODEL_PLANE_GROUP_KEYS = [
1885
1912
  ];
1886
1913
  const APPROVAL_GROUP_KEYS = [
1887
1914
  "approvalRequire", "approvalDeny", "approvalTimeoutSec", "approvalAutoBudget", "approvalNeverAuto",
1888
- "approvalHmacKeys", "durableApproval", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1915
+ "approvalHmacKeys", "durableApproval", "durableApprovalSource", "streamAskWindowMsSource", "directApprovalDoor", "directDoorActive", "resourceSuspend",
1889
1916
  "resourceSuspendTtlSec", "resourceSuspendTotalTokens", "resourceSuspendTotalBudgetUsd", "resourceSuspendMaxSlices", "askQuestionEnabled", "questionThrottle", "toolApprovalEnabled", "permissionRulesEnabled", "permissionRulesEnabledExplicit", "streamAskWindowMarginMs", "streamApproval", "unattendedApprovalPolicy", "mcpElicitation",
1890
1917
  "sensitiveWritePatterns", "writeProtectedPaths", "manualModeShellGate", "permissionsDisableAutoMode", "crossSessionInbound", "crossSessionDialogExpiry",
1891
1918
  ];
@@ -1907,7 +1934,7 @@ const ORCHESTRATION_GROUP_KEYS = [
1907
1934
  "workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
1908
1935
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
1909
1936
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
1910
- "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1937
+ "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1911
1938
  "imageBakes", "readFace", "readFaceSource", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1912
1939
  ];
1913
1940
  const LIMITS_HTTP_GROUP_KEYS = [
@@ -1968,12 +1995,13 @@ export function loadConfig() {
1968
1995
  const postureOn = (name, infraReady = true) => {
1969
1996
  const explicit = parseBoolWord(name, process.env[name]);
1970
1997
  const value = explicit ?? (singleUserTurnkey && infraReady);
1971
- registerKnob({ name, polarity: "posture", value, source: explicit === undefined ? "posture" : "env" });
1998
+ registerKnob({ name, polarity: "posture", value, source: explicit !== undefined ? "env" : singleUserTurnkey ? "posture" : "default" });
1972
1999
  return value;
1973
2000
  };
1974
2001
  const store = parseStoreDomain({ requirePrincipal });
1975
2002
  const modelPlane = parseModelDomain();
1976
- const approval = parseApprovalDomain({ postureOn });
2003
+ const durableStoreReady = store.dbBackend !== "memory";
2004
+ const approval = parseApprovalDomain({ postureOn, singleUserTurnkey, infraReady: durableStoreReady });
1977
2005
  const memory = parseMemoryDomain({ requirePrincipal });
1978
2006
  const auth = parseAuthDomain({ requirePrincipal });
1979
2007
  const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
@@ -55,6 +55,14 @@ export interface PathAdjudication {
55
55
  readonly env: ExecutionEnv;
56
56
  /** 相对路径基准(core 的 `rootPath`)。缺席 ⇒ 相对形在真身那层判不了,补词法臂(见下)。 */
57
57
  readonly cwd?: string;
58
+ /** 🔴 S-177:**真任务根**(权限规则模式的基)。与上面那个 `cwd` 分开:后者在 host 腿的无 session-cwd
59
+ * 形上是一个永不创建的哨兵目录 —— 对写门 fail-safe,对规则 fail-open。理由逐字见
60
+ * `task-settings.ts` 的 `FsWriteGateWiring.taskRoot`。缺席 ⇒ 需要基的规则拼法被 core 整表拒(fail-loud)。 */
61
+ readonly taskRoot?: string;
62
+ /** 🔴 S-177 / codex r2:**执行环境的 home**(`~/…` 形规则的基)。只在引擎与执行同机时在场 ——
63
+ * 远端车道上 server 的 home 是另一个用户的目录,回落它 = 规则静默失效。逐字见
64
+ * `task-settings.ts` 的 `FsWriteGateWiring.taskHome`。 */
65
+ readonly taskHome?: string;
58
66
  }
59
67
  /** [ref]([ref] 案二):durable 部署上的 AskUserQuestion 门的活体面探针(QuestionCoordinator 的切面)。 */
60
68
  export interface LiveQuestionFace {
@@ -1,4 +1,6 @@
1
+ import { screenedGate } from "../../trace/project.js";
1
2
  import { personDecision } from "../../host-decision.js";
3
+ import { sameExecutionRecord } from "@sema-agent/core";
2
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
3
5
  import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
4
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -71,6 +73,21 @@ async function decideIdempotentReplay(cs, args) {
71
73
  if (row.payloadFp === null || row.payloadFp !== requestFp)
72
74
  return undefined;
73
75
  }
76
+ const executionOutcome = (() => {
77
+ const records = rows.map((r) => r.executionOutcome);
78
+ const first = records[0];
79
+ if (first === null || first === undefined)
80
+ return undefined;
81
+ for (const r of records) {
82
+ if (r === null)
83
+ return undefined;
84
+ if (!sameExecutionRecord(r, first)) {
85
+ warn("decide_replay_execution_outcome_divergent", { sessionId, rows: rows.length });
86
+ return undefined;
87
+ }
88
+ }
89
+ return screenedGate(first);
90
+ })();
74
91
  return {
75
92
  status: 200,
76
93
  body: {
@@ -78,6 +95,7 @@ async function decideIdempotentReplay(cs, args) {
78
95
  idempotent: true,
79
96
  decision,
80
97
  bindingEnforced: true,
98
+ ...(executionOutcome !== undefined ? { executionOutcome } : {}),
81
99
  },
82
100
  };
83
101
  }
@@ -1,3 +1,15 @@
1
+ /**
2
+ * [ref] 件二+件三 —— **装配自证**域:启动期静态自检(拒启判据)与 operator 诊断读面
3
+ * (`GET /v1/diagnostics/wiring`)。
4
+ *
5
+ * 🔴 两件为什么同住一个模块:它们回答的是同一个问题(「这台 worker 到底接了什么线」),只是一个在 boot
6
+ * 用它拒绝启动、一个在运行期把它读给 operator。分成两处 = 拒启用的判据与诊断报的读数会各自演化,而
7
+ * 「拒启说没矛盾、诊断页显示矛盾」正是本设计要消灭的那类装配谎言。故 boot 半场是本文件导出的**纯函数**
8
+ * (可单测、无 I/O),路由半场只是它的读口。
9
+ *
10
+ * 分层:本模块不值 import `server.ts`(那条边会闭合运行时装载环),对它只有 `import type`。
11
+ */
12
+ import { type PostureKnobReading, type PostureSource } from "../../posture-source.js";
1
13
  import type { IncomingMessage, ServerResponse } from "node:http";
2
14
  import type { WiringManifest } from "@sema-agent/core";
3
15
  import { type StreamApprovalGate, type StreamApprovalGateInput } from "../../tool-approval.js";
@@ -7,10 +19,17 @@ import type { RouteCtx } from "../route-ctx.js";
7
19
  export type StreamApprovalGateReading = "active" | Extract<StreamApprovalGate, {
8
20
  active: false;
9
21
  }>["reason"];
10
- /** server 自己的装配谓词读数(core 的静态半场之外的那一半:本仓在这台 worker 上做出的判断)。 */
22
+ /** server 自己的装配谓词读数(core 的静态半场之外的那一半:本仓在这台 worker 上做出的判断)。
23
+ *
24
+ * 🔴 S-178:三个 **posture 家族**旋钮改报 `{value, source, note}`(与 S-167 的 `readFace` 同一张四词表、
25
+ * 同一只 note 函数)。裸布尔答不出裁定的补偿项要求的那句话 —— 单机形升级后 `durableApproval` 缺席即
26
+ * ON、审批窗缺席即 24h,运维必须在一个读面上看得见「这是 posture 派生的」以及「怎么钉回」。
27
+ * `streamApproval` 那一格是**门读数**(另一根轴:这条腿现在通不通),不是旋钮,故形不变。 */
11
28
  export interface ServerWiringGates {
12
29
  streamApproval: StreamApprovalGateReading;
13
- durableApproval: boolean;
30
+ durableApproval: PostureKnobReading<boolean>;
31
+ streamAskWindowMs: PostureKnobReading<number>;
32
+ sessionAutoTitle: PostureKnobReading<boolean>;
14
33
  checkpointStore: boolean;
15
34
  }
16
35
  /** `buildServerWiringGates` 的入参 = 三个谓词各自的**真输入**(不接受已算好的布尔:那样这里读到的就
@@ -23,6 +42,12 @@ export interface ServerWiringGateInput {
23
42
  /** 在场性即事实;本面不取实例,故 `unknown` 足够(取窄型会把整棵 store 类型树拖进诊断域)。 */
24
43
  checkpointStore: unknown;
25
44
  durableApproval: boolean;
45
+ /** S-178:三个 posture 旋钮的**来源**,取自 config 上与值同处写的那一格(不在这里重判)。 */
46
+ durableApprovalSource: PostureSource;
47
+ streamAskWindowMs: number;
48
+ streamAskWindowMsSource: PostureSource;
49
+ sessionAutoTitle: boolean;
50
+ sessionAutoTitleSource: PostureSource;
26
51
  }
27
52
  /** 纯数据产物(`build*`):三个门的当前读数。`streamApproval` 走**单一谓词**
28
53
  * `resolveStreamApprovalGate` —— 与 `/v1/capabilities` 的 `streamApproval` 位、与回决口的 501 门同一个
@@ -1,3 +1,4 @@
1
+ import { buildPostureKnobReading } from "../../posture-source.js";
1
2
  import { buildReadFaceWiring } from "../../read-face-posture.js";
2
3
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
3
4
  import { sendJson, sendError } from "../send.js";
@@ -11,7 +12,21 @@ export function buildServerWiringGates(input) {
11
12
  });
12
13
  return {
13
14
  streamApproval: gate.active ? "active" : gate.reason,
14
- durableApproval: input.durableApproval,
15
+ durableApproval: buildPostureKnobReading(input.durableApproval, {
16
+ knob: "DURABLE_APPROVAL",
17
+ source: input.durableApprovalSource,
18
+ postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set) composed with a durable store being present: park/resume and the /v1/approvals routes are on by default — set DURABLE_APPROVAL=false to pin them off",
19
+ }),
20
+ streamAskWindowMs: buildPostureKnobReading(input.streamAskWindowMs, {
21
+ knob: "STREAM_ASK_WINDOW_MS",
22
+ source: input.streamAskWindowMsSource,
23
+ postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): an approval card waits 24h instead of 5 minutes, so stepping away does not settle the run unattended — set STREAM_ASK_WINDOW_MS to pin your own window",
24
+ }),
25
+ sessionAutoTitle: buildPostureKnobReading(input.sessionAutoTitle, {
26
+ knob: "SESSION_AUTO_TITLE",
27
+ source: input.sessionAutoTitleSource,
28
+ postureMeaning: "this knob has no posture arm — its default is the same on every deployment shape",
29
+ }),
15
30
  checkpointStore: input.checkpointStore !== undefined,
16
31
  };
17
32
  }
@@ -100,6 +115,11 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
100
115
  backend: deps.backend,
101
116
  checkpointStore: deps.checkpointStore,
102
117
  durableApproval: deps.config.durableApproval === true,
118
+ durableApprovalSource: deps.config.durableApprovalSource,
119
+ streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
120
+ streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
121
+ sessionAutoTitle: deps.config.sessionAutoTitle === true,
122
+ sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
103
123
  }),
104
124
  });
105
125
  return;
@@ -5,8 +5,8 @@ import { createLogger } from "../../observability/logger.js";
5
5
  import { RULE_IMPORT_RETRY_AFTER_SEC, MAX_RULE_SCOPE_CHARS, parseRuleScope, serializeRuleScope } from "../../rules-consent.js";
6
6
  import { CONTROL_AND_BIDI_CHARS } from "../../text-bidi.js";
7
7
  import { HttpError } from "../../security.js";
8
- import { MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
9
- import { PERSISTED_RULE_MATCHES, PERSISTED_RULE_TOOLS } from "../../permission-rule-vocab.js";
8
+ import { MAX_RULE_TEXT_CHARS, RULE_BEHAVIORS } from "@sema-agent/core";
9
+ import { PERSISTED_RULE_MATCHES, isPersistedRuleTool } from "../../permission-rule-vocab.js";
10
10
  const logger = createLogger();
11
11
  export const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
12
12
  export const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
@@ -46,7 +46,8 @@ const LocalImportRowSchema = z
46
46
  .object({
47
47
  rule: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
48
48
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
49
- tool: z.enum(PERSISTED_RULE_TOOLS),
49
+ behavior: z.enum(RULE_BEHAVIORS),
50
+ tool: z.string().min(1).max(190).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
50
51
  match: z.enum(PERSISTED_RULE_MATCHES),
51
52
  command: z.string().min(1).max(MAX_RULE_TEXT_CHARS),
52
53
  adds: z.array(LocalImportAddSchema).max(64),
@@ -58,16 +59,18 @@ const LocalImportPrepareBodySchema = z.object({ rows: z.array(LocalImportRowSche
58
59
  const LocalImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
59
60
  const RuleRevokeBodySchema = z
60
61
  .object({
62
+ behavior: z.enum(RULE_BEHAVIORS),
61
63
  rule: z.string().min(1).max(1024),
62
64
  scope: z.string().min(1).max(MAX_RULE_SCOPE_CHARS),
63
65
  principal: z.string().min(1).max(190).optional(),
64
66
  })
65
67
  .strict();
68
+ const RULES_CURSOR_FORMAT = 2;
66
69
  const RulesCursorSchema = z
67
- .object({ rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
70
+ .object({ v: z.literal(RULES_CURSOR_FORMAT), rev: z.number().int().nonnegative().safe(), principal: z.string(), scope: z.string(), after: z.string() })
68
71
  .strict();
69
72
  function encodeRulesCursor(c) {
70
- return Buffer.from(JSON.stringify(c)).toString("base64url");
73
+ return Buffer.from(JSON.stringify({ v: RULES_CURSOR_FORMAT, ...c })).toString("base64url");
71
74
  }
72
75
  function decodeRulesCursor(raw) {
73
76
  try {
@@ -78,7 +81,7 @@ function decodeRulesCursor(raw) {
78
81
  return { ok: false, why: err instanceof Error ? err.message : String(err) };
79
82
  }
80
83
  }
81
- const rowKey = (r) => `${r.scope}\u0000${r.rule}`;
84
+ const rowKey = (r) => `${r.scope}\u0000${r.rule}\u0000${r.behavior}`;
82
85
  export async function handleRules(req, res, url, ctx) {
83
86
  const miss = { fell: false };
84
87
  await handleRulesBody(req, res, url, ctx, miss);
@@ -132,7 +135,7 @@ async function handleRulesBody(req, res, url, ctx, miss) {
132
135
  if (isLocalPrepare) {
133
136
  const parsed = LocalImportPrepareBodySchema.safeParse(rawBody);
134
137
  if (!parsed.success) {
135
- sendError(res, 400, "request.body_shape", "body must be {rows:[{rule,scope,tool,match,command,adds}]}");
138
+ sendError(res, 400, "request.body_shape", "body must be {rows:[{behavior,rule,scope,tool,match,command,adds}]}");
136
139
  return;
137
140
  }
138
141
  const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
@@ -241,7 +244,9 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
241
244
  sendError(res, 400, "request.query_invalid", "the rules cursor was minted at a different revision — restart the listing", { rev: listing.rev });
242
245
  return;
243
246
  }
244
- const matching = listing.rows.filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after));
247
+ const matching = listing.rows
248
+ .filter((r) => (filterKey === "" || r.scope === filterKey) && (cursor === undefined || rowKey(r) > cursor.after))
249
+ .sort((x, y) => (rowKey(x) < rowKey(y) ? -1 : rowKey(x) > rowKey(y) ? 1 : 0));
245
250
  const page = matching.slice(0, limit);
246
251
  const last = page[page.length - 1];
247
252
  const nextCursor = matching.length > limit && last !== undefined
@@ -252,7 +257,7 @@ async function handleRuleList(req, res, caller, lane, operatorPrincipals) {
252
257
  async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals) {
253
258
  const parsed = RuleRevokeBodySchema.safeParse(rawBody);
254
259
  if (!parsed.success) {
255
- sendError(res, 400, "request.body_shape", "body must be {rule, scope} with an optional principal");
260
+ sendError(res, 400, "request.body_shape", "body must be {behavior, rule, scope} with an optional principal");
256
261
  return;
257
262
  }
258
263
  const scope = parseRuleScope(parsed.data.scope);
@@ -265,7 +270,7 @@ async function handleRuleRevoke(res, rawBody, caller, lane, operatorPrincipals)
265
270
  sendError(res, 403, "auth.operator_only", "revoking another principal's rules is an operator-only action");
266
271
  return;
267
272
  }
268
- const outcome = await lane.removeRule({ principal: target.principal, rule: parsed.data.rule, scope });
273
+ const outcome = await lane.removeRule({ principal: target.principal, behavior: parsed.data.behavior, rule: parsed.data.rule, scope });
269
274
  switch (outcome.status) {
270
275
  case "removed":
271
276
  sendJson(res, 200, { status: "removed", rev: outcome.rev, stillLive: outcome.stillLive });
@@ -12,7 +12,7 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
12
12
  import { buildCancelContext } from "../../run-cancel-context.js";
13
13
  import { recordResourceWindow } from "../../resource-window.js";
14
14
  import { redactSecrets } from "../../trace/redact.js";
15
- import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
15
+ import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, toolRosterDeltaEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
16
16
  import { cascadeConfig, runMeta } from "../run-meta.js";
17
17
  import { scopedIdempotencyKey } from "../idempotency.js";
18
18
  import { sendJson, sendError, sseHeaders } from "../send.js";
@@ -231,20 +231,15 @@ async function handleTasksBody(req, res, url, ctx, miss) {
231
231
  markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
232
232
  let ledgerSink;
233
233
  let ledgerTerminal = false;
234
- if (detachOnDisconnect && durableTaskId && deps.runStore) {
235
- const rs = deps.runStore;
236
- const tid = durableTaskId;
237
- ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
238
- usageKey = durableTaskId;
239
- if (usageKey)
240
- deps.modelUsage?.register(usageKey);
241
- }
242
234
  if (deps.checkpointStore && prepared.spec.sessionId) {
243
235
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
244
236
  }
245
237
  if (durableTaskId && deps.runStore) {
246
238
  const rs = deps.runStore;
247
239
  const tid = durableTaskId;
240
+ ledgerSink = createLedgerSink({ appendEvent: (seq, type, data) => rs.appendEvent(tid, seq, type, data), persistThinking: deps.config.traceThinking, onActivity: (kind, statusData) => recordTurnActivity(tid, kind, statusData) });
241
+ usageKey = tid;
242
+ deps.modelUsage?.register(tid);
248
243
  const owner = principal ?? null;
249
244
  const abortFromVerb = () => {
250
245
  cancelledViaVerb.add(tid);
@@ -484,6 +479,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
484
479
  : wiringManifestEventData(ev)),
485
480
  });
486
481
  }
482
+ else if (ev.type === "tool_roster_delta") {
483
+ const rosterDelta = toolRosterDeltaEventData(ev);
484
+ if (rosterDelta !== undefined)
485
+ sseData(res, { type: "tool_roster_delta", ...rosterDelta });
486
+ }
487
487
  else if (ev.type === "human_input") {
488
488
  sseData(res, { type: "human_input", ...humanInputEventData(ev) });
489
489
  }
@@ -115,6 +115,7 @@ export function validateUserSkills(skills) {
115
115
  }
116
116
  return null;
117
117
  }
118
+ const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
118
119
  const MAX_BODY = 8 * 1024 * 1024;
119
120
  const MAX_IMAGES_PER_REQUEST = 20;
120
121
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
@@ -125,7 +126,7 @@ function mentionableNames(config, catalog) {
125
126
  }
126
127
  function isCheckpointReopenedFailure(r) {
127
128
  const code = failureCodeOf(r.terminal);
128
- return code === "resume.env_failed" || code === "resume.tool_unavailable";
129
+ return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
129
130
  }
130
131
  function nestedParkStatusOf(r) {
131
132
  const paused = nestedPauseOf(r.terminal);
@@ -323,6 +324,7 @@ export function createHttpServer(rawDeps) {
323
324
  ...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
324
325
  configHash,
325
326
  pid: process.pid,
327
+ startedAt: PROCESS_STARTED_AT_MS,
326
328
  port: deps.config.port,
327
329
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
328
330
  ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
@@ -2434,6 +2436,7 @@ const WORKER_SWAP_REDEEMABLE = {
2434
2436
  env_factory_missing: true,
2435
2437
  governed_unwired: true,
2436
2438
  origin_missing: true,
2439
+ ledger_event_unknown: true,
2437
2440
  real_approval_damaged: false,
2438
2441
  real_approval_forged: false,
2439
2442
  read_face_damaged: false,
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `McpServerSpec.toolFaces` —— MCP 工具的**面覆盖层**(core 7.8.0 [ref] §1.1/§2.7;声明面
3
+ * settings-schema 1.9.0)。一条规则、三条腿共用(请求腿 `task-mcp.ts normalizeServer` / center 腿
4
+ * `config-center/skills-mcp.ts resolveMcpServers` / 本地 config.d 腿 `config-provider.ts` 只做承载),
5
+ * 与 {@link import("./mcp-source-label.js").readMcpSourceLabel} 同一个「判据单点、三腿一致」的形。
6
+ *
7
+ * ## 分工(为什么本层**不**复述 core 的那张表)
8
+ * 覆盖层的**语义**校验属 core 的摄入侧,而且只有它做得了:`pathTarget.param` 是不是这只工具**物化后
9
+ * schema** 的顶层属性、审批卡要的键在不在、`ruleFace` 指的是不是标量键 —— 三件都要看远端 server 真发
10
+ * 回来的 schema,本层手里根本没有。core 的处置也已经定死:不合 ⇒ **这只工具在这条腿上不挂**,并通告
11
+ * 一次 `config.tool_face_invalid`(亲读 `dist/core/runner/tool-face-overlay.js` 的 `return undefined` 臂 ——
12
+ * 该文件 d.ts 头注里那句「the tool mounts BARE」是**过期**的,以实现为准)。
13
+ *
14
+ * 本层因此只做 core 的校验器**假定成立**的那一件事:**JSON 结构合不合 TS 型**。理由是硬的 —— core 的
15
+ * `toolFaceProblem` 是按 TS 型写的,`renderHints.summaryParams` 是个字符串时它会 `.find is not a function`
16
+ * 地把 prepare 整条炸掉,`ruleFace.params` 是个数字时 `for...of` 直接抛。原样透传一份任意 JSON 不是
17
+ * 「解析透明」,是把类型错误变成运行期崩溃。
18
+ *
19
+ * 闭词(`family` / `pathTarget.access` / `renderHints.approvalCard`)判据**取自 core 的导出常量**
20
+ * (`TOOL_FAMILIES` / `TOOL_PATH_ACCESSES` / `TOOL_APPROVAL_CARDS`),本仓一个词都不枚举 —— 与
21
+ * `MCP_DELIVERY_VERDICTS` 的消费姿势同款:词表单一属主在 core,抄一遍就是抄了一张会漂的表。
22
+ *
23
+ * ## 坏形的方向:**整条 server 不进**(fail-closed)
24
+ * 🔴 这一条被 codex 对抗复审 r1 的 [high] **翻正过一次**,把翻正的理由留在这里,因为它是本模块最容易
25
+ * 想错的一格:初版按 `contentOrigin` 的先例判「丢声明、留 server + 留痕」,而那条先例管的是**放宽**键
26
+ * (丢它 ⇒ 回落到过度标记 = fail-closed 的那一侧)。`toolFaces` 是**收窄**声明:它声明的 `pathTarget`
27
+ * 正是敏感路径策略、写围栏、编辑跟踪读的那一格 —— 丢掉它,那只工具就**无面挂载**,运维声明的保护
28
+ * 静默失效。codex 实测(同一只 MCP 写工具、同一个 `/work/.env` 目标,仅把 `renderHints.summaryParams`
29
+ * 从数组写成字符串):`createSensitivePathPolicy` 从 deny 变成 allow。一条 warn 补偿不了这个方向。
30
+ *
31
+ * 所以判据回到本仓对**收窄键**已经成文的那一条(`task-mcp.ts` 的 `allowTools`,[ref] / [ref] codex F1):
32
+ * **一个键在场就必须合法,否则这条 server 整条不进**。三问逐条:
33
+ * ① 谁需要 —— 在部署侧给 MCP 工具声明写围栏 / 审批卡 / 显示名的运维;
34
+ * ② 谁受伤 —— 覆盖层写错的那台 server:它**整条**不挂(请求腿进 `dropped` 并点名上报,center 腿跳过
35
+ * 该条、兄弟 server 照挂)。代价是真的(少一套工具),但它**响亮且立刻**;反方向的代价是一条
36
+ * **静默失效的安全声明**,而后者按 [ref] 不许存在于安全轴上;
37
+ * ③ 拿什么补偿 —— 报出来的名字本身:请求腿 `mcp_injection_dropped{dropped:[name]}`、center 腿
38
+ * `sema_registry_mcp_tool_faces_invalid{server,problem,note}` 一条点名 warn(`problem` 里带工具名与哪一段不合;运维照着改那一行即可)。
39
+ * 每只工具的面是**整只**判(core 明写 "applied whole or not at all");而**任一只**判不出形 ⇒ 整条 server
40
+ * 不进 —— 「留下好的那几只」同样是让坏的那一只无面挂载,与整张丢没有区别。
41
+ */
42
+ import { type McpServerSpec } from "@sema-agent/core";
43
+ type ToolFaces = NonNullable<McpServerSpec["toolFaces"]>;
44
+ /**
45
+ * 一条 server 条目的 `toolFaces` 判形结果:
46
+ * · `{ ok: true, faces }` —— 合形(`faces` 缺席 = 这台 server 没有覆盖层,或声明了一个**空**覆盖层:
47
+ * 两者对 core 同义,都不铸键);
48
+ * · `{ ok: false, problem }` —— 判不出形,**整条 server 不进**(方向与理由见模块头注),`problem` 是
49
+ * 给运维的一句点名(只带工具名与哪一段不合,不带面的值)。
50
+ */
51
+ export type McpToolFacesRead = {
52
+ ok: true;
53
+ faces?: ToolFaces;
54
+ } | {
55
+ ok: false;
56
+ problem: string;
57
+ };
58
+ /**
59
+ * 从任一腿的原始条目里读出 `toolFaces` —— **三条腿共用的唯一判据点**。
60
+ * 缺席 ⇒ 合形无覆盖层;整只不是对象 / **任一只**工具的面判不出形 ⇒ `ok:false`(调用方整条 server 不收)。
61
+ */
62
+ export declare function readMcpToolFaces(v: unknown): McpToolFacesRead;
63
+ export {};
64
+ //# sourceMappingURL=mcp-tool-faces.d.ts.map