@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.
- package/USAGE.md +6 -3
- package/dist/approval-card.d.ts +8 -0
- package/dist/approval.d.ts +3 -0
- package/dist/approval.js +2 -1
- package/dist/boot/governance-seams.js +1 -1
- package/dist/boot/resolve-spec.js +12 -5
- package/dist/boot/stores.js +3 -3
- package/dist/capabilities/hands-lane.d.ts +3 -1
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-center/skills-mcp.d.ts +3 -3
- package/dist/config-center/skills-mcp.js +17 -4
- package/dist/config-center/types.d.ts +11 -0
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +27 -4
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/routes/approvals-assistant.js +18 -0
- package/dist/http/routes/diagnostics.d.ts +27 -2
- package/dist/http/routes/diagnostics.js +21 -1
- package/dist/http/routes/rules.js +15 -10
- package/dist/http/routes/tasks.js +9 -9
- package/dist/http/server.js +4 -1
- package/dist/mcp-tool-faces.d.ts +64 -0
- package/dist/mcp-tool-faces.js +93 -0
- package/dist/observability/fail-open.d.ts +8 -0
- package/dist/observability/fail-open.js +8 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/permission-rule-vocab.d.ts +13 -21
- package/dist/permission-rule-vocab.js +6 -5
- package/dist/plugins/checkpoint-store-sql.d.ts +68 -7
- package/dist/plugins/checkpoint-store-sql.js +90 -25
- package/dist/plugins/local-checkpoint-store.d.ts +19 -1
- package/dist/plugins/local-checkpoint-store.js +6 -1
- package/dist/plugins/permission-rule-store-file.d.ts +3 -5
- package/dist/plugins/permission-rule-store-file.js +2 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
- package/dist/plugins/permission-rule-store-sql.js +35 -20
- package/dist/plugins/pg-pool.js +5 -0
- package/dist/plugins/tidb-pool.js +11 -0
- package/dist/posture-source.d.ts +57 -0
- package/dist/posture-source.js +27 -0
- package/dist/read-face-posture.d.ts +9 -4
- package/dist/read-face-posture.js +7 -11
- package/dist/rules-consent.d.ts +15 -10
- package/dist/rules-consent.js +41 -21
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +18 -4
- package/dist/task-mcp.js +23 -4
- package/dist/task-settings.d.ts +30 -0
- package/dist/task-settings.js +39 -18
- package/dist/tool-approval.d.ts +5 -4
- package/dist/tool-approval.js +3 -3
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +22 -0
- package/dist/trace/project.js +21 -1
- 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
|
-
|
|
922
|
-
const
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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(
|
|
966
|
+
const ns = protocolOf(subject);
|
|
958
967
|
if (ns === undefined) {
|
|
959
|
-
if (
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -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
|