@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/session-titler.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
2
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
2
3
|
export const TITLE_MAX_CHARS = 80;
|
|
3
4
|
const OBJECTIVE_SNIPPET_CHARS = 600;
|
|
4
5
|
const LLM_TIMEOUT_MS = 15_000;
|
|
5
6
|
const NO_ROW_RETRY_MS = 20_000;
|
|
6
7
|
const FAIL_CAUSE_KEY_CHARS = 80;
|
|
7
|
-
const FAIL_CAUSE_CAP = 100;
|
|
8
8
|
const SEEN_CAP = 10_000;
|
|
9
9
|
export function sanitizeTitle(raw) {
|
|
10
10
|
const oneLine = raw.replace(/[\r\n]+/g, " ").replace(CONTROL_AND_BIDI_STRIP_RE, "").trim();
|
|
@@ -22,18 +22,9 @@ export function buildTitlePrompt(objective) {
|
|
|
22
22
|
}
|
|
23
23
|
export function createSessionTitler(deps) {
|
|
24
24
|
const seen = new Set();
|
|
25
|
-
const warnedCauses = new Set();
|
|
26
25
|
const logFailure = (sessionId, error) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (warnedCauses.has(cause)) {
|
|
30
|
-
deps.logger?.debug?.("session_title_failed", meta);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (warnedCauses.size >= FAIL_CAUSE_CAP)
|
|
34
|
-
warnedCauses.clear();
|
|
35
|
-
warnedCauses.add(cause);
|
|
36
|
-
deps.logger?.warn?.("session_title_failed", { ...meta, note: "同 cause 后续降为 debug 级" });
|
|
26
|
+
deps.logger?.debug?.("session_title_failed", { sessionId, error: error.slice(0, 200) });
|
|
27
|
+
recordFailOpen("server.session-titler.gateway-unreachable", error.slice(0, FAIL_CAUSE_KEY_CHARS));
|
|
37
28
|
};
|
|
38
29
|
return {
|
|
39
30
|
maybeTitle(sessionId, objective, model) {
|
package/dist/task-mcp.d.ts
CHANGED
|
@@ -110,11 +110,21 @@ export declare function assertRequestMcpUnlocked(bodyMcp: unknown, lockedKeys: R
|
|
|
110
110
|
*/
|
|
111
111
|
export declare function assertRequestMcpContentOrigin(bodyMcp: unknown): void;
|
|
112
112
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* The ONE predicate for "this MCP server name belongs to the deployment": the scenario baseline (what mounted for
|
|
114
|
+
* this scenario) ∪ every name the deployment DECLARED (`CenterMcp.reservedNames` — mounted or not, any scenario).
|
|
115
|
+
* A caller can ADD servers but can never SHADOW or REFILL a deployment-declared one: a center entry that was skipped
|
|
116
|
+
* (disabled / missing env ref / malformed `toolFaces`) still occupies its name, otherwise "skipped" would silently
|
|
117
|
+
* mean "free for the caller to remount without the operator's `allowTools` / `toolFaces`" (车CF codex r2 [high]).
|
|
118
|
+
* Same posture as `mergeUserSkills`.
|
|
116
119
|
*/
|
|
117
|
-
export declare function
|
|
120
|
+
export declare function deploymentClosedMcpNames(baseline: McpServerSpec[] | undefined, reservedNames: readonly string[]): ReadonlySet<string>;
|
|
121
|
+
/**
|
|
122
|
+
* Merge GATED per-request MCP servers OVER the deployment baseline (center/config). Names closed by the deployment
|
|
123
|
+
* ({@link deploymentClosedMcpNames}) are dropped from `gated`. `reservedNames` has NO default: a security predicate's
|
|
124
|
+
* input is never implied — a caller with no deployment declaration passes `[]` and says so. Returns the baseline unchanged
|
|
125
|
+
* when nothing is added.
|
|
126
|
+
*/
|
|
127
|
+
export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, gated: McpServerSpec[], reservedNames: readonly string[]): McpServerSpec[] | undefined;
|
|
118
128
|
/**
|
|
119
129
|
* The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
|
|
120
130
|
* caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
|
|
@@ -127,6 +137,10 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
|
127
137
|
};
|
|
128
138
|
requirePrincipal?: boolean;
|
|
129
139
|
lockedConfigKeys?: readonly LockedKey[];
|
|
140
|
+
/** The deployment's declared-name set rides on the same config object the gate reads (one owner, no second arg). */
|
|
141
|
+
mcpServers?: {
|
|
142
|
+
reservedNames: readonly string[];
|
|
143
|
+
};
|
|
130
144
|
}, logger?: {
|
|
131
145
|
info?(m: string, meta?: unknown): void;
|
|
132
146
|
warn(m: string, meta?: unknown): void;
|
package/dist/task-mcp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { resolveComplianceDenies } from "@sema-agent/core";
|
|
|
2
2
|
import { HttpError } from "./security.js";
|
|
3
3
|
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
|
|
4
4
|
import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
5
|
+
import { readMcpToolFaces } from "./mcp-tool-faces.js";
|
|
5
6
|
export const MAX_REQUEST_MCP_SERVERS = 32;
|
|
6
7
|
export function mcpInjectionHonored(config) {
|
|
7
8
|
if (config.lockedConfigKeys?.includes("mcp") === true)
|
|
@@ -79,6 +80,11 @@ function normalizeServer(e, noteBadOrigin) {
|
|
|
79
80
|
noteBadOrigin(spec.name, o.contentOrigin);
|
|
80
81
|
}
|
|
81
82
|
Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
|
|
83
|
+
const faces = readMcpToolFaces(o.toolFaces);
|
|
84
|
+
if (!faces.ok)
|
|
85
|
+
return null;
|
|
86
|
+
if (faces.faces !== undefined)
|
|
87
|
+
spec.toolFaces = faces.faces;
|
|
82
88
|
return spec;
|
|
83
89
|
}
|
|
84
90
|
export function validateRequestMcp(raw) {
|
|
@@ -124,11 +130,14 @@ export function assertRequestMcpContentOrigin(bodyMcp) {
|
|
|
124
130
|
` (omit the key entirely for the default: this server's tools classify as external content)`, { code: "request.field_invalid" });
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
|
-
export function
|
|
133
|
+
export function deploymentClosedMcpNames(baseline, reservedNames) {
|
|
134
|
+
return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
|
|
135
|
+
}
|
|
136
|
+
export function mergeRequestMcp(baseline, gated, reservedNames) {
|
|
128
137
|
if (gated.length === 0)
|
|
129
138
|
return baseline;
|
|
130
|
-
const
|
|
131
|
-
const add = gated.filter((s) => !
|
|
139
|
+
const closed = deploymentClosedMcpNames(baseline, reservedNames);
|
|
140
|
+
const add = gated.filter((s) => !closed.has(s.name));
|
|
132
141
|
return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
|
|
133
142
|
}
|
|
134
143
|
export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
|
|
@@ -153,6 +162,16 @@ export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
|
|
|
153
162
|
note: "declaration DROPPED — these servers keep the pre-378 default (external content, session gets marked); the servers themselves still mount",
|
|
154
163
|
});
|
|
155
164
|
}
|
|
156
|
-
|
|
165
|
+
const reservedNames = config.mcpServers?.reservedNames ?? [];
|
|
166
|
+
const closed = deploymentClosedMcpNames(baseline, reservedNames);
|
|
167
|
+
const blocked = v.ok.filter((s) => closed.has(s.name)).map((s) => s.name);
|
|
168
|
+
if (blocked.length > 0) {
|
|
169
|
+
logger?.warn("mcp_injection_dropped", {
|
|
170
|
+
reason: "name_reserved_by_deployment",
|
|
171
|
+
dropped: blocked,
|
|
172
|
+
note: "these names are declared by the deployment (center/config) — mounted or skipped, a caller can neither shadow nor refill them; rename the request-side server",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return mergeRequestMcp(baseline, v.ok, reservedNames);
|
|
157
176
|
}
|
|
158
177
|
//# sourceMappingURL=task-mcp.js.map
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -293,6 +293,36 @@ export interface FsWriteGateWiring {
|
|
|
293
293
|
* (⇒ ask) and `acceptEdits` degrades to the `default` arm — the same fail-safe direction as the host lane's
|
|
294
294
|
* never-created sentinel dir (resolve-spec 修5). */
|
|
295
295
|
cwd?: string;
|
|
296
|
+
/**
|
|
297
|
+
* 🔴 S-177 / codex r1 [high](验真后修)—— **权限规则**的路径基,与上面那个 `cwd` **刻意分成两个字段**。
|
|
298
|
+
*
|
|
299
|
+
* 两者在 host 腿上常常同值,但它们回答的不是同一个问题,而混用一个字段会让其中一个静默失效:
|
|
300
|
+
* · `cwd` 是**写门**的基。它在「有 remoteExec 但这条 session 还没登记工作目录」时取的是一个
|
|
301
|
+
* **永不创建的哨兵目录**(`resolve-spec` 的 `fs-write-gate-unrooted`)。对写门那是 fail-SAFE 的:
|
|
302
|
+
* 相对目标在哨兵下解析不出真身 ⇒ 落 ask。
|
|
303
|
+
* · `taskRoot` 是**规则模式**的基(core `PathRuleBases.root`)。规则编译器按**词法**匹配,不问目录
|
|
304
|
+
* 存不存在 ⇒ 把哨兵递给它,`deny Edit(src/**)` 会编译成功、指向一个谁也不会写的目录,对真实工作区
|
|
305
|
+
* 的 `<工作区>/src/secret.ts` 这类目标判 allow —— 一条运维亲手写下的收紧规则**静默失效**,方向与写门那条正相反。
|
|
306
|
+
*
|
|
307
|
+
* ⇒ 本字段**只在真任务根已知时**在场。缺席 ⇒ 规则编译器拿不到 `pathBases.root` ⇒ 需要该基的拼法
|
|
308
|
+
* (`Edit(src/**)` / `Edit(/etc/**)`)被 core 以 `unsupported.path_base` **整表拒**(→ 422,fail-loud),
|
|
309
|
+
* 而不是编成一条够不着任何东西的规则。绝对形(`//abs`)与 `~/` 形不需要基,两种部署形都照常编得出。
|
|
310
|
+
*/
|
|
311
|
+
taskRoot?: string;
|
|
312
|
+
/**
|
|
313
|
+
* 🔴 S-177 / codex r2 [high](验真后修)—— **执行环境的 home**(core `PathRuleBases.home`,`~/…` 形的基)。
|
|
314
|
+
*
|
|
315
|
+
* core 对缺席的 `home` 回落 `os.homedir()` —— 那是**这个 server 进程**的 home。引擎与执行同机时它就是
|
|
316
|
+
* 对的;**远端车道**(SSH / 容器 / 沙箱)上执行用户的 home 与 server 的 home 是两个目录,于是
|
|
317
|
+
* `deny Edit(~/.sema/**)` 编译成功、却拿 server 的 home 去匹配远端的目标 ⇒ 一条收紧规则**静默失效**
|
|
318
|
+
* (与 `taskRoot` 那条哨兵病同族、同方向)。
|
|
319
|
+
*
|
|
320
|
+
* ⇒ 本字段只在 home **真知道**时在场(引擎与执行同机)。缺席时,`~/` 形的规则由
|
|
321
|
+
* {@link rejectUnmatchableSettingsNames} **响亮拒**(422),绝不回落到 server 的 home。
|
|
322
|
+
* ⚠️ 上游缺口如实登记:core 今天没有「这条规则需要哪些 base」的查询口,也不把 `home` 当必答项 ——
|
|
323
|
+
* 所以「这条规则用了 `~/` 吗」由本仓读**core 自己解析出的规范 command**(`~/` 前缀)来判,不自铸文法。
|
|
324
|
+
*/
|
|
325
|
+
taskHome?: string;
|
|
296
326
|
/** The session scratchpad dir (envFacts.scratchpadDir, [ref]③) — writes there are always auto-allowed. */
|
|
297
327
|
scratchpadDir?: string;
|
|
298
328
|
/** [ref]① / core 1.294 exemption seam: the session "don't ask again" probe (approvalExemptionStore.has),
|
package/dist/task-settings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, } from "@sema-agent/core";
|
|
2
|
-
import {
|
|
1
|
+
import { combinePolicies, createAllowDenyPolicy, createFsWriteGatePolicy, createPermissionRulePolicy, parseRuleText, tightenTaskSpec, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, ruleToolGrammarOf, } from "@sema-agent/core";
|
|
2
|
+
import { findUnmatchableToolNames, formatUnmatchableToolNames } from "./config.js";
|
|
3
3
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
4
4
|
import { parseHooksConfig } from "./hooks/hook-runner.js";
|
|
5
5
|
export function coercePermissionMode(raw) {
|
|
@@ -225,17 +225,18 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
|
|
|
225
225
|
}
|
|
226
226
|
return v;
|
|
227
227
|
}
|
|
228
|
-
const
|
|
229
|
-
function
|
|
228
|
+
const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
|
|
229
|
+
function createRuleListPolicy(names, behavior, pathBases) {
|
|
230
230
|
try {
|
|
231
|
-
return createPermissionRulePolicy(
|
|
231
|
+
return createPermissionRulePolicy(names.map((name) => ({ rule: name, behavior, source: RULE_SOURCE_BY_BEHAVIOR[behavior] })), {
|
|
232
232
|
defaultAction: "allow",
|
|
233
233
|
caps: { maxRules: MAX_SETTINGS_PERMISSION_RULES },
|
|
234
|
+
...(pathBases !== undefined ? { pathBases } : {}),
|
|
234
235
|
});
|
|
235
236
|
}
|
|
236
237
|
catch (e) {
|
|
237
238
|
const coreMsg = (e instanceof Error ? e.message : String(e)).replace(CONTROL_AND_BIDI_STRIP_RE, "");
|
|
238
|
-
throw new Error(
|
|
239
|
+
throw new Error(`${RULE_SOURCE_BY_BEHAVIOR[behavior].replace("settings.", "")} has rule(s) core cannot compile — ${coreMsg}`, { cause: e });
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
const WORKFLOW_ASK_NAMES = [RUN_WORKFLOW_TOOL_NAME];
|
|
@@ -267,31 +268,51 @@ function createWorkflowPlanDenyPolicy() {
|
|
|
267
268
|
nameSets: [{ deny: WORKFLOW_ASK_NAMES }],
|
|
268
269
|
};
|
|
269
270
|
}
|
|
270
|
-
function rejectUnmatchableSettingsNames(perms) {
|
|
271
|
-
|
|
271
|
+
function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
272
|
+
if (pathBases?.home === undefined) {
|
|
273
|
+
for (const [field, names, behavior] of [
|
|
274
|
+
["permissions.ask", perms.ask, "ask"],
|
|
275
|
+
["permissions.deny", perms.deny, "deny"],
|
|
276
|
+
["permissions.allow", perms.allow, "allow"],
|
|
277
|
+
]) {
|
|
278
|
+
for (const name of names ?? []) {
|
|
279
|
+
const parsed = parseRuleText(name, behavior);
|
|
280
|
+
if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
|
|
281
|
+
throw new Error(`${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
|
|
282
|
+
`on a remote lane the engine's own home is a different user's directory, so the rule would compile and then match nothing that runs. ` +
|
|
283
|
+
`Spell the path absolutely (${"Edit(//abs/path/**)"} form), or run the task on a lane whose home this worker knows.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (const [field, names, behavior, consumer] of [
|
|
289
|
+
["permissions.ask", perms.ask, "ask", "rule-policy"],
|
|
290
|
+
["permissions.deny", perms.deny, "deny", "rule-policy"],
|
|
291
|
+
["permissions.allow", perms.allow, "allow", "tool-name-table"],
|
|
292
|
+
]) {
|
|
272
293
|
if (!names)
|
|
273
294
|
continue;
|
|
274
|
-
const bad = findUnmatchableToolNames(names);
|
|
295
|
+
const bad = findUnmatchableToolNames(names, { consumer, behavior, ...(pathBases !== undefined ? { pathBases } : {}) });
|
|
275
296
|
if (bad.length > 0)
|
|
276
297
|
throw new Error(formatUnmatchableToolNames(field, bad));
|
|
277
298
|
}
|
|
278
|
-
if (perms.allow) {
|
|
279
|
-
const bad = findCcRuleFormNames(perms.allow);
|
|
280
|
-
if (bad.length > 0)
|
|
281
|
-
throw new Error(formatUnmatchableToolNames("permissions.allow", bad));
|
|
282
|
-
}
|
|
283
299
|
}
|
|
284
300
|
export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
285
301
|
const perms = settings.permissions;
|
|
286
302
|
if (!perms)
|
|
287
303
|
return {};
|
|
288
304
|
const { allow, deny, ask, defaultMode } = perms;
|
|
289
|
-
|
|
305
|
+
const pathBases = gate?.taskRoot !== undefined || gate?.taskHome !== undefined
|
|
306
|
+
? { ...(gate.taskRoot !== undefined ? { root: gate.taskRoot } : {}), ...(gate.taskHome !== undefined ? { home: gate.taskHome } : {}) }
|
|
307
|
+
: undefined;
|
|
308
|
+
rejectUnmatchableSettingsNames(perms, pathBases);
|
|
290
309
|
const policies = [];
|
|
291
|
-
if (allow
|
|
292
|
-
policies.push(createAllowDenyPolicy({
|
|
310
|
+
if (allow)
|
|
311
|
+
policies.push(createAllowDenyPolicy({ allow }));
|
|
312
|
+
if (deny)
|
|
313
|
+
policies.push(createRuleListPolicy(deny, "deny", pathBases));
|
|
293
314
|
if (ask)
|
|
294
|
-
policies.push(
|
|
315
|
+
policies.push(createRuleListPolicy(ask, "ask", pathBases));
|
|
295
316
|
const fsWriteGate = (acceptCwd) => {
|
|
296
317
|
if (!gate)
|
|
297
318
|
return undefined;
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -126,8 +126,9 @@ export interface ToolApprovalFrame {
|
|
|
126
126
|
requiresRealApproval?: true;
|
|
127
127
|
/**
|
|
128
128
|
* **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**:
|
|
129
|
-
* 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS`
|
|
130
|
-
*
|
|
129
|
+
* 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.9.0 现值:content_question /
|
|
130
|
+
* unresolvable / org_unavailable / org_rule / rule_store_unavailable / hook / ask_rule /
|
|
131
|
+
* denial_limit_fallback / shell_gate_tighten / safety_tighten / policy)。引擎在门上盖章(core d.ts:
|
|
131
132
|
* 「engine-stamped at the gate, never a policy's claim」),`AskRequest.origin` 逐字透传。
|
|
132
133
|
*
|
|
133
134
|
* 🔴 **echo-only,零消费**:自动车道的资格判据全在 core(`classifierMayAnswer` 的闭表),server
|
|
@@ -864,7 +865,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
864
865
|
* [ref] / 5.58 复审 F-1 —— OFFER 的**文本座尺**(红先修)。
|
|
865
866
|
*
|
|
866
867
|
* 🔴 病灶(真复现,`npm run build --flag=<700 字> && git status`):`batch` 成员的 `segment` 是复合命令
|
|
867
|
-
* 那一段的**原字节**,它**不经** `
|
|
868
|
+
* 那一段的**原字节**,它**不经** `parseRuleText`,所以 core 的 `MAX_RULE_TEXT_CHARS` 对它一个字
|
|
868
869
|
* 都不管;而卡的 `ApprovalCardSchema` 对四个文本座一律 `.max(MAX_RULE_TEXT_CHARS)`。逐字拷进卡 ⇒
|
|
869
870
|
* `card_json` 落库 ⇒ `ensureAsk` 当场回读 `safeParse` 判假 ⇒ `persisted-row-unusable` ⇒ **整只 ask 走
|
|
870
871
|
* park**,重放腿一并跳过该行。与「重扫二轮 §基数帽」是**同一条悬崖的另一条边**(那次是 offer 条数
|
|
@@ -885,7 +886,7 @@ export declare class ToolApprovalCoordinator {
|
|
|
885
886
|
/** 兑付侧座(single 的 rule/command、command 成员的 rule/command、directoryRead 成员的 rule/directory)任一超尺?
|
|
886
887
|
* `segment` 与 `uncoveredDetail[].segment` 是纯渲染座,不在此列(截长是诚实省略)。 */
|
|
887
888
|
private static ruleOfferTextsOverSize;
|
|
888
|
-
/** codex r1 [high]([ref]):offer 的自称座与 `
|
|
889
|
+
/** codex r1 [high]([ref]):offer 的自称座与 `parseRuleText(rule, "allow")` 派生的三元组逐字一致?(理由见调用点) */
|
|
889
890
|
private static ruleOfferDerivedFactsAgree;
|
|
890
891
|
private static boundRuleOfferTexts;
|
|
891
892
|
private buildRuleLaneMaterial;
|
package/dist/tool-approval.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { contentKindOf } from "./approval-content-kind.js";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
import { uuidv7, MAX_RULE_TEXT_CHARS,
|
|
3
|
+
import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
|
|
4
4
|
import { precheckCardRuleText } from "./rules-consent.js";
|
|
5
5
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
6
6
|
import { hasBidiControls } from "./text-bidi.js";
|
|
@@ -69,7 +69,7 @@ class ApprovalStoreDeadlineError extends Error {
|
|
|
69
69
|
this.name = "ApprovalStoreDeadlineError";
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
const FS_WRITE_TOOLS =
|
|
72
|
+
const FS_WRITE_TOOLS = pathConfinableWriteToolNames();
|
|
73
73
|
function sessionAllowCategory(toolName) {
|
|
74
74
|
return FS_WRITE_TOOLS.has(toolName) ? "fs-write" : `tool:${toolName}`;
|
|
75
75
|
}
|
|
@@ -381,7 +381,7 @@ export class ToolApprovalCoordinator {
|
|
|
381
381
|
}
|
|
382
382
|
static ruleOfferDerivedFactsAgree(offer) {
|
|
383
383
|
const derive = (rule) => {
|
|
384
|
-
const p =
|
|
384
|
+
const p = parseRuleText(rule, "allow");
|
|
385
385
|
return "rule" in p ? p.rule : undefined;
|
|
386
386
|
};
|
|
387
387
|
if (offer.kind === "single") {
|
|
@@ -31,7 +31,7 @@ type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjec
|
|
|
31
31
|
type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin";
|
|
32
32
|
type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
|
|
33
33
|
type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
|
|
34
|
-
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end";
|
|
34
|
+
type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end" | "tool_roster_delta";
|
|
35
35
|
type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
|
|
36
36
|
type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
|
|
37
37
|
type _GuardMailbox = AssertAllKeysHandled<Exclude<keyof MailboxMessage, MailboxProjected>>;
|
|
@@ -49,7 +49,7 @@ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "seve
|
|
|
49
49
|
type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
|
|
50
50
|
type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
|
|
51
51
|
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" | "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";
|
|
52
|
-
type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
|
|
52
|
+
type TaskSpecUnwired = "excludeAllTools" | "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars";
|
|
53
53
|
type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
|
|
54
54
|
type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
|
|
55
55
|
type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveMemoryScopes" | "stats" | "editedFiles";
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -407,6 +407,28 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
407
407
|
* 都红不了(2026-09-04 实测:该钉 26/26 全绿,红出 `autoMode` 的是编译期门)。段级 + 成员级的**编译期**门在
|
|
408
408
|
* `core-keyset-guard.ts` ⑥b —— 那才是「core 加了段/加了成员」的可靠红点,两道并用缺一不可。
|
|
409
409
|
*/
|
|
410
|
+
/**
|
|
411
|
+
* `tool_roster_delta`(core 7.8.0 [ref] ⑪ 新 `TaskEvent` 臂)—— **运行期**名册变更:整只变更后名册
|
|
412
|
+
* (`delta.roster`)+ 一段只有持有 `fromDigest` 的消费者才用得上的摘要。
|
|
413
|
+
*
|
|
414
|
+
* 契约里唯一的硬话是失败方向:`fromDigest` 对不上**不是拒绝** —— 携带的整只名册无论如何都是新状态,
|
|
415
|
+
* 消费端记一次 skew 后按快照重同步。所以本构造器**不**替消费端判 digest,只判「这一帧的载荷是不是
|
|
416
|
+
* core 说的那个形」。
|
|
417
|
+
*
|
|
418
|
+
* 判形与 {@link wiringManifestEventData} 的 `tools` 段同一条姿势(理由逐字见那里):属主是 core 的
|
|
419
|
+
* typebox schema `ToolRosterDelta`,过了整只透传,没过 ⇒ **整帧不发** + F 类留痕。半张名册变更帧比
|
|
420
|
+
* 不发更坏:消费端会据它把没读到的行当成「这只工具被卸了」。
|
|
421
|
+
*
|
|
422
|
+
* ✅ **已转正(core 7.9.0 / [ref] S3)**:引擎自 7.9.0 起**真推**这条帧 —— 驱动在推 `wiring_manifest`
|
|
423
|
+
* 的那一点订阅腿的名册增量座,一次改变名册的 refresh ⇒ **恰一帧**(序在 manifest 之后、那次 refresh 自己的
|
|
424
|
+
* `tool_end` 之前),`delta.fromDigest` = 消费端手上那份名册的 digest。7.66.0 写下的「前瞻接线」披露随本版
|
|
425
|
+
* 退役:本臂现在会真的触发,消费端可以依赖它了。
|
|
426
|
+
*/
|
|
427
|
+
export declare function toolRosterDeltaEventData(ev: {
|
|
428
|
+
delta?: unknown;
|
|
429
|
+
eventId?: unknown;
|
|
430
|
+
parentToolCallId?: unknown;
|
|
431
|
+
}): Record<string, unknown> | undefined;
|
|
410
432
|
export declare function wiringManifestEventData(ev: {
|
|
411
433
|
manifest?: unknown;
|
|
412
434
|
eventId?: unknown;
|
package/dist/trace/project.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { redactErrorMessage } from "../observability/run-terminal-log.js";
|
|
2
|
-
import { MCP_DELIVERY_VERDICTS, screenGateOutcome } from "@sema-agent/core";
|
|
2
|
+
import { MCP_DELIVERY_VERDICTS, screenGateOutcome, ToolRoster, ToolRosterDelta } from "@sema-agent/core";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
3
4
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
4
5
|
import { redactSecrets, redactDeep } from "./redact.js";
|
|
5
6
|
import { injectionTierOrAbsent } from "./injection-tier.js";
|
|
@@ -94,6 +95,15 @@ export function screenedGate(v) {
|
|
|
94
95
|
recordFailOpen("server.trace.gate-outcome-defective", "disposition is not one of the two arms");
|
|
95
96
|
return undefined;
|
|
96
97
|
}
|
|
98
|
+
const shapeDefect = (d.kind === "denied" && typeof d.deniedBy !== "string" && "disposition.deniedBy is not a string") ||
|
|
99
|
+
(g.settlement !== undefined && (g.settlement === null || typeof g.settlement !== "object") && "settlement is not an object") ||
|
|
100
|
+
(g.settlement !== undefined && g.settlement !== null && typeof g.settlement.kind !== "string" && "settlement.kind is not a string") ||
|
|
101
|
+
(g.origin !== undefined && typeof g.origin !== "string" && "origin is not a string") ||
|
|
102
|
+
undefined;
|
|
103
|
+
if (shapeDefect !== undefined) {
|
|
104
|
+
recordFailOpen("server.trace.gate-outcome-defective", shapeDefect);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
97
107
|
const defects = screenGateOutcome(g);
|
|
98
108
|
if (defects.length > 0) {
|
|
99
109
|
recordFailOpen("server.trace.gate-outcome-defective", defects.join("; "));
|
|
@@ -250,6 +260,13 @@ export function workspaceChangedEventData(ev) {
|
|
|
250
260
|
...identityFields(ev),
|
|
251
261
|
};
|
|
252
262
|
}
|
|
263
|
+
export function toolRosterDeltaEventData(ev) {
|
|
264
|
+
if (!Value.Check(ToolRosterDelta, ev.delta)) {
|
|
265
|
+
recordFailOpen("server.trace.tool-roster-malformed", "tool_roster_delta.delta did not fit core ToolRosterDelta");
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return { delta: ev.delta, ...identityFields(ev) };
|
|
269
|
+
}
|
|
253
270
|
export function wiringManifestEventData(ev) {
|
|
254
271
|
const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
255
272
|
const bool = (v) => (typeof v === "boolean" ? v : undefined);
|
|
@@ -314,6 +331,8 @@ export function wiringManifestEventData(ev) {
|
|
|
314
331
|
return undefined;
|
|
315
332
|
return armed === (reason === "armed") ? { armed, reason } : undefined;
|
|
316
333
|
})();
|
|
334
|
+
const toolsIn = m.tools;
|
|
335
|
+
const tools = toolsIn === undefined ? undefined : Value.Check(ToolRoster, toolsIn) ? toolsIn : (recordFailOpen("server.trace.tool-roster-malformed", "wiring_manifest.tools did not fit core ToolRoster"), undefined);
|
|
317
336
|
const mcpIn = m.mcp;
|
|
318
337
|
const mcp = (() => {
|
|
319
338
|
if (!Array.isArray(mcpIn))
|
|
@@ -352,6 +371,7 @@ export function wiringManifestEventData(ev) {
|
|
|
352
371
|
...(permissionRules !== undefined ? { permissionRules } : {}),
|
|
353
372
|
...(modelGate !== undefined ? { modelGate } : {}),
|
|
354
373
|
...(autoMode !== undefined ? { autoMode } : {}),
|
|
374
|
+
...(tools !== undefined ? { tools } : {}),
|
|
355
375
|
...(mcp !== undefined ? { mcp } : {}),
|
|
356
376
|
...identityFields(ev),
|
|
357
377
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.67.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "7.
|
|
58
|
-
"@sema-agent/settings-schema": "1.
|
|
57
|
+
"@sema-agent/core": "7.9.0",
|
|
58
|
+
"@sema-agent/settings-schema": "1.9.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
61
61
|
"mysql2": "^3.22.4",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "8.
|
|
72
|
+
"@sema-agent/sdk": "8.6.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|