@sema-agent/server 7.130.0 → 7.131.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/MIGRATION.md +87 -0
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/USAGE.md +36 -8
- package/dist/audit.js +4 -1
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/resolve-spec.js +38 -15
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-10-http-server.js +6 -3
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/hands-lane.js +2 -2
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.js +3 -3
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +8 -7
- package/dist/capabilities/scenarios.js +5 -4
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +16 -5
- package/dist/config-invariants.js +2 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +6 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +18 -17
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/hooks/hook-llm.js +2 -2
- package/dist/hooks/hook-runner.d.ts +54 -8
- package/dist/hooks/hook-runner.js +86 -16
- package/dist/http/admission.js +2 -2
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +12 -11
- package/dist/http/checkpoint-ctx.js +4 -4
- package/dist/http/dispatch.js +1 -1
- package/dist/http/resume-legs.js +6 -5
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +6 -16
- package/dist/http/routes/runs.js +6 -10
- package/dist/http/routes/sessions.js +22 -10
- package/dist/http/routes/tasks.js +16 -11
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +10 -0
- package/dist/http/send.js +6 -0
- package/dist/http/server.d.ts +16 -12
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mcp-assertion-header.d.ts +2 -2
- package/dist/mcp-assertion-header.js +3 -3
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-route-endpoint.d.ts +51 -11
- package/dist/model-route-endpoint.js +45 -23
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/fail-open.d.ts +11 -6
- package/dist/observability/fail-open.js +5 -1
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/one-time-migrations.d.ts +10 -1
- package/dist/plugins/one-time-migrations.js +16 -1
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +16 -9
- package/dist/rule-entry-list.d.ts +124 -17
- package/dist/rule-entry-list.js +140 -24
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +8 -7
- package/dist/runs.d.ts +42 -2
- package/dist/runs.js +45 -2
- package/dist/spec-fields.d.ts +8 -4
- package/dist/stored-request-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +1 -1
- package/dist/task-mcp.d.ts +41 -77
- package/dist/task-mcp.js +143 -75
- package/dist/task-settings.d.ts +41 -5
- package/dist/task-settings.js +39 -25
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +11 -0
- package/dist/trace/project.d.ts +6 -0
- package/dist/trace/project.js +11 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +3 -2
package/dist/task-mcp.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mcpInjectionDroppedNotice, readMcpToolAxes, resolveComplianceDenies } from "@sema-agent/core";
|
|
2
|
-
import { isUsableWireName } from "./wire-name-bound.js";
|
|
2
|
+
import { describeBadWireName, isUsableWireName } from "./wire-name-bound.js";
|
|
3
3
|
import { HttpError, createCodedHttpError } from "./security.js";
|
|
4
4
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
5
5
|
import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
|
|
6
|
-
import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
6
|
+
import { MCP_SOURCE_LABEL_MAX, isMcpSourceLabel, readMcpSourceLabel } from "./mcp-source-label.js";
|
|
7
7
|
import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, readMcpIdentityHeaders, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
|
|
8
8
|
import { echoKeyName } from "./request-key-closure.js";
|
|
9
9
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
@@ -27,6 +27,80 @@ export function mcpInjectionHonored(config) {
|
|
|
27
27
|
function isStringRecord(v) {
|
|
28
28
|
return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
|
|
29
29
|
}
|
|
30
|
+
function isRecord(v) {
|
|
31
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
32
|
+
}
|
|
33
|
+
function isStringArray(v) {
|
|
34
|
+
return Array.isArray(v) && v.every((a) => typeof a === "string");
|
|
35
|
+
}
|
|
36
|
+
function isElicitationFlag(v) {
|
|
37
|
+
return typeof v === "boolean";
|
|
38
|
+
}
|
|
39
|
+
function describeShape(v) {
|
|
40
|
+
if (v === undefined)
|
|
41
|
+
return "nothing (the key is absent)";
|
|
42
|
+
if (v === null)
|
|
43
|
+
return "null";
|
|
44
|
+
if (Array.isArray(v))
|
|
45
|
+
return "an array";
|
|
46
|
+
if (typeof v === "string")
|
|
47
|
+
return v.length === 0 ? "an empty string" : `a string of ${v.length} characters`;
|
|
48
|
+
if (typeof v === "object")
|
|
49
|
+
return "an object";
|
|
50
|
+
return `a ${typeof v}`;
|
|
51
|
+
}
|
|
52
|
+
function describeNotStringArray(v) {
|
|
53
|
+
return Array.isArray(v) ? "an array with a non-string element" : describeShape(v);
|
|
54
|
+
}
|
|
55
|
+
function describeNotStringRecord(v) {
|
|
56
|
+
return isRecord(v) ? "an object with a non-string value" : describeShape(v);
|
|
57
|
+
}
|
|
58
|
+
const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
|
|
59
|
+
function readRequestTransport(t) {
|
|
60
|
+
if (!t || typeof t !== "object")
|
|
61
|
+
return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeShape(t)}`);
|
|
62
|
+
const tr = t;
|
|
63
|
+
const lost = [];
|
|
64
|
+
if (tr.kind === "stdio") {
|
|
65
|
+
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
66
|
+
return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeShape(tr.command)}`);
|
|
67
|
+
if (tr.args !== undefined && !isStringArray(tr.args))
|
|
68
|
+
lost.push({ path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(tr.args)}` });
|
|
69
|
+
if (tr.env !== undefined && !isStringRecord(tr.env))
|
|
70
|
+
lost.push({ path: "transport.env", reason: `must be an object of string values — got ${describeNotStringRecord(tr.env)}` });
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
transport: {
|
|
74
|
+
kind: "stdio",
|
|
75
|
+
command: tr.command,
|
|
76
|
+
...(isStringArray(tr.args) ? { args: tr.args } : {}),
|
|
77
|
+
...(isStringRecord(tr.env) ? { env: tr.env } : {}),
|
|
78
|
+
},
|
|
79
|
+
lost,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (tr.kind === "http") {
|
|
83
|
+
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
84
|
+
return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
|
|
85
|
+
const ids = readMcpIdentityHeaders(tr);
|
|
86
|
+
if (tr.headers !== undefined && !isStringRecord(tr.headers))
|
|
87
|
+
lost.push({ path: "transport.headers", reason: `must be an object of string values — got ${describeNotStringRecord(tr.headers)}` });
|
|
88
|
+
if (tr.principalHeader !== undefined && ids.principalHeader === undefined) {
|
|
89
|
+
lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeShape(tr.principalHeader)}` });
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
transport: {
|
|
94
|
+
kind: "http",
|
|
95
|
+
url: tr.url,
|
|
96
|
+
...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
|
|
97
|
+
...ids,
|
|
98
|
+
},
|
|
99
|
+
lost,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeShape(tr.kind)}`);
|
|
103
|
+
}
|
|
30
104
|
const bad = (field) => ({ ok: false, field });
|
|
31
105
|
const MALFORMED_NAME = "<malformed>";
|
|
32
106
|
function declaredName(e) {
|
|
@@ -40,41 +114,16 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
|
|
|
40
114
|
const o = e;
|
|
41
115
|
if (!isUsableWireName(o.name))
|
|
42
116
|
return bad("name");
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
return bad(
|
|
46
|
-
const
|
|
47
|
-
let transport;
|
|
48
|
-
if (tr.kind === "stdio") {
|
|
49
|
-
if (typeof tr.command !== "string" || tr.command.length === 0)
|
|
50
|
-
return bad("transport.command");
|
|
51
|
-
transport = {
|
|
52
|
-
kind: "stdio",
|
|
53
|
-
command: tr.command,
|
|
54
|
-
...(Array.isArray(tr.args) && tr.args.every((a) => typeof a === "string") ? { args: tr.args } : {}),
|
|
55
|
-
...(isStringRecord(tr.env) ? { env: tr.env } : {}),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
else if (tr.kind === "http") {
|
|
59
|
-
if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
|
|
60
|
-
return bad("transport.url");
|
|
61
|
-
transport = {
|
|
62
|
-
kind: "http",
|
|
63
|
-
url: tr.url,
|
|
64
|
-
...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
|
|
65
|
-
...readMcpIdentityHeaders(tr),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
return bad("transport.kind");
|
|
70
|
-
}
|
|
71
|
-
const spec = { name: o.name, transport };
|
|
117
|
+
const read = readRequestTransport(o.transport);
|
|
118
|
+
if (!read.ok)
|
|
119
|
+
return bad(read.issue.path);
|
|
120
|
+
const spec = { name: o.name, transport: read.transport };
|
|
72
121
|
if (o.allowTools !== undefined) {
|
|
73
|
-
if (!
|
|
122
|
+
if (!isStringArray(o.allowTools))
|
|
74
123
|
return bad("allowTools");
|
|
75
124
|
spec.allowTools = o.allowTools;
|
|
76
125
|
}
|
|
77
|
-
if (
|
|
126
|
+
if (isElicitationFlag(o.elicitation))
|
|
78
127
|
spec.elicitation = o.elicitation;
|
|
79
128
|
const axes = readMcpToolAxes(o.toolAxes);
|
|
80
129
|
let judged = axes;
|
|
@@ -138,30 +187,6 @@ export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
|
|
|
138
187
|
code: "config.locked_key",
|
|
139
188
|
});
|
|
140
189
|
}
|
|
141
|
-
export function assertRequestMcpContentOrigin(bodyMcp) {
|
|
142
|
-
if (!Array.isArray(bodyMcp))
|
|
143
|
-
return;
|
|
144
|
-
for (const e of bodyMcp) {
|
|
145
|
-
if (!e || typeof e !== "object" || Array.isArray(e))
|
|
146
|
-
continue;
|
|
147
|
-
const v = e.contentOrigin;
|
|
148
|
-
if (v === undefined)
|
|
149
|
-
continue;
|
|
150
|
-
if (isContentOriginWord(v))
|
|
151
|
-
continue;
|
|
152
|
-
throw new HttpError(400, `mcpServers[].contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(v)}` +
|
|
153
|
-
` (omit the key entirely for the default: this server's tools classify as external content)`, { code: "request.field_invalid" });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
export function assertRequestMcpIdentityHeaders(bodyMcp) {
|
|
157
|
-
if (!Array.isArray(bodyMcp))
|
|
158
|
-
return;
|
|
159
|
-
for (const e of bodyMcp) {
|
|
160
|
-
const refusal = requestEntryIdentityRefusal(e);
|
|
161
|
-
if (refusal !== undefined)
|
|
162
|
-
throw createCodedHttpError(400, refusal.errorCode, refusal.message);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
190
|
export function requestEntryIdentityIssue(entry) {
|
|
166
191
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
167
192
|
return undefined;
|
|
@@ -170,39 +195,82 @@ export function requestEntryIdentityIssue(entry) {
|
|
|
170
195
|
return undefined;
|
|
171
196
|
return mcpIdentityHeadersIssue(t);
|
|
172
197
|
}
|
|
173
|
-
export function requestEntryIdentityRefusal(entry) {
|
|
198
|
+
export function requestEntryIdentityRefusal(entry, at) {
|
|
174
199
|
const issue = requestEntryIdentityIssue(entry);
|
|
175
|
-
if (issue === undefined)
|
|
200
|
+
if (issue === undefined || !isRecord(entry))
|
|
176
201
|
return undefined;
|
|
177
|
-
const name = echoKeyName("", declaredName(entry));
|
|
178
202
|
if (issue === "both_set")
|
|
179
|
-
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(
|
|
180
|
-
const
|
|
181
|
-
|
|
203
|
+
return { errorCode: "request.body_shape", message: describeMcpIdentityHeadersConflict(echoKeyName("", declaredName(entry))) };
|
|
204
|
+
const transport = entry.transport;
|
|
205
|
+
const value = isRecord(transport) ? transport.assertionHeader : undefined;
|
|
206
|
+
return entryFieldRefusal(entry, at, { path: "transport.assertionHeader", reason: describeMcpAssertionHeaderNotAString(value) });
|
|
182
207
|
}
|
|
183
|
-
export function
|
|
208
|
+
export function assertRequestMcpEntries(bodyMcp) {
|
|
184
209
|
if (!Array.isArray(bodyMcp))
|
|
185
210
|
return;
|
|
186
|
-
for (let i = 0; i < bodyMcp.length; i++) {
|
|
187
|
-
const refusal =
|
|
211
|
+
for (let i = 0; i < Math.min(bodyMcp.length, MAX_REQUEST_MCP_SERVERS); i++) {
|
|
212
|
+
const refusal = requestEntryRefusal(bodyMcp[i], `mcpServers[${i}]`);
|
|
188
213
|
if (refusal !== undefined)
|
|
189
214
|
throw createCodedHttpError(400, refusal.errorCode, refusal.message);
|
|
190
215
|
}
|
|
191
216
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
217
|
+
const ENTRY_KEY_JUDGES = {
|
|
218
|
+
allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
|
|
219
|
+
? undefined
|
|
220
|
+
: { path: "allowTools", reason: `must be an array of strings (omit the key to leave the server's tools unrestricted) — got ${describeNotStringArray(o.allowTools)}` },
|
|
221
|
+
contentOrigin: (o) => o.contentOrigin === undefined || isContentOriginWord(o.contentOrigin)
|
|
222
|
+
? undefined
|
|
223
|
+
: {
|
|
224
|
+
path: "contentOrigin",
|
|
225
|
+
reason: `must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(o.contentOrigin)}` +
|
|
226
|
+
` (omit the key for the default: this server's tools classify as external content)`,
|
|
227
|
+
},
|
|
228
|
+
elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeShape(o.elicitation)}` }),
|
|
229
|
+
name: (o) => (isUsableWireName(o.name) ? undefined : { path: "name", reason: `must be a non-empty string within the wire-name length bound — got ${describeBadWireName(o.name)}` }),
|
|
230
|
+
source: (o) => o.source === undefined || isMcpSourceLabel(o.source)
|
|
231
|
+
? undefined
|
|
232
|
+
: { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeShape(o.source)}` },
|
|
233
|
+
toolAxes: (o) => {
|
|
234
|
+
const read = readMcpToolAxes(o.toolAxes);
|
|
235
|
+
return read.ok ? undefined : { path: "toolAxes", reason: read.problem };
|
|
236
|
+
},
|
|
237
|
+
toolFaces: (o) => {
|
|
238
|
+
const read = readMcpToolFaces(o.toolFaces);
|
|
239
|
+
return read.ok ? undefined : { path: "toolFaces", reason: read.problem };
|
|
240
|
+
},
|
|
241
|
+
transport: (o) => {
|
|
242
|
+
const read = readRequestTransport(o.transport);
|
|
243
|
+
return read.ok ? read.lost[0] : read.issue;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
const ENTRY_REFUSAL_TAIL = "The request is refused rather than mounting the server without it: a declaration that silently did not apply would leave the server running differently from what you declared. " +
|
|
247
|
+
"Fix the entry (an optional key may also be omitted).";
|
|
248
|
+
function entryFieldRefusal(entry, at, issue) {
|
|
198
249
|
const server = echoKeyName("", declaredName(entry));
|
|
199
250
|
return {
|
|
200
251
|
errorCode: "request.field_invalid",
|
|
201
|
-
message: `${at}.
|
|
202
|
-
`The whole declaration is refused: the tools that were fine are not kept and nothing is defaulted, because a toolAxes declaration is your own risk-axis judgement for this server's tools ` +
|
|
203
|
-
`(it can move a tool down or up), and a half-applied one would silently differ from what you declared. Fix the entry, or omit the key to declare nothing.`,
|
|
252
|
+
message: `${at}.${issue.path} (server "${server}") is not a usable declaration — ${issue.reason.replace(CONTROL_AND_BIDI_STRIP_RE, "")}. ${ENTRY_REFUSAL_TAIL}`,
|
|
204
253
|
};
|
|
205
254
|
}
|
|
255
|
+
function requestEntryRefusal(entry, at) {
|
|
256
|
+
if (!isRecord(entry))
|
|
257
|
+
return undefined;
|
|
258
|
+
const identity = requestEntryIdentityRefusal(entry, at);
|
|
259
|
+
if (identity !== undefined)
|
|
260
|
+
return identity;
|
|
261
|
+
for (const key of MCP_SERVER_ENTRY_KEYS) {
|
|
262
|
+
const issue = ENTRY_KEY_JUDGES[key](entry);
|
|
263
|
+
if (issue !== undefined)
|
|
264
|
+
return entryFieldRefusal(entry, at, issue);
|
|
265
|
+
}
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
export function requestEntryToolAxesRefusal(entry, at) {
|
|
269
|
+
if (!isRecord(entry))
|
|
270
|
+
return undefined;
|
|
271
|
+
const issue = ENTRY_KEY_JUDGES.toolAxes(entry);
|
|
272
|
+
return issue === undefined ? undefined : entryFieldRefusal(entry, at, issue);
|
|
273
|
+
}
|
|
206
274
|
export function deploymentClosedMcpNames(baseline, reservedNames) {
|
|
207
275
|
return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
|
|
208
276
|
}
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
* - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
|
|
48
48
|
* main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
|
|
49
49
|
*/
|
|
50
|
-
import { type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
|
|
51
|
-
import type { HooksConfig } from "@sema-agent/settings-schema/hooks";
|
|
50
|
+
import { type PathRuleBases, type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
|
|
52
51
|
import { type KeyClosureIssue } from "./request-key-closure.js";
|
|
52
|
+
import { type RunnableHooksConfig } from "./hooks/hook-runner.js";
|
|
53
|
+
import type { StoredHooksRereadM20 } from "./stored-request-hooks-m20.js";
|
|
53
54
|
import { type EntryPermissionMode } from "./runtime-governance.js";
|
|
54
55
|
/**
|
|
55
56
|
* 请求面 `permissionMode` 的闭集 = core `PERMISSION_MODES` **全集**(顶层 `permissionMode` / `settings.permissions.defaultMode` /
|
|
@@ -213,10 +214,10 @@ export interface ParsedTaskSettings {
|
|
|
213
214
|
* orthogonal axes — `thinking:"max"` (the reasoning axis) + `selfOrchestration:true` (the orchestration axis,
|
|
214
215
|
* through its existing fail-closed gate). NOT a new mechanism; just a preset that fans out to existing spec fields. */
|
|
215
216
|
ultracode?: boolean;
|
|
216
|
-
/** hooks hook-runner(阶段一)
|
|
217
|
+
/** hooks hook-runner(阶段一):经 `parseHooksConfig`(契约包分类器 + 服务侧上限 + 本仓会跑的条目类型)判过的 hooks 配置。像 shellEnv 一样
|
|
217
218
|
* 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
|
|
218
219
|
* core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
|
|
219
|
-
hooks?:
|
|
220
|
+
hooks?: RunnableHooksConfig;
|
|
220
221
|
/** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
|
|
221
222
|
* 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
|
|
222
223
|
* 这正是本版把技能条数/描述两条帽整条退役后的补偿面:per-turn 成本的旋钮在**渲染侧**,不在受理侧。
|
|
@@ -232,6 +233,15 @@ export interface ParseTaskSettingsResult {
|
|
|
232
233
|
/** Sub-fields the caller SENT but this version does not wire(`env`;`hooks` 仅在 MALFORMED 时入列——valid hooks
|
|
233
234
|
* 已是 applied 字段)。Empty when none. */
|
|
234
235
|
deferred: string[];
|
|
236
|
+
/** 一次性迁移 M20(窗口见 `plugins/one-time-migrations.ts` 登记表):存下的 hooks 被唯一读者拒掉、经重读座剥掉点名的键后过了读者 ⇒ 本次读数里 `settings.hooks` 是重读的产物,
|
|
237
|
+
* 并带被剥的坐标(出门形)。缺席 = 没有发生重读。 */
|
|
238
|
+
hooksReread?: {
|
|
239
|
+
readonly stripped: readonly string[];
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** `parseTaskSettings` 的可选座:续跑重放腿才有的重读座(一次性迁移 M20;新提交腿与不问 hooks 的读者不递)。 */
|
|
243
|
+
export interface ParseTaskSettingsSeats {
|
|
244
|
+
readonly storedHooksReread?: StoredHooksRereadM20;
|
|
235
245
|
}
|
|
236
246
|
/** Cap on `outputStyle` length — it lands in the system prompt, so it shares the `MAX_SYSTEM_PROMPT_CHARS` (16384)
|
|
237
247
|
* posture (an uncapped prompt body is a per-turn token-cost hole; cross review). Exported so the HTTP layer
|
|
@@ -333,7 +343,7 @@ export declare function taskHooksHonored(config: {
|
|
|
333
343
|
* 重放也走的宽容层,一个 4xx 在这里会把存量 parked 任务永久砖死;FRESH 提交面由 {@link taskSettingsKeyIssue}
|
|
334
344
|
* 在 `server.ts` prepareSpec 里 400 响亮拒 + 逐字列名。双层姿势与 outputStyle / permissionMode 两处逐字相同。
|
|
335
345
|
*/
|
|
336
|
-
export declare function parseTaskSettings(raw: unknown): ParseTaskSettingsResult;
|
|
346
|
+
export declare function parseTaskSettings(raw: unknown, seats?: ParseTaskSettingsSeats): ParseTaskSettingsResult;
|
|
337
347
|
/** Defensive acceptance of the `appendSystemPrompt` rider ([ref] R2 + codex F2): the HTTP 400 gate (prepareSpec,
|
|
338
348
|
* MAX_SYSTEM_PROMPT_CHARS) only covers FRESH submits — every resume family re-enters resolveSpec on a STORED body,
|
|
339
349
|
* and a pre-1.243 store preserved unknown request keys verbatim (the field was INERT then), so an over-cap legacy
|
|
@@ -457,6 +467,32 @@ export interface FsWriteGateWiring {
|
|
|
457
467
|
* 本 wiring 的组合点。) */
|
|
458
468
|
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
459
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* deny / ask 两桶(core 规则编译器读的收紧表)的**问序,单表**:{@link rulePolicyBucketsRefusal} 的两段循环(名字层 + 文法 · `~/` 要 home)都迭代它 ——
|
|
472
|
+
* 两桶都有坏条目时,拒点与存体预判点名同一个桶、说同一句。表里每个词同时是 core 规则的 `behavior`。
|
|
473
|
+
*/
|
|
474
|
+
export declare const RULE_POLICY_BUCKETS: readonly ["ask", "deny"];
|
|
475
|
+
export type RulePolicyBucket = (typeof RULE_POLICY_BUCKETS)[number];
|
|
476
|
+
/**
|
|
477
|
+
* deny / ask 两桶的**完整规则判**,唯一一只(拒点 {@link rejectUnmatchableSettingsNames} 与存体预判 `http/stored-body-replay.ts` 同调):
|
|
478
|
+
* ② 按 {@link RULE_POLICY_BUCKETS} 的序逐桶问 `unmatchableToolNamesRefusal`(名字层 + core 规则文法);③ 都过了,再逐桶判 `~/` 形要 home 的那一拒。第一个坏处答 `{ bucket, message }`。
|
|
479
|
+
* 入参:桶、{@link RulePolicyBases}。**home 同源**:拒点的 home 是 `resolve-spec.ts` 的 `laneHomeBase()` 经 `executionLaneHomeDir(config)` 算出的,预判传同一个函数对同一份部署配置算出的。
|
|
480
|
+
* **声明给文法的基**只有拒点有(根 + home 的并集,执行车道才知道根):core 的规则文法对**没声明**的基不下判词(见 `config.ts` `uncompilableRuleGuidance`),所以预判什么也不声明、两腿逐字同句;
|
|
481
|
+
* 预判不能把 home 当「半个基」声明给文法 —— core 的路径族只从根推,缺根时一个合法的 Windows 形 home 会按默认族被判 `unsupported.path_base`(V6-k)。
|
|
482
|
+
* ③ 排在最后:一条同时文法不成立又带 `~/` 的规则,先说文法(补上 home 它仍被文法拒,先说 home 是误导)。
|
|
483
|
+
*/
|
|
484
|
+
export interface RulePolicyBases {
|
|
485
|
+
/** 声明给 core 规则文法的路径基(这条 task 的根 + home):拒点才有;存体预判不声明。 */
|
|
486
|
+
readonly declared?: PathRuleBases;
|
|
487
|
+
/** 执行环境的 home(`~/` 形规则的前提);这个部署解不出 ⇒ 缺席。 */
|
|
488
|
+
readonly home?: string;
|
|
489
|
+
}
|
|
490
|
+
export declare function rulePolicyBucketsRefusal(permissions: Readonly<Partial<Record<RulePolicyBucket, readonly string[]>>>, bases: RulePolicyBases): {
|
|
491
|
+
readonly bucket: RulePolicyBucket;
|
|
492
|
+
readonly message: string;
|
|
493
|
+
} | undefined;
|
|
494
|
+
/** 请求的 settings 编不成生效的规则 / 有永远不生效的条目 ⇒ 422 的句首(三只桶同一个出口;`resolveSpec` 的拒点与存体重放预判的 409 句子同用这一个,不各写一份)。 */
|
|
495
|
+
export declare const REQUEST_SETTINGS_REFUSAL_PREFIX = "request settings cannot be applied as written: ";
|
|
460
496
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
461
497
|
toolPolicy?: ToolPolicy;
|
|
462
498
|
};
|
package/dist/task-settings.js
CHANGED
|
@@ -193,18 +193,26 @@ export function taskSettingsKeyIssue(raw) {
|
|
|
193
193
|
export function taskHooksHonored(config) {
|
|
194
194
|
return config.requirePrincipal !== true;
|
|
195
195
|
}
|
|
196
|
-
export function parseTaskSettings(raw) {
|
|
196
|
+
export function parseTaskSettings(raw, seats = {}) {
|
|
197
197
|
if (raw === null || raw === undefined || typeof raw !== "object")
|
|
198
198
|
return { deferred: [] };
|
|
199
199
|
const s = raw;
|
|
200
200
|
const deferred = [];
|
|
201
201
|
let hooksConfig;
|
|
202
|
+
let hooksReread;
|
|
202
203
|
if (s.hooks !== undefined && s.hooks !== null) {
|
|
203
204
|
const parsedHooks = parseHooksConfig(s.hooks);
|
|
204
205
|
if (parsedHooks.config)
|
|
205
206
|
hooksConfig = parsedHooks.config;
|
|
206
|
-
else
|
|
207
|
-
|
|
207
|
+
else {
|
|
208
|
+
const reread = seats.storedHooksReread?.(s.hooks);
|
|
209
|
+
if (reread !== undefined) {
|
|
210
|
+
hooksConfig = reread.config;
|
|
211
|
+
hooksReread = { stripped: reread.stripped };
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
deferred.push("hooks");
|
|
215
|
+
}
|
|
208
216
|
}
|
|
209
217
|
const shellEnv = cleanEnvMap(s.env);
|
|
210
218
|
if (s.env !== undefined && s.env !== null && shellEnv === undefined)
|
|
@@ -252,7 +260,7 @@ export function parseTaskSettings(raw) {
|
|
|
252
260
|
const maxDesc = s.skillListingMaxDescChars;
|
|
253
261
|
if (skillListingMaxDescOk(maxDesc))
|
|
254
262
|
out.skillListingMaxDescChars = maxDesc;
|
|
255
|
-
return { settings: Object.keys(out).length > 0 ? out : undefined, deferred };
|
|
263
|
+
return { settings: Object.keys(out).length > 0 ? out : undefined, deferred, ...(hooksReread !== undefined ? { hooksReread } : {}) };
|
|
256
264
|
}
|
|
257
265
|
export function hasConstitutionAnchors(text) {
|
|
258
266
|
return typeof text === "string" && text.includes(CYBER_RISK) && text.includes(URL_SAFETY) && text.includes(HARNESS_SECTION_ANCHOR);
|
|
@@ -321,33 +329,39 @@ function createWorkflowAskPolicy(isExempt) {
|
|
|
321
329
|
};
|
|
322
330
|
}
|
|
323
331
|
function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
332
|
+
const refused = rulePolicyBucketsRefusal(perms, { ...(pathBases !== undefined ? { declared: pathBases } : {}), ...(pathBases?.home !== undefined ? { home: pathBases.home } : {}) });
|
|
333
|
+
if (refused !== undefined)
|
|
334
|
+
throw new Error(refused.message);
|
|
335
|
+
}
|
|
336
|
+
export const RULE_POLICY_BUCKETS = ["ask", "deny"];
|
|
337
|
+
export function rulePolicyBucketsRefusal(permissions, bases) {
|
|
338
|
+
for (const bucket of RULE_POLICY_BUCKETS) {
|
|
339
|
+
const names = permissions[bucket];
|
|
340
|
+
if (!names)
|
|
341
|
+
continue;
|
|
342
|
+
const unmatchable = unmatchableToolNamesRefusal(`permissions.${bucket}`, names, { consumer: "rule-policy", behavior: bucket, ...(bases.declared !== undefined ? { pathBases: bases.declared } : {}) });
|
|
343
|
+
if (unmatchable !== undefined)
|
|
344
|
+
return { bucket, message: unmatchable.message };
|
|
345
|
+
}
|
|
346
|
+
if (bases.home === undefined) {
|
|
347
|
+
for (const bucket of RULE_POLICY_BUCKETS) {
|
|
348
|
+
const field = `permissions.${bucket}`;
|
|
349
|
+
for (const name of permissions[bucket] ?? []) {
|
|
350
|
+
const parsed = parseRuleText(name, bucket);
|
|
331
351
|
if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
352
|
+
return {
|
|
353
|
+
bucket,
|
|
354
|
+
message: `${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
|
|
355
|
+
`the engine never guesses one: without a home base the rule resolves to "unreadable" and every matching call stops to ask, so it guards nothing. ` +
|
|
356
|
+
`Spell the path absolutely (${"Edit(//abs/path/**)"} form), declare the lane's home with REMOTE_EXEC_HOME_DIR, or run the task on a lane whose home this worker knows.`,
|
|
357
|
+
};
|
|
335
358
|
}
|
|
336
359
|
}
|
|
337
360
|
}
|
|
338
361
|
}
|
|
339
|
-
|
|
340
|
-
["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
341
|
-
["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
342
|
-
];
|
|
343
|
-
for (const [field, names, judgment] of judgments) {
|
|
344
|
-
if (!names)
|
|
345
|
-
continue;
|
|
346
|
-
const unmatchable = unmatchableToolNamesRefusal(field, names, judgment);
|
|
347
|
-
if (unmatchable !== undefined)
|
|
348
|
-
throw new Error(unmatchable.message);
|
|
349
|
-
}
|
|
362
|
+
return undefined;
|
|
350
363
|
}
|
|
364
|
+
export const REQUEST_SETTINGS_REFUSAL_PREFIX = "request settings cannot be applied as written: ";
|
|
351
365
|
export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
352
366
|
const perms = settings.permissions;
|
|
353
367
|
if (!perms)
|
|
@@ -148,9 +148,9 @@ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCal
|
|
|
148
148
|
type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
|
|
149
149
|
type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
|
|
150
150
|
type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
|
|
151
|
-
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" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools";
|
|
151
|
+
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" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools" | "userAttachments";
|
|
152
152
|
/** 导出:请求体键闭集门(`request-key-closure.ts`)据它锁「本仓记为未接线的引擎座,提交口不许受理」—— 两处分类只能同向。 */
|
|
153
|
-
export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport"
|
|
153
|
+
export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport";
|
|
154
154
|
type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
|
|
155
155
|
type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
|
|
156
156
|
type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles" | "responseModel" | "compaction";
|
|
@@ -104,6 +104,35 @@ export declare function createSerialLedgerAppend(appendEvent: (seq: number, type
|
|
|
104
104
|
* 返回 `.catch` 用的处理器(产物是函数 ⇒ `create*`)。
|
|
105
105
|
*/
|
|
106
106
|
export declare function createForwardAppendFailedNote(type: string): (err: unknown) => void;
|
|
107
|
+
/**
|
|
108
|
+
* 人在环帧(`question` / `question_complete`、`elicitation` / `elicitation_complete`)的**账本孪生投递口**。
|
|
109
|
+
*
|
|
110
|
+
* 一条律:**凡进 run 流的 HITL 帧都有账本孪生;腿只决定「还要不要同时直写 socket」**。后台腿没有 live 面,
|
|
111
|
+
* 它的投递口就是账本追加本身(`runs.ts`);同步流腿有 live SSE,走本函数 —— 同一只帧对象先落账、再按需直写,
|
|
112
|
+
* 于是两条腿对同一只帧的落法只差「直写要不要」,不差「账本有没有」。
|
|
113
|
+
*
|
|
114
|
+
* 顺序与失败语义(与后台腿同一条):
|
|
115
|
+
* · **先账本后直写**。账本落不下 ⇒ 本次投递**失败**(抛),帧不上 socket —— 一只没有孪生的帧不许单独露面
|
|
116
|
+
* (否则壳看见一张重载就丢的卡)。调用方(协调器)按「开帧投递失败」处理:这一问结算 `unavailable`、记一次既有的
|
|
117
|
+
* P-DEBT 留痕;消卡帧则落「面包屑丢失」的 F 类留痕。
|
|
118
|
+
* · **seq 不自造**:孪生走 `sink.append` —— 本腿账本唯一的那条串行链(与 text / tool / done 行同一个计数器)。
|
|
119
|
+
* · **直写口已不可达**(客户端离开,而 run 因 detach 继续)⇒ 账本孪生即送达,不再触碰那条已死的连接
|
|
120
|
+
* (不抛、也不记「丢帧」—— 帧没丢,在账本里)。
|
|
121
|
+
* · **本腿没有账本**(无 durable run 行的部署:`sink` 缺席)⇒ 直写就是全部投递,死流姿态由 `writeLive` 自己带
|
|
122
|
+
* (question 抛、elicitation 静默记账),字节逐字同孪生落地之前。
|
|
123
|
+
*
|
|
124
|
+
* 命名(CLAUDE.md 工厂命名律):`create*` —— 返回的是捕获了两个口的闭包,不是纯数据。
|
|
125
|
+
*/
|
|
126
|
+
export declare function createLedgerTwinEmit<F extends {
|
|
127
|
+
type: LedgerEventType;
|
|
128
|
+
}>(opts: {
|
|
129
|
+
/** 本腿的账本(sync 腿 = 有 durable run 行才有;缺席 ⇒ 无孪生可落)。只用 `append`:seq 由它那条链铸。 */
|
|
130
|
+
sink: Pick<LedgerSink, "append"> | undefined;
|
|
131
|
+
/** 本腿的直写口(sync 腿 = 一个具名 SSE 事件)。 */
|
|
132
|
+
writeLive: (frame: F) => void;
|
|
133
|
+
/** 直写面此刻可达吗 —— 与协调器问「这条腿还够得着人吗」的是**同一只**谓词(`QuestionRunContext.deliverable`)。 */
|
|
134
|
+
liveReachable: () => boolean;
|
|
135
|
+
}): (frame: F) => Promise<void>;
|
|
107
136
|
export declare function createLedgerSink(opts: {
|
|
108
137
|
/** The seq-stamped durable append — typically `(seq, type, data) => runStore.appendEvent(taskId, seq, type, data)`. */
|
|
109
138
|
appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise<void>;
|
|
@@ -17,6 +17,17 @@ export function createSerialLedgerAppend(appendEvent, startSeq) {
|
|
|
17
17
|
export function createForwardAppendFailedNote(type) {
|
|
18
18
|
return (err) => recordFailOpen("server.ledger.forward-append-failed", `type=${type} err=${err instanceof Error ? err.message : String(err)}`);
|
|
19
19
|
}
|
|
20
|
+
export function createLedgerTwinEmit(opts) {
|
|
21
|
+
return async (frame) => {
|
|
22
|
+
if (opts.sink !== undefined) {
|
|
23
|
+
const { type, ...data } = frame;
|
|
24
|
+
await opts.sink.append(type, data);
|
|
25
|
+
if (!opts.liveReachable())
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
opts.writeLive(frame);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
20
31
|
export function createLedgerSink(opts) {
|
|
21
32
|
const keys = opts.notifiedKeys ?? new NotifiedKeys();
|
|
22
33
|
let seq = 0;
|
package/dist/trace/project.d.ts
CHANGED
|
@@ -851,6 +851,11 @@ export declare function wiringManifestFaceEventData(ev: {
|
|
|
851
851
|
* `[from …]` 渲染就是靠它决定加不加 `(unverified)`),剥掉它比剥掉 id 更坏:剩下一个看不出真假的名字。
|
|
852
852
|
* · `principal` —— **刻意不投影**(与 ToolApprovalFrame 的 `principal` 同裁):帧本就经 owner-scoped 流
|
|
853
853
|
* 投递,租户身份不必再由帧自述。要它请先提消费需求,别顺手加。
|
|
854
|
+
* · `attachments[]`(core 7.50.0,只在声明了附件的目标那一帧 `applied` 上)—— 逐成员挑四键:`ordinal`(引擎铸的序号)/
|
|
855
|
+
* `kind`(引擎认出的种类,闭集词;读不出种类时缺席)/ `outcome`(引擎的结局词,闭集)⇒ verbatim;`label` = 调用方**自己声明的**
|
|
856
|
+
* 路径或名字(引擎原样回声,不是解析后的落点 —— 多租户沙箱车道上它是这次 run 自己工作区里的相对 / 绝对路径串,帧上不出现任何别的租户
|
|
857
|
+
* 或宿主盘的信息),与已展示的工具实参 / `workspace_changed.cwd` 同一信任级(UNTRUSTED 的路径串)⇒ 过 `redactSecrets`。长度界由引擎定
|
|
858
|
+
* (`USER_ATTACHMENT_LABEL_MAX`),本仓不另截。非对象的成员丢、集外的成员键不投(同 `actor` 的白名单理由)。
|
|
854
859
|
*
|
|
855
860
|
* `actor` 逐键挑(禁 spread):core 已声明 `ActorAssertion` 是冻结三键形,但白名单的意义正是「core 加第
|
|
856
861
|
* 四键时由人处置」——身份对象上多出来的键裸上线是本文件反复登记的病族。
|
|
@@ -866,6 +871,7 @@ export declare function humanInputEventData(ev: {
|
|
|
866
871
|
actor?: unknown;
|
|
867
872
|
principal?: unknown;
|
|
868
873
|
entryId?: unknown;
|
|
874
|
+
attachments?: unknown;
|
|
869
875
|
eventId?: unknown;
|
|
870
876
|
parentToolCallId?: unknown;
|
|
871
877
|
}): Record<string, unknown>;
|
package/dist/trace/project.js
CHANGED
|
@@ -665,6 +665,16 @@ export function wiringManifestFaceEventData(ev, operatorFace, instructions) {
|
|
|
665
665
|
export function humanInputEventData(ev) {
|
|
666
666
|
const str = (v) => (typeof v === "string" && v.length > 0 ? v : undefined);
|
|
667
667
|
const a = typeof ev.actor === "object" && ev.actor !== null && !Array.isArray(ev.actor) ? { ...ev.actor } : {};
|
|
668
|
+
const attachments = Array.isArray(ev.attachments)
|
|
669
|
+
? ev.attachments
|
|
670
|
+
.filter((m) => typeof m === "object" && m !== null && !Array.isArray(m))
|
|
671
|
+
.map((m) => ({
|
|
672
|
+
...(typeof m.ordinal === "number" && Number.isFinite(m.ordinal) ? { ordinal: m.ordinal } : {}),
|
|
673
|
+
...(str(m.kind) !== undefined ? { kind: m.kind } : {}),
|
|
674
|
+
...(str(m.outcome) !== undefined ? { outcome: m.outcome } : {}),
|
|
675
|
+
...(str(m.label) !== undefined ? { label: redactSecrets(String(m.label)) } : {}),
|
|
676
|
+
}))
|
|
677
|
+
: undefined;
|
|
668
678
|
const actor = {
|
|
669
679
|
...(str(a.id) !== undefined ? { id: redactSecrets(String(a.id)) } : {}),
|
|
670
680
|
...(typeof a.hostAsserted === "boolean" ? { hostAsserted: a.hostAsserted } : {}),
|
|
@@ -679,6 +689,7 @@ export function humanInputEventData(ev) {
|
|
|
679
689
|
...(str(ev.issuer) !== undefined ? { issuer: redactSecrets(String(ev.issuer)) } : {}),
|
|
680
690
|
...(Object.keys(actor).length > 0 ? { actor } : {}),
|
|
681
691
|
...(str(ev.entryId) !== undefined ? { entryId: ev.entryId } : {}),
|
|
692
|
+
...(attachments !== undefined ? { attachments } : {}),
|
|
682
693
|
...identityFields(ev),
|
|
683
694
|
};
|
|
684
695
|
}
|
|
@@ -161,7 +161,7 @@ export declare const WIRE_PROJECTION_FACES: {
|
|
|
161
161
|
readonly human_input: {
|
|
162
162
|
readonly projector: "humanInputEventData";
|
|
163
163
|
readonly coreType: string;
|
|
164
|
-
readonly excluded: readonly ["principal"
|
|
164
|
+
readonly excluded: readonly ["principal"];
|
|
165
165
|
readonly minted: readonly [];
|
|
166
166
|
};
|
|
167
167
|
readonly task_notification: {
|
|
@@ -30,7 +30,7 @@ export const WIRE_PROJECTION_FACES = {
|
|
|
30
30
|
context_usage: { projector: "contextUsageEventData", coreType: arm("context_usage"), excluded: [], minted: [] },
|
|
31
31
|
turn_end: { projector: "turnEndEventData", coreType: arm("turn_end"), excluded: [], minted: [] },
|
|
32
32
|
compaction_outcome: { projector: "compactionOutcomeEventData", coreType: arm("compaction_outcome"), excluded: [], minted: [] },
|
|
33
|
-
human_input: { projector: "humanInputEventData", coreType: arm("human_input"), excluded: ["principal"
|
|
33
|
+
human_input: { projector: "humanInputEventData", coreType: arm("human_input"), excluded: ["principal"], minted: [] },
|
|
34
34
|
task_notification: {
|
|
35
35
|
projector: "taskNotificationEventData",
|
|
36
36
|
coreType: `TaskNotificationPayload & ${arm("task_notification")}`,
|
|
@@ -28,4 +28,11 @@ export declare function buildObjectivePreview(text: string, chars: number): stri
|
|
|
28
28
|
export declare const RUN_OBJECTIVE_PREVIEW_CHARS = 120;
|
|
29
29
|
/** run 行的预览:用户写的那段 → 脱敏 → 截 {@link RUN_OBJECTIVE_PREVIEW_CHARS};没有用户正文 ⇒ `""`(列写空串 = 有记录、剥空)。 */
|
|
30
30
|
export declare function buildRunObjectivePreview(objective: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* 这条消息是不是**附件帧**(core 7.50.0 请求层附件:目标声明的每只文件由引擎铸成一条自己的 user 消息,紧跟目标提交)。
|
|
33
|
+
* 判据 = 引擎写在消息上的送达记录 `attachment` **在场**(core:那只记录是「这条消息是附件帧」的唯一来源,文本从不判)。坏形记录同样算在场 ——
|
|
34
|
+
* 两个读者都往「不是人写的话」那一侧落(预览走下一条;会话视图按文件字节脱敏),那是安全的一侧。
|
|
35
|
+
* 一个事实一个读法:会话视图的处置表(`audit.ts`)与本地会话列表的预览(`plugins/local-session-store.ts`)同调这一只。
|
|
36
|
+
*/
|
|
37
|
+
export declare function isAttachmentFrame(message: unknown): boolean;
|
|
31
38
|
//# sourceMappingURL=user-authored-text.d.ts.map
|
|
@@ -15,4 +15,7 @@ export const RUN_OBJECTIVE_PREVIEW_CHARS = 120;
|
|
|
15
15
|
export function buildRunObjectivePreview(objective) {
|
|
16
16
|
return buildObjectivePreview(objective, RUN_OBJECTIVE_PREVIEW_CHARS);
|
|
17
17
|
}
|
|
18
|
+
export function isAttachmentFrame(message) {
|
|
19
|
+
return typeof message === "object" && message !== null && Object.hasOwn(message, "attachment");
|
|
20
|
+
}
|
|
18
21
|
//# sourceMappingURL=user-authored-text.js.map
|