@sema-agent/server 7.66.0 → 7.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/resolve-spec.js +13 -6
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-types.d.ts +15 -2
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/route-ctx.d.ts +5 -1
- 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/runs.js +1 -1
- package/dist/http/routes/tasks.js +4 -8
- package/dist/http/server.d.ts +6 -1
- package/dist/http/server.js +7 -21
- package/dist/mcp-tool-faces.js +11 -2
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -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/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/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/runs.d.ts +10 -1
- package/dist/runs.js +2 -1
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +50 -6
- package/dist/task-mcp.js +71 -35
- 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 +2 -2
- package/dist/trace/engine-notice-wire.d.ts +17 -1
- package/dist/trace/engine-notice-wire.js +6 -1
- package/dist/trace/project.d.ts +4 -3
- package/package.json +3 -3
package/dist/task-mcp.d.ts
CHANGED
|
@@ -22,9 +22,25 @@
|
|
|
22
22
|
* `runtimeCapsResolver`) is a DEFERRED v2.
|
|
23
23
|
* When the gate is closed, a sent `mcpServers` is IGNORED (advertised via `capabilities.mcpInjection`).
|
|
24
24
|
*/
|
|
25
|
-
import { type CompliancePosture, type LockedKey, type McpServerSpec } from "@sema-agent/core";
|
|
25
|
+
import { type CompliancePosture, type EngineNotice, type LockedKey, type McpInjectionDropReason, type McpServerSpec } from "@sema-agent/core";
|
|
26
26
|
/** Bound on caller-supplied servers (anti-DoS; a real local `.mcp.json` has a handful). */
|
|
27
27
|
export declare const MAX_REQUEST_MCP_SERVERS = 32;
|
|
28
|
+
/**
|
|
29
|
+
* 🔴 **一类丢弃至多点名多少条**(codex 对抗复审 r1 [high],验真后采纳)—— 与挂载上限**同一个数**,
|
|
30
|
+
* 刻意不新造一个可调旋钮:点名的量级本就该与挂载的量级同阶。
|
|
31
|
+
*
|
|
32
|
+
* 病(本批引入,不是既有形):`MAX_REQUEST_MCP_SERVERS` 管的是「挂几台」,管不住「你发了几台」——
|
|
33
|
+
* HTTP 体上限 8 MiB 装得下十万个名字,而 `gate_closed`(整只列表被忽略)与 `over_cap`(上限之后的尾项)
|
|
34
|
+
* 两臂原本逐条点名,于是一条已认证请求就能造出十万条事实、十万条通告,逐条排进本腿的**串行账本写链**
|
|
35
|
+
* ——同一条链上后续的正常事件全部排在它们后面。修前的名字数组把超上限折成**一条**聚合串,所以无界的
|
|
36
|
+
* 是本批新写的那两个循环。
|
|
37
|
+
*
|
|
38
|
+
* 为什么不改成「数组太长就 400」(codex 的另一条建议,**未采纳**):今天 33 台的请求是**照常受理、挂前
|
|
39
|
+
* 32 台**的,加一条新的拒绝面为了一个资源边界去改受理语义,爆炸半径比病本身大(而且 resume 腿重放存量
|
|
40
|
+
* body 会当场砖死一批已 park 的任务——本仓 `opts.leg` 三处先例逐字写着「砖死 resume 比降级糟」)。
|
|
41
|
+
* 有界点名两头都占:该说的仍然说得出,量级钉死在与挂载同阶。
|
|
42
|
+
*/
|
|
43
|
+
export declare const MAX_REQUEST_MCP_DROP_NAMES = 32;
|
|
28
44
|
/**
|
|
29
45
|
* Does THIS deployment honor caller-supplied per-request MCP servers? TRUE for a SINGLE-USER deployment
|
|
30
46
|
* (`requirePrincipal !== true`) — the requester is the super-admin of their own worker, so their `.mcp.json` is
|
|
@@ -37,19 +53,42 @@ export declare function mcpInjectionHonored(config: {
|
|
|
37
53
|
lockedConfigKeys?: readonly LockedKey[];
|
|
38
54
|
compliancePosture?: CompliancePosture;
|
|
39
55
|
}): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* L-167(core 7.9.1)—— 一条**没有被挂上**的请求腿 MCP 条目的**事实**。宿主报事实,core 铸词:
|
|
58
|
+
* `reason` 的词表属主是 core(`McpInjectionDropReason` = malformed_entry / name_reserved_by_deployment /
|
|
59
|
+
* gate_closed / over_cap),本仓一个词都不枚举,通告的码/受众/句子一个字都不拼。
|
|
60
|
+
*
|
|
61
|
+
* `sessionId` **不在**这个形上:它是通告的**路由半场**,在铸点(`mintInjectionDropNotices`)才补 ——
|
|
62
|
+
* 一条丢弃事实在没有会话语境的调用方(桩、单测、未来的非 HTTP 腿)那里同样成立,而通告不成立。
|
|
63
|
+
*/
|
|
64
|
+
export interface McpInjectionDrop {
|
|
65
|
+
readonly server: string;
|
|
66
|
+
readonly reason: McpInjectionDropReason;
|
|
67
|
+
/** `malformed_entry` 专用:**哪个键**判不出形。结构性给出(读器在拒的那一行就知道),不从文案里 parse。 */
|
|
68
|
+
readonly field?: string;
|
|
69
|
+
}
|
|
70
|
+
/** L-167:本条腿的用户面通告席。`sessionId` = 路由半场(`auth.sessionId`,与 `spec.sessionId` 同源);
|
|
71
|
+
* `emit` = 收下 core 铸好的 `EngineNotice`。**本模块不知道**它最后落在 SSE、账本还是日志上 —— 那是
|
|
72
|
+
* 投递腿的事,本模块只负责「事实 → core 工厂 → 一条通告」。 */
|
|
73
|
+
export interface McpInjectionDropSeat {
|
|
74
|
+
readonly sessionId: string | undefined;
|
|
75
|
+
readonly emit: (notice: EngineNotice) => void;
|
|
76
|
+
}
|
|
40
77
|
/**
|
|
41
78
|
* Shape-check the raw `body.mcpServers`. `null` → a non-array (caller error; the route may 400). Otherwise the
|
|
42
79
|
* valid subset + the names of dropped (malformed/over-cap) entries + [ref]'s `badContentOrigin`
|
|
43
80
|
* (**降级**掉的内容出处声明,与 `dropped` 是两回事:那条 server **留着**,丢的只是它的声明)。
|
|
44
81
|
*/
|
|
45
|
-
export declare function validateRequestMcp(raw: unknown):
|
|
82
|
+
export declare function validateRequestMcp(raw: unknown): ValidatedRequestMcp | null;
|
|
83
|
+
type ValidatedRequestMcp = {
|
|
46
84
|
ok: McpServerSpec[];
|
|
47
|
-
dropped:
|
|
85
|
+
dropped: McpInjectionDrop[];
|
|
86
|
+
droppedTotal: number;
|
|
48
87
|
badContentOrigin: Array<{
|
|
49
88
|
server: string;
|
|
50
89
|
value: string;
|
|
51
90
|
}>;
|
|
52
|
-
}
|
|
91
|
+
};
|
|
53
92
|
/**
|
|
54
93
|
* [ref] 件C §4.3 —— **同步**拒面:部署锁了 `mcp` 而请求仍带 `mcpServers` ⇒ 400 `config.locked_key`。
|
|
55
94
|
*
|
|
@@ -128,7 +167,7 @@ export declare function mergeRequestMcp(baseline: McpServerSpec[] | undefined, g
|
|
|
128
167
|
/**
|
|
129
168
|
* The single entry point the spec-builder calls: compute the effective `TaskSpec.mcp` = deployment baseline + the
|
|
130
169
|
* caller's gated per-request servers. Off the honored lane (or with no caller servers) returns the baseline
|
|
131
|
-
* unchanged. Logs an `
|
|
170
|
+
* unchanged. Logs an `mcp_injection_dropped` (reason `gate_closed`) when a caller SENT servers a closed gate dropped (so the shell never
|
|
132
171
|
* silently believes its `.mcp.json` took effect — it also reads `capabilities.mcpInjection`).
|
|
133
172
|
*/
|
|
134
173
|
export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined, bodyMcp: unknown, config: {
|
|
@@ -144,5 +183,10 @@ export declare function resolveRequestMcp(baseline: McpServerSpec[] | undefined,
|
|
|
144
183
|
}, logger?: {
|
|
145
184
|
info?(m: string, meta?: unknown): void;
|
|
146
185
|
warn(m: string, meta?: unknown): void;
|
|
147
|
-
}
|
|
186
|
+
},
|
|
187
|
+
/** L-167:本条腿的**用户面**投递口。`sessionId` = 通告的路由半场(缺席 ⇒ 一条都不铸,见
|
|
188
|
+
* {@link mintInjectionDropNotices});`emit` 收下 core 铸好的通告,由调用腿决定投到哪(见 `boot/resolve-spec.ts`)。
|
|
189
|
+
* 整段缺席(桩、单测、非 HTTP 腿)⇒ 只走日志终点,行为逐字与本件之前相同。 */
|
|
190
|
+
notice?: McpInjectionDropSeat): McpServerSpec[] | undefined;
|
|
191
|
+
export {};
|
|
148
192
|
//# sourceMappingURL=task-mcp.d.ts.map
|
package/dist/task-mcp.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { resolveComplianceDenies } from "@sema-agent/core";
|
|
1
|
+
import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
2
|
import { HttpError } from "./security.js";
|
|
3
3
|
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
|
|
4
4
|
import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
5
5
|
import { readMcpToolFaces } from "./mcp-tool-faces.js";
|
|
6
6
|
export const MAX_REQUEST_MCP_SERVERS = 32;
|
|
7
|
+
export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
|
|
7
8
|
export function mcpInjectionHonored(config) {
|
|
8
9
|
if (config.lockedConfigKeys?.includes("mcp") === true)
|
|
9
10
|
return false;
|
|
@@ -14,20 +15,27 @@ export function mcpInjectionHonored(config) {
|
|
|
14
15
|
function isStringRecord(v) {
|
|
15
16
|
return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
|
|
16
17
|
}
|
|
18
|
+
const bad = (field) => ({ ok: false, field });
|
|
19
|
+
const MALFORMED_NAME = "<malformed>";
|
|
20
|
+
function declaredName(e) {
|
|
21
|
+
return e && typeof e === "object" && typeof e.name === "string" && e.name !== ""
|
|
22
|
+
? String(e.name)
|
|
23
|
+
: MALFORMED_NAME;
|
|
24
|
+
}
|
|
17
25
|
function normalizeServer(e, noteBadOrigin) {
|
|
18
26
|
if (!e || typeof e !== "object")
|
|
19
|
-
return
|
|
27
|
+
return bad("entry");
|
|
20
28
|
const o = e;
|
|
21
29
|
if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
|
|
22
|
-
return
|
|
30
|
+
return bad("name");
|
|
23
31
|
const t = o.transport;
|
|
24
32
|
if (!t || typeof t !== "object")
|
|
25
|
-
return
|
|
33
|
+
return bad("transport");
|
|
26
34
|
const tr = t;
|
|
27
35
|
let transport;
|
|
28
36
|
if (tr.kind === "stdio") {
|
|
29
37
|
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
30
|
-
return
|
|
38
|
+
return bad("transport.command");
|
|
31
39
|
transport = {
|
|
32
40
|
kind: "stdio",
|
|
33
41
|
command: tr.command,
|
|
@@ -37,7 +45,7 @@ function normalizeServer(e, noteBadOrigin) {
|
|
|
37
45
|
}
|
|
38
46
|
else if (tr.kind === "http") {
|
|
39
47
|
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
40
|
-
return
|
|
48
|
+
return bad("transport.url");
|
|
41
49
|
transport = {
|
|
42
50
|
kind: "http",
|
|
43
51
|
url: tr.url,
|
|
@@ -46,12 +54,12 @@ function normalizeServer(e, noteBadOrigin) {
|
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
else {
|
|
49
|
-
return
|
|
57
|
+
return bad("transport.kind");
|
|
50
58
|
}
|
|
51
59
|
const spec = { name: o.name, transport };
|
|
52
60
|
if (o.allowTools !== undefined) {
|
|
53
61
|
if (!Array.isArray(o.allowTools) || !o.allowTools.every((a) => typeof a === "string"))
|
|
54
|
-
return
|
|
62
|
+
return bad("allowTools");
|
|
55
63
|
spec.allowTools = o.allowTools;
|
|
56
64
|
}
|
|
57
65
|
if (typeof o.elicitation === "boolean")
|
|
@@ -82,29 +90,32 @@ function normalizeServer(e, noteBadOrigin) {
|
|
|
82
90
|
Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
|
|
83
91
|
const faces = readMcpToolFaces(o.toolFaces);
|
|
84
92
|
if (!faces.ok)
|
|
85
|
-
return
|
|
93
|
+
return bad("toolFaces");
|
|
86
94
|
if (faces.faces !== undefined)
|
|
87
95
|
spec.toolFaces = faces.faces;
|
|
88
|
-
return spec;
|
|
96
|
+
return { ok: true, spec };
|
|
89
97
|
}
|
|
90
98
|
export function validateRequestMcp(raw) {
|
|
91
99
|
if (raw === undefined || raw === null)
|
|
92
|
-
return
|
|
100
|
+
return validateRequestMcpEntries([]);
|
|
93
101
|
if (!Array.isArray(raw))
|
|
94
102
|
return null;
|
|
103
|
+
return validateRequestMcpEntries(raw);
|
|
104
|
+
}
|
|
105
|
+
function validateRequestMcpEntries(raw) {
|
|
95
106
|
const ok = [];
|
|
96
107
|
const dropped = [];
|
|
97
108
|
const badContentOrigin = [];
|
|
98
109
|
for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
|
|
99
110
|
const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }));
|
|
100
|
-
if (v)
|
|
101
|
-
ok.push(v);
|
|
111
|
+
if (v.ok)
|
|
112
|
+
ok.push(v.spec);
|
|
102
113
|
else
|
|
103
|
-
dropped.push(
|
|
114
|
+
dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
|
|
104
115
|
}
|
|
105
|
-
|
|
106
|
-
dropped.push(
|
|
107
|
-
return { ok, dropped, badContentOrigin };
|
|
116
|
+
for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
|
|
117
|
+
dropped.push({ server: declaredName(e), reason: "over_cap" });
|
|
118
|
+
return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin };
|
|
108
119
|
}
|
|
109
120
|
export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
110
121
|
if (!lockedKeys.has("mcp"))
|
|
@@ -140,21 +151,33 @@ export function mergeRequestMcp(baseline, gated, reservedNames) {
|
|
|
140
151
|
const add = gated.filter((s) => !closed.has(s.name));
|
|
141
152
|
return add.length === 0 ? baseline : [...(baseline ?? []), ...add];
|
|
142
153
|
}
|
|
143
|
-
export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
|
|
154
|
+
export function resolveRequestMcp(baseline, bodyMcp, config, logger, notice) {
|
|
144
155
|
if (bodyMcp === undefined || bodyMcp === null)
|
|
145
156
|
return baseline;
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (v === null) {
|
|
157
|
+
const { mcp, drops } = classifyRequestMcp(baseline, bodyMcp, config, logger);
|
|
158
|
+
mintInjectionDropNotices(drops, notice);
|
|
159
|
+
return mcp;
|
|
160
|
+
}
|
|
161
|
+
function classifyRequestMcp(baseline, bodyMcp, config, logger) {
|
|
162
|
+
if (bodyMcp !== undefined && bodyMcp !== null && !Array.isArray(bodyMcp)) {
|
|
153
163
|
logger?.warn("mcp_injection_ignored", { reason: "mcpServers not an array" });
|
|
154
|
-
return baseline;
|
|
164
|
+
return { mcp: baseline, drops: [] };
|
|
155
165
|
}
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
const entries = Array.isArray(bodyMcp) ? bodyMcp : [];
|
|
167
|
+
const outcome = mcpInjectionHonored(config) ? classifyHonored(baseline, entries, config, logger) : classifyGateClosed(baseline, entries);
|
|
168
|
+
if (outcome.drops.length > 0)
|
|
169
|
+
logger?.warn("mcp_injection_dropped", { dropped: outcome.drops, droppedTotal: outcome.droppedTotal });
|
|
170
|
+
return { mcp: outcome.mcp, drops: outcome.drops };
|
|
171
|
+
}
|
|
172
|
+
function classifyGateClosed(baseline, entries) {
|
|
173
|
+
return {
|
|
174
|
+
mcp: baseline,
|
|
175
|
+
drops: entries.slice(0, MAX_REQUEST_MCP_DROP_NAMES).map((e) => ({ server: declaredName(e), reason: "gate_closed" })),
|
|
176
|
+
droppedTotal: entries.length,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function classifyHonored(baseline, entries, config, logger) {
|
|
180
|
+
const v = validateRequestMcpEntries(entries);
|
|
158
181
|
if (v.badContentOrigin.length > 0) {
|
|
159
182
|
logger?.warn("mcp_content_class_dropped", {
|
|
160
183
|
entries: v.badContentOrigin,
|
|
@@ -165,13 +188,26 @@ export function resolveRequestMcp(baseline, bodyMcp, config, logger) {
|
|
|
165
188
|
const reservedNames = config.mcpServers?.reservedNames ?? [];
|
|
166
189
|
const closed = deploymentClosedMcpNames(baseline, reservedNames);
|
|
167
190
|
const blocked = v.ok.filter((s) => closed.has(s.name)).map((s) => s.name);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
191
|
+
return {
|
|
192
|
+
mcp: mergeRequestMcp(baseline, v.ok, reservedNames),
|
|
193
|
+
drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
|
|
194
|
+
droppedTotal: v.droppedTotal + blocked.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function mintInjectionDropNotices(drops, notice) {
|
|
198
|
+
if (notice === undefined || drops.length === 0)
|
|
199
|
+
return;
|
|
200
|
+
const sessionId = notice.sessionId;
|
|
201
|
+
if (typeof sessionId !== "string" || sessionId.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
const said = new Set();
|
|
204
|
+
for (const d of drops) {
|
|
205
|
+
if (d.server !== MALFORMED_NAME) {
|
|
206
|
+
if (said.has(d.server))
|
|
207
|
+
continue;
|
|
208
|
+
said.add(d.server);
|
|
209
|
+
}
|
|
210
|
+
notice.emit(mcpInjectionDroppedNotice({ sessionId, server: d.server, reason: d.reason, ...(d.field !== undefined ? { field: d.field } : {}) }));
|
|
174
211
|
}
|
|
175
|
-
return mergeRequestMcp(baseline, v.ok, reservedNames);
|
|
176
212
|
}
|
|
177
213
|
//# sourceMappingURL=task-mcp.js.map
|
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";
|
|
@@ -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") {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
|
|
17
17
|
* 刚立,立完就在下一次加码时自己咬住了)。
|
|
18
18
|
*/
|
|
19
|
-
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit"];
|
|
19
|
+
export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped"];
|
|
20
20
|
export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
|
|
21
21
|
/** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */
|
|
22
22
|
export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode;
|
|
@@ -136,5 +136,21 @@ export declare function registerEngineNoticeLeg(opts: {
|
|
|
136
136
|
live?: EngineNoticeWireSink;
|
|
137
137
|
durable?: EngineNoticeWireSink;
|
|
138
138
|
router?: EngineNoticeRouter;
|
|
139
|
+
/**
|
|
140
|
+
* L-167 —— **腿开张之前**就已经铸好的通告(装配期 `resolveSpec` 的产物,经
|
|
141
|
+
* `PreparedTaskSubmission.engineNotices` 递过来)。注册完口**立刻**经同一只 {@link EngineNoticeRouter.route}
|
|
142
|
+
* 投出:白名单、归属键、消毒、隔离四件判据一个都不绕,**不是**第二条投递机制 —— 只是把 `route()` 挪到
|
|
143
|
+
* 「这条会话已经有口在听」的那一拍。
|
|
144
|
+
*
|
|
145
|
+
* 🔴 为什么不让铸点自己 `route()`:装配(`prepareSpec`)跑在本函数**之前**,那一刻这条会话一只口都没有,
|
|
146
|
+
* `route()` 恒返 0 —— 一条结构上永远投不出去的用户面披露,就是本仓 `task.user_steer_undrained` 陈腐明拒
|
|
147
|
+
* 那一族的病。缺席/空数组 ⇒ 一个字节不投(既有腿逐字不变)。
|
|
148
|
+
*/
|
|
149
|
+
pending?: readonly {
|
|
150
|
+
code: string;
|
|
151
|
+
message: string;
|
|
152
|
+
detail?: Record<string, unknown>;
|
|
153
|
+
sessionId?: unknown;
|
|
154
|
+
}[];
|
|
139
155
|
}): () => void;
|
|
140
156
|
//# sourceMappingURL=engine-notice-wire.d.ts.map
|
|
@@ -20,6 +20,7 @@ export const ENGINE_NOTICE_WIRE_CODES = [
|
|
|
20
20
|
"peer.held_settled",
|
|
21
21
|
"peer.idle_subscription",
|
|
22
22
|
"classifier.denial_limit",
|
|
23
|
+
"mcp.injection_dropped",
|
|
23
24
|
];
|
|
24
25
|
const WIRE_CODES = new Set(ENGINE_NOTICE_WIRE_CODES);
|
|
25
26
|
export function isEngineNoticeWireCode(code) {
|
|
@@ -150,6 +151,10 @@ export function registerEngineNoticeLeg(opts) {
|
|
|
150
151
|
const sinks = [...(live ? [live] : []), ...(durable ? [durable] : [])];
|
|
151
152
|
if (sinks.length === 0)
|
|
152
153
|
return () => undefined;
|
|
153
|
-
|
|
154
|
+
const router = opts.router ?? defaultEngineNoticeRouter;
|
|
155
|
+
const unregister = router.registerLeg(sessionId, sinks);
|
|
156
|
+
for (const notice of opts.pending ?? [])
|
|
157
|
+
router.route(notice);
|
|
158
|
+
return unregister;
|
|
154
159
|
}
|
|
155
160
|
//# sourceMappingURL=engine-notice-wire.js.map
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -419,9 +419,10 @@ export declare function workspaceChangedEventData(ev: {
|
|
|
419
419
|
* typebox schema `ToolRosterDelta`,过了整只透传,没过 ⇒ **整帧不发** + F 类留痕。半张名册变更帧比
|
|
420
420
|
* 不发更坏:消费端会据它把没读到的行当成「这只工具被卸了」。
|
|
421
421
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
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
|
+
* 退役:本臂现在会真的触发,消费端可以依赖它了。
|
|
425
426
|
*/
|
|
426
427
|
export declare function toolRosterDeltaEventData(ev: {
|
|
427
428
|
delta?: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.68.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "7.
|
|
57
|
+
"@sema-agent/core": "7.9.2",
|
|
58
58
|
"@sema-agent/settings-schema": "1.9.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "8.
|
|
72
|
+
"@sema-agent/sdk": "8.8.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|