@sema-agent/server 7.129.1 → 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 +102 -0
- package/README.md +8 -5
- package/README.zh-CN.md +6 -4
- package/USAGE.md +48 -9
- package/dist/audit.js +4 -1
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/resolve-spec.js +43 -16
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/stage-10-http-server.js +7 -2
- 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 +19 -6
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +34 -0
- package/dist/capabilities/scenarios.js +20 -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/skills.js +1 -1
- 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/exec-bounds.d.ts +6 -6
- package/dist/hooks/hook-llm.d.ts +11 -1
- package/dist/hooks/hook-llm.js +11 -14
- package/dist/hooks/hook-runner.d.ts +55 -9
- 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 +13 -5
- package/dist/http/routes/runs.js +18 -14
- 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 -4
- 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 +16 -7
- package/dist/observability/fail-open.js +10 -2
- package/dist/plugins/local-session-store.js +2 -2
- package/dist/plugins/one-time-migrations.d.ts +21 -1
- package/dist/plugins/one-time-migrations.js +33 -1
- package/dist/plugins/remote-env-adb.js +5 -4
- package/dist/plugins/remote-env-local-docker.js +5 -4
- package/dist/plugins/remote-env-ssh.js +5 -4
- 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/runtime-caps-resolver.d.ts +7 -0
- package/dist/runtime-caps-resolver.js +1 -0
- package/dist/session-titler.d.ts +4 -1
- package/dist/session-titler.js +13 -3
- 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 +29 -0
- package/dist/stored-request-tool-axes-m19.js +27 -0
- package/dist/task-mcp.d.ts +55 -60
- package/dist/task-mcp.js +197 -96
- package/dist/task-settings.d.ts +41 -5
- package/dist/task-settings.js +39 -25
- package/dist/tool-axis-words.d.ts +14 -17
- 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/skills-mcp.d.ts +1 -1
- package/dist/web-admin-client/types.d.ts +1 -1
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +3 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import vm from "node:vm";
|
|
3
3
|
import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
|
|
4
|
-
import {
|
|
4
|
+
import { classifyHooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_ENTRY_TYPES, HOOK_EVENTS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, hookMatcherForm, hookMatcherSelects, interpolateHookEnvVars, } from "@sema-agent/settings-schema/hooks";
|
|
5
|
+
import { formatConfigPath, outgoingPath } from "@sema-agent/settings-schema";
|
|
5
6
|
import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
|
|
6
7
|
import { boundedNoticeText } from "../fleet/fleet-bus.js";
|
|
7
8
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
@@ -57,6 +58,23 @@ function toAssistantTurnError(errorKind) {
|
|
|
57
58
|
return "unknown";
|
|
58
59
|
return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
|
|
59
60
|
}
|
|
61
|
+
const HOOK_ENTRY_TYPE_RUNS = {
|
|
62
|
+
command: true,
|
|
63
|
+
http: true,
|
|
64
|
+
prompt: true,
|
|
65
|
+
agent: true,
|
|
66
|
+
mcp_tool: false,
|
|
67
|
+
};
|
|
68
|
+
function isSupportedHookEntryType(t) {
|
|
69
|
+
return HOOK_ENTRY_TYPE_RUNS[t];
|
|
70
|
+
}
|
|
71
|
+
export const SUPPORTED_HOOK_ENTRY_TYPES = HOOK_ENTRY_TYPES.filter(isSupportedHookEntryType);
|
|
72
|
+
function isHookEventName(key) {
|
|
73
|
+
return HOOK_EVENTS.includes(key);
|
|
74
|
+
}
|
|
75
|
+
function isRunnableHookEntry(entry) {
|
|
76
|
+
return isSupportedHookEntryType(entry.type);
|
|
77
|
+
}
|
|
60
78
|
export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
|
|
61
79
|
export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 1_000;
|
|
62
80
|
export const MATCHER_ECHO_CHARS = 200;
|
|
@@ -201,23 +219,61 @@ export function toolMatcherIssue(config, depth, budget = {}) {
|
|
|
201
219
|
}
|
|
202
220
|
return undefined;
|
|
203
221
|
}
|
|
222
|
+
const HOOKS_PIECE_MEANING = {
|
|
223
|
+
"hooks-not-object": "`hooks` must be an object keyed by hook event name",
|
|
224
|
+
"unknown-event": "not a hook event name",
|
|
225
|
+
"event-not-array": "an event's value must be an array of matcher groups",
|
|
226
|
+
"matcher-invalid": "a matcher group must be an object with an optional string `matcher` and an array `hooks`",
|
|
227
|
+
"entry-invalid": "a hook entry must be an object whose `type` and fields are valid for that type",
|
|
228
|
+
"guard-outside-hooks": "a PreToolUse / PermissionRequest hook sits outside `hooks`",
|
|
229
|
+
"hooks-is-matcher": "`hooks` holds a matcher group or an entry directly; it must be an object keyed by hook event name",
|
|
230
|
+
"guard-under-unknown-event": "an unknown event name holds a PreToolUse / PermissionRequest hook",
|
|
231
|
+
"guard-event-not-array": "a PreToolUse / PermissionRequest event's value must be an array of matcher groups",
|
|
232
|
+
"guard-matcher-invalid": "a PreToolUse / PermissionRequest matcher group is not valid",
|
|
233
|
+
"guard-entry-invalid": "a PreToolUse / PermissionRequest hook entry is not valid",
|
|
234
|
+
};
|
|
235
|
+
export function hooksCoordinate(path) {
|
|
236
|
+
return `settings.${formatConfigPath(outgoingPath(path))}`;
|
|
237
|
+
}
|
|
238
|
+
function hooksPieceRefusal(piece) {
|
|
239
|
+
const field = piece.field !== undefined ? ` (field: ${piece.field})` : "";
|
|
240
|
+
return `${hooksCoordinate(piece.path)}: ${piece.code} — ${HOOKS_PIECE_MEANING[piece.code]}${field}`;
|
|
241
|
+
}
|
|
242
|
+
function unsupportedHookEntryTypeRefusal(path, type) {
|
|
243
|
+
return `${hooksCoordinate(path)}: this server does not run hook entries of type ${type}; supported: ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`;
|
|
244
|
+
}
|
|
204
245
|
export function parseHooksConfig(raw) {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
const first =
|
|
208
|
-
|
|
246
|
+
const classified = classifyHooksConfig({ hooks: raw });
|
|
247
|
+
if (classified.outcome !== "loaded") {
|
|
248
|
+
const first = classified.outcome === "rejected" ? classified.rejections[0] : classified.dropped[0];
|
|
249
|
+
const pieces = classified.outcome === "rejected" ? classified.rejections : classified.dropped;
|
|
250
|
+
return { error: first !== undefined ? hooksPieceRefusal(first) : `${hooksCoordinate(["hooks"])}: the hooks reader refused this configuration without naming a piece`, pieces };
|
|
209
251
|
}
|
|
210
|
-
const
|
|
211
|
-
|
|
252
|
+
const loaded = classified.hooks;
|
|
253
|
+
if (loaded === undefined) {
|
|
254
|
+
return { error: hooksPieceRefusal({ code: "hooks-not-object", path: ["hooks"] }) };
|
|
255
|
+
}
|
|
256
|
+
const config = {};
|
|
257
|
+
for (const key of Object.keys(loaded)) {
|
|
258
|
+
if (!isHookEventName(key)) {
|
|
259
|
+
return { error: hooksPieceRefusal({ code: "unknown-event", path: ["hooks", key] }) };
|
|
260
|
+
}
|
|
261
|
+
const event = key;
|
|
262
|
+
const groups = loaded[event];
|
|
212
263
|
if (!groups)
|
|
213
264
|
continue;
|
|
214
265
|
if (groups.length > MAX_HOOK_MATCHER_GROUPS) {
|
|
215
266
|
return { error: `settings.hooks.${event}: ${groups.length} matcher groups exceeds cap ${MAX_HOOK_MATCHER_GROUPS}` };
|
|
216
267
|
}
|
|
217
268
|
let entries = 0;
|
|
218
|
-
|
|
219
|
-
|
|
269
|
+
const kept = [];
|
|
270
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
271
|
+
const hooks = [];
|
|
272
|
+
for (const [entryIndex, entry] of group.hooks.entries()) {
|
|
220
273
|
entries++;
|
|
274
|
+
if (!isRunnableHookEntry(entry)) {
|
|
275
|
+
return { error: unsupportedHookEntryTypeRefusal(["hooks", event, groupIndex, "hooks", entryIndex, "type"], entry.type) };
|
|
276
|
+
}
|
|
221
277
|
if (entry.type === "command" && entry.command.length > MAX_HOOK_COMMAND_CHARS) {
|
|
222
278
|
return { error: `settings.hooks.${event}: command exceeds ${MAX_HOOK_COMMAND_CHARS} chars` };
|
|
223
279
|
}
|
|
@@ -237,11 +293,14 @@ export function parseHooksConfig(raw) {
|
|
|
237
293
|
return { error: `settings.hooks.${event}: allowedEnvVars exceeds cap ${MAX_HOOK_HTTP_HEADERS}` };
|
|
238
294
|
}
|
|
239
295
|
}
|
|
296
|
+
hooks.push(entry);
|
|
240
297
|
}
|
|
298
|
+
kept.push(group.matcher !== undefined ? { matcher: group.matcher, hooks } : { hooks });
|
|
241
299
|
}
|
|
242
300
|
if (entries > MAX_HOOK_ENTRIES_PER_EVENT) {
|
|
243
301
|
return { error: `settings.hooks.${event}: ${entries} entries exceeds cap ${MAX_HOOK_ENTRIES_PER_EVENT}` };
|
|
244
302
|
}
|
|
303
|
+
config[event] = kept;
|
|
245
304
|
}
|
|
246
305
|
return { config };
|
|
247
306
|
}
|
|
@@ -712,6 +771,23 @@ function parseHookStdout(stdout) {
|
|
|
712
771
|
return undefined;
|
|
713
772
|
}
|
|
714
773
|
}
|
|
774
|
+
function runRunnableEntry(entry, payload, ctx, event, llmExtra) {
|
|
775
|
+
switch (entry.type) {
|
|
776
|
+
case "http":
|
|
777
|
+
return runHttpHook(entry, payload, ctx, event);
|
|
778
|
+
case "prompt":
|
|
779
|
+
return runLlmHook(entry, payload, ctx, llmExtra);
|
|
780
|
+
case "agent":
|
|
781
|
+
return runLlmHook(entry, payload, ctx, { system: ccAgentHookSystemFor(event), event });
|
|
782
|
+
case "command":
|
|
783
|
+
return runCommandHook(entry, payload, ctx);
|
|
784
|
+
default: {
|
|
785
|
+
const unreachable = entry;
|
|
786
|
+
void unreachable;
|
|
787
|
+
throw new Error(`hook entry of an unsupported type reached the runner although parseHooksConfig admits only ${SUPPORTED_HOOK_ENTRY_TYPES.join(" / ")}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
715
791
|
async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceFired, deadlineAt, keyBase, llmExtra) {
|
|
716
792
|
const results = [];
|
|
717
793
|
let llmEntries = 0;
|
|
@@ -731,10 +807,6 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
|
|
|
731
807
|
const onceKey = `${keyBase ?? event}:${gi}:${ei}`;
|
|
732
808
|
if (entry.once && onceFired.has(onceKey))
|
|
733
809
|
continue;
|
|
734
|
-
if (entry.type !== "command" && entry.type !== "http" && entry.type !== "prompt" && entry.type !== "agent") {
|
|
735
|
-
ctx.logger.warn("hook_entries_skipped", { event, type: entry.type, reason: "type_not_supported" });
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
810
|
if (entry.type === "prompt" || entry.type === "agent") {
|
|
739
811
|
const carrier = entry.type === "prompt" ? ctx.hookLlm : ctx.hookAgent;
|
|
740
812
|
if (!carrier) {
|
|
@@ -771,9 +843,7 @@ async function runMatchingEntries(event, groups, matchValue, payload, ctx, onceF
|
|
|
771
843
|
onceFired.add(onceKey);
|
|
772
844
|
if (entry.type === "prompt" || entry.type === "agent")
|
|
773
845
|
llmEntries++;
|
|
774
|
-
const run =
|
|
775
|
-
: entry.type === "prompt" || entry.type === "agent" ? await runLlmHook(entry, payload, ctx, entry.type === "prompt" ? llmExtra : { system: ccAgentHookSystemFor(event), event })
|
|
776
|
-
: await runCommandHook(entry, payload, ctx);
|
|
846
|
+
const run = await runRunnableEntry(entry, payload, ctx, event, llmExtra);
|
|
777
847
|
if (run.spawnError) {
|
|
778
848
|
ctx.logger.warn(entry.type === "http" ? "hook_http_request_failed" : entry.type === "prompt" || entry.type === "agent" ? "hook_llm_failed" : "hook_command_spawn_failed", { event, type: entry.type, error: clip(run.spawnError, 300) });
|
|
779
849
|
if (!run.noticed)
|
package/dist/http/admission.js
CHANGED
|
@@ -16,7 +16,7 @@ import { verifyRoundsFromBody } from "./verify-rounds.js";
|
|
|
16
16
|
import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
|
|
17
17
|
import { resolveDevicePlacementIntent } from "./device-placement.js";
|
|
18
18
|
import { createRunAcceptance } from "../run-acceptance.js";
|
|
19
|
-
import { sendError,
|
|
19
|
+
import { sendError, sendHttpError } from "./send.js";
|
|
20
20
|
import { ABSOLUTE_HOST_PATH_MAX, isAbsoluteHostPath, WEB_ADMIN_ENV } from "@sema-agent/settings-schema";
|
|
21
21
|
const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
|
|
22
22
|
export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
|
|
@@ -655,7 +655,7 @@ export function createAdmission(ctx) {
|
|
|
655
655
|
onTypedFailure(err);
|
|
656
656
|
return null;
|
|
657
657
|
}
|
|
658
|
-
|
|
658
|
+
sendHttpError(res, err);
|
|
659
659
|
return null;
|
|
660
660
|
}
|
|
661
661
|
throw err;
|
|
@@ -73,7 +73,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
73
73
|
return undefined;
|
|
74
74
|
if (read.kind === "absent")
|
|
75
75
|
return undefined;
|
|
76
|
-
return storedBodyReplayRefusal(read.ctx, deps.config
|
|
76
|
+
return storedBodyReplayRefusal(read.ctx, deps.config);
|
|
77
77
|
};
|
|
78
78
|
let parkedSkipped = 0;
|
|
79
79
|
const replaySkipped = [];
|
|
@@ -96,7 +96,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
96
96
|
deps.logger?.warn?.("deny_sweep_stored_body_skipped", {
|
|
97
97
|
count: replaySkipped.length,
|
|
98
98
|
skipped: replaySkipped,
|
|
99
|
-
note: "these expired approvals belong to tasks whose stored request this version does not replay (kind allow_list_reading: settings.permissions.allow written under an earlier reading; kind
|
|
99
|
+
note: "these expired approvals belong to tasks whose stored request this version does not replay (kind allow_list_reading: settings.permissions.allow written under an earlier reading; kind inert_tool_list: a tool-list entry (a skill disallowedTools, an agent definition's denyTools / allowTools or the request's excludeTools / allowTools) the engine reads as empty), so the SLA deny cannot be delivered. They stay suspended until the terminal backstop reaps them or someone cancels them; nothing was retried",
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* · 拒句按「这行是在哪种读法下写的」分两句({@link ALLOW_LIST_READING_ERAS} 穷举):无记号(或记号认不得)= 名单时代;`"statement"` = 表态时代。
|
|
17
17
|
* · 体里没有 `allow`(或空数组 —— 三代都把空数组读成「没有」)的行不写记号、不受本规则影响。
|
|
18
18
|
*
|
|
19
|
-
* ## 第二问(7.117.0
|
|
20
|
-
*
|
|
21
|
-
* 7.42 什么也不拒的条目)
|
|
19
|
+
* ## 第二问(7.117.0 起,引擎 7.42 逐字读规则条目;7.131.0 起覆盖全部工具名清单):工具名清单读成空的存量体
|
|
20
|
+
* 同一面上的第二个「新提交会被拒、存量体重放不能放过」:提交体里工具名清单 —— 技能(顶层 / 子代)的 `disallowedTools`(7.117.0:`"Bash "` 这类 7.41 去白后有效、
|
|
21
|
+
* 7.42 什么也不拒的条目)、子代定义的 `denyTools` / `allowTools`、顶层 `excludeTools` / `allowTools`(7.131.0:限制侧近似名 / 大小写孪生、退役名、文法读不出,放行侧只近似名)—— 有引擎读成空的条目 ⇒ 提交 400(`resolveSpec`,拒点);
|
|
22
|
+
* 存量停驻体重放 ⇒ 409 `resume_blocked_by_policy`(`blockedBy` = 该层的拒码)。判据函数 = `rule-entry-list.ts` `inertToolListEntryOf`。
|
|
22
23
|
*
|
|
23
24
|
* ## 第三问(7.122.0):体里模式词的读法记号
|
|
24
25
|
* 7.122.0 起一条 run 的模式词只有一个源 —— 存储的提交体顶层 `permissionMode`(提交入口把运维 `AUTONOMY=plan` 的地板盖进去一次;`plan_review` 写回改它),
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
* 问点五个,同调同答:续跑驱动段(`resume-legs.ts` `resolveResumeSpecOrBlocked`,另四条腿的唯一判点)· plan_review 的 prepare 段(读类拒,在 nonce 消费之前)·
|
|
37
38
|
* SLA 拒扫腿的跳过臂(`approval-sweeps.ts`)· 终态 run 的 steer 铸唤醒卡之前(`routes/runs.ts`:铸卡前先证明它赎得回,证不出 ⇒ 不铸、当场同一个拒)·
|
|
38
39
|
* `resolveSpec` 本身留作真正的拒点(提交腿 400;续跑腿在到它之前已被本判据答掉)。再开第六个问点必须调这两只,不许各判各的 —— 此前铸卡前只判了
|
|
39
|
-
* allow
|
|
40
|
+
* allow 记号,工具名清单那一问漏在外面,卡铸出来了、`/wake` 恒 409([ref] 合并复审两腿同报)。
|
|
40
41
|
*
|
|
41
42
|
* ## 何时删(一次性迁移,不是长期双读)
|
|
42
43
|
* 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件与窗登记在 `plugins/one-time-migrations.ts` 的
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
import type { PermissionMode } from "@sema-agent/core";
|
|
47
48
|
import type { CheckpointCtx } from "./route-ctx.js";
|
|
48
49
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
49
|
-
import type {
|
|
50
|
+
import type { InertToolListEntry } from "../rule-entry-list.js";
|
|
50
51
|
/** 读法记号的闭集(一词 = 当前读法):这一行体里的 `settings.permissions.allow` 是按「交给引擎的 allow 层」写下、也该按它读的
|
|
51
52
|
* (与能力位 `settings.permissions.allowLayer` 同一个说法)。 */
|
|
52
53
|
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["allow_layer"];
|
|
@@ -140,8 +141,8 @@ export type StoredBodyReplayRefusal = {
|
|
|
140
141
|
readonly era: AllowListReadingEra;
|
|
141
142
|
readonly writerVersion?: string;
|
|
142
143
|
} | {
|
|
143
|
-
readonly kind: "
|
|
144
|
-
readonly entry:
|
|
144
|
+
readonly kind: "inert_tool_list";
|
|
145
|
+
readonly entry: InertToolListEntry;
|
|
145
146
|
} | {
|
|
146
147
|
readonly kind: "permission_mode_reading";
|
|
147
148
|
readonly stored: PermissionMode | undefined;
|
|
@@ -168,7 +169,7 @@ export interface ResumeRefusalAnswer {
|
|
|
168
169
|
}
|
|
169
170
|
/**
|
|
170
171
|
* 「续跑被一条准入策略挡住」的**唯一**铸点:`resolveSpec` 在重放存量体时抛出的 `HttpError`(400 族)折成 409 `resume_blocked_by_policy`,
|
|
171
|
-
* `blockedBy` = 那条 400 的码(属主 `http/send.ts` `httpErrorCode`),`extra` 原样随体。{@link storedBodyReplayAnswer}
|
|
172
|
+
* `blockedBy` = 那条 400 的码(属主 `http/send.ts` `httpErrorCode`),`extra` 原样随体。{@link storedBodyReplayAnswer} 的工具名清单那一臂与
|
|
172
173
|
* `resume-legs.ts` 的折叠口都调它 —— 句子与码只在这里写一次(冻结门 `api-error-text-freeze` 的模板骨架表锚这一处)。
|
|
173
174
|
*/
|
|
174
175
|
export declare function resumeBlockedByPolicyAnswer(message: string, blockedBy: string, extra?: Record<string, unknown> | undefined): ResumeRefusalAnswer;
|
|
@@ -179,15 +180,15 @@ export declare function resumeBlockedByPolicyAnswer(message: string, blockedBy:
|
|
|
179
180
|
export declare function storedBodyReplayLogFields(refusal: StoredBodyReplayRefusal): {
|
|
180
181
|
kind: StoredBodyReplayRefusal["kind"];
|
|
181
182
|
era?: AllowListReadingEra;
|
|
182
|
-
layer?:
|
|
183
|
+
layer?: InertToolListEntry["layer"];
|
|
183
184
|
storedMode?: PermissionMode | null;
|
|
184
185
|
floor?: PermissionMode;
|
|
185
186
|
note: string;
|
|
186
187
|
};
|
|
187
188
|
/**
|
|
188
189
|
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的答复:每条规则一只既有铸点(allow 记号 ⇒ {@link allowListReadingRefusal};模式词记号 ⇒ {@link permissionModeReadingRefusal};
|
|
189
|
-
* 这两只答 `checkpoint.unsupported_version`,体按那一码的键律补两版本键,`readerVersion` =
|
|
190
|
-
*
|
|
190
|
+
* 这两只答 `checkpoint.unsupported_version`,体按那一码的键律补两版本键,`readerVersion` = 调用方传的本进程服务版本;工具名清单 ⇒ {@link resumeBlockedByPolicyAnswer},与提交腿的 400
|
|
191
|
+
* (该层的拒码:`request.field_invalid` / `config.agent.unknown_tool` / `config.tool_face_control`)同码同句 —— 体上只有这一个问题时;存量体同时还踩中 `resolveSpec` 更早的拒〔三桶超帽 / 托管姿态〕时,提交侧答更早那条、续跑侧答本句,两边都是拒,指路不同)。
|
|
191
192
|
*/
|
|
192
193
|
export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal, readerVersion: string): ResumeRefusalAnswer;
|
|
193
194
|
//# sourceMappingURL=checkpoint-ctx.d.ts.map
|
|
@@ -87,8 +87,8 @@ export function storedBodyReplayLogFields(refusal) {
|
|
|
87
87
|
switch (refusal.kind) {
|
|
88
88
|
case "allow_list_reading":
|
|
89
89
|
return { kind: refusal.kind, era: refusal.era, note: "this session's stored request carries settings.permissions.allow written under an earlier reading (list era: a whitelist; statement era: a statement that cleared only the unstated question); it is not replayed under the current reading — the task stays suspended until someone cancels it" };
|
|
90
|
-
case "
|
|
91
|
-
return { kind: refusal.kind, layer: refusal.entry.layer, note: "this session's stored request carries a
|
|
90
|
+
case "inert_tool_list":
|
|
91
|
+
return { kind: refusal.kind, layer: refusal.entry.layer, note: "this session's stored request carries a tool-list entry the engine reads as empty (a skill disallowedTools, an agent definition's denyTools / allowTools, the request's excludeTools / allowTools, or the rules in settings.permissions.deny / ask: a retired name, a case twin, a spelling no tool name can have, a rule core's grammar cannot compile, or a ~/ path rule on a lane with no home), or a permission list that is malformed or over the entry cap in either direction; it is not replayed — the task stays suspended until someone cancels it and resubmits with the entry fixed" };
|
|
92
92
|
case "permission_mode_reading":
|
|
93
93
|
return { kind: refusal.kind, storedMode: refusal.stored ?? null, floor: refusal.floor, note: "this session's stored request was written before the operator's AUTONOMY plan floor was recorded in the task (earlier versions re-applied it on every resume); replaying it now would run under its submitted permission mode — the task stays suspended until someone cancels it, or AUTONOMY is set to read-only so it can finish with read-only hands" };
|
|
94
94
|
default: {
|
|
@@ -103,8 +103,8 @@ export function storedBodyReplayAnswer(refusal, readerVersion) {
|
|
|
103
103
|
const answer = allowListReadingRefusal(refusal.era);
|
|
104
104
|
return { status: answer.status, body: { ...answer.body, ...buildCheckpointVersionKeys(refusal.writerVersion, readerVersion) } };
|
|
105
105
|
}
|
|
106
|
-
case "
|
|
107
|
-
return resumeBlockedByPolicyAnswer(refusal.entry.message,
|
|
106
|
+
case "inert_tool_list":
|
|
107
|
+
return resumeBlockedByPolicyAnswer(refusal.entry.message, refusal.entry.code, refusal.entry.extra !== undefined ? { ...refusal.entry.extra } : undefined);
|
|
108
108
|
case "permission_mode_reading": {
|
|
109
109
|
const answer = permissionModeReadingRefusal();
|
|
110
110
|
return { status: answer.status, body: { ...answer.body, ...buildCheckpointVersionKeys(refusal.writerVersion, readerVersion) } };
|
package/dist/http/dispatch.js
CHANGED
|
@@ -149,7 +149,7 @@ export function createDispatch(ctx) {
|
|
|
149
149
|
configHash,
|
|
150
150
|
pid: process.pid,
|
|
151
151
|
startedAt: PROCESS_STARTED_AT_MS,
|
|
152
|
-
port:
|
|
152
|
+
port: req.socket.localPort,
|
|
153
153
|
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
154
154
|
...buildHealthSelfReport(deps, hasServiceAuth(deps.config)),
|
|
155
155
|
...(signal.restart ? { ...(restartActionable(signal) ? { restartRequired: true } : {}), restart: signal.restart } : {}),
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -422,11 +422,11 @@ export function createResumeLegs(ctx) {
|
|
|
422
422
|
noteLateSettleIfAccepted(driven);
|
|
423
423
|
return driven;
|
|
424
424
|
}
|
|
425
|
-
const
|
|
426
|
-
const
|
|
425
|
+
const ONE_SHOT_SUBMIT_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "rewindFiles", "userAttachments"];
|
|
426
|
+
const ONE_SHOT_SUBMIT_STRIP = Object.fromEntries(ONE_SHOT_SUBMIT_KEYS.map((k) => [k, undefined]));
|
|
427
427
|
const resolveResumeSpec = async (ctx, req, auth) => {
|
|
428
428
|
const engineNotices = [];
|
|
429
|
-
const spec = await deps.resolveSpec({ ...ctx.body, ...
|
|
429
|
+
const spec = await deps.resolveSpec({ ...ctx.body, ...ONE_SHOT_SUBMIT_STRIP }, req, auth, {
|
|
430
430
|
leg: "resume",
|
|
431
431
|
onNotice: (n) => engineNotices.push(n),
|
|
432
432
|
...(ctx.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
@@ -438,7 +438,7 @@ export function createResumeLegs(ctx) {
|
|
|
438
438
|
return storedBodyReplayAnswer(refusal, serviceVersion());
|
|
439
439
|
};
|
|
440
440
|
const resolveResumeSpecOrBlocked = async (ctx, req, auth) => {
|
|
441
|
-
const replay = storedBodyReplayRefusal(ctx, deps.config
|
|
441
|
+
const replay = storedBodyReplayRefusal(ctx, deps.config);
|
|
442
442
|
if (replay !== undefined)
|
|
443
443
|
return { ok: false, answer: refuseStoredBodyReplay(auth.sessionId, replay) };
|
|
444
444
|
try {
|
|
@@ -932,6 +932,7 @@ export function createResumeLegs(ctx) {
|
|
|
932
932
|
break;
|
|
933
933
|
}
|
|
934
934
|
case "human_input":
|
|
935
|
+
anchor.onHumanInput(ev);
|
|
935
936
|
await flush();
|
|
936
937
|
await append("human_input", humanInputEventData(ev));
|
|
937
938
|
break;
|
|
@@ -1400,7 +1401,7 @@ export function createResumeLegs(ctx) {
|
|
|
1400
1401
|
return refuse(resumeCtxCorruptAnswer(ctxRead.where));
|
|
1401
1402
|
if (ctxRead.kind === "absent")
|
|
1402
1403
|
return refuse({ status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } });
|
|
1403
|
-
const replay = storedBodyReplayRefusal(ctxRead.ctx, deps.config
|
|
1404
|
+
const replay = storedBodyReplayRefusal(ctxRead.ctx, deps.config);
|
|
1404
1405
|
if (replay !== undefined)
|
|
1405
1406
|
return refuse(refuseStoredBodyReplay(sessionId, replay));
|
|
1406
1407
|
return { ok: true, prepared: { sessionId, token, cp, ctx: ctxRead.ctx } };
|
|
@@ -22,7 +22,7 @@ import { callGrantBindingOf, questionGrantBindingOf } from "../../standing-answe
|
|
|
22
22
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
23
23
|
import { assertNever } from "../route-ctx.js";
|
|
24
24
|
import * as facts from "../../capability-facts.js";
|
|
25
|
-
import { readResumeCtx
|
|
25
|
+
import { readResumeCtx } from "../resume-ctx-read.js";
|
|
26
26
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
27
27
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
|
|
28
28
|
const DECIDE_BODY_DECLARED_KEYS = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
|
-
import { modelImageInput, isAtMentionable, selectableCatalog } from "../../model-select.js";
|
|
1
|
+
import { PERMISSION_MODES, USER_ATTACHMENT_SEAT, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
|
|
2
|
+
import { modelImageInput, modelReasoningDecl, isAtMentionable, selectableCatalog } from "../../model-select.js";
|
|
3
3
|
import { buildDroppedEntriesWire } from "../../web-admin-client/read-warnings.js";
|
|
4
4
|
import { buildCandidateRejectedWire } from "../../web-admin-client/candidate-gate.js";
|
|
5
5
|
import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
|
|
@@ -12,6 +12,7 @@ import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protoco
|
|
|
12
12
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
13
13
|
import { buildEmptyMcpProbeFace, buildMcpInjectionRefusal, buildMcpProbeCacheKey, buildMcpProbeInflightRefusal, buildMcpProbeRateKey, buildMcpProbeRateRefusal, createMcpProbeLocal, buildMcpProbeDeclarationRefusal, buildMcpProbeIncompleteRefusal, McpProbeIncomplete, MCP_PROBE_HTTP_STATUS, MCP_PROBE_MAX_INFLIGHT, MCP_PROBE_VERDICT_TIMEOUT_MS, createMcpProbeWalk, readMcpProbeBody, } from "../../capabilities/mcp-probe.js";
|
|
14
14
|
import { mcpForScenario } from "../../web-admin-client/facade.js";
|
|
15
|
+
import { sendScenarioNotFound } from "../scenario-read-face.js";
|
|
15
16
|
import { sendJson, sendError } from "../send.js";
|
|
16
17
|
import { assertNever } from "../route-ctx.js";
|
|
17
18
|
import { fileHistoryCaptureOf } from "../../file-history-capture.js";
|
|
@@ -51,7 +52,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
51
52
|
const name = match.params.name;
|
|
52
53
|
const detail = deps.scenarioDetails?.[name];
|
|
53
54
|
if (!detail) {
|
|
54
|
-
|
|
55
|
+
sendScenarioNotFound(res);
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
sendJson(res, 200, detail);
|
|
@@ -146,6 +147,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
146
147
|
disableSkills: true,
|
|
147
148
|
userInvokedSkills: true,
|
|
148
149
|
skillToolLists: true,
|
|
150
|
+
userAttachments: USER_ATTACHMENT_SEAT,
|
|
149
151
|
projectContext: cwdHonored(deps.config),
|
|
150
152
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
151
153
|
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined && autonomyKeepsOpenWriteFace(deps.config.autonomy),
|
|
@@ -217,7 +219,13 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
217
219
|
sendPrincipalRefusal(res, req, deps.config);
|
|
218
220
|
return;
|
|
219
221
|
}
|
|
220
|
-
|
|
222
|
+
const scenarioParams = new URLSearchParams(match.search).getAll("scenario");
|
|
223
|
+
if (scenarioParams.length > 1) {
|
|
224
|
+
sendError(res, 400, "request.field_invalid", "scenario must be given at most once (omit it to read this deployment's default scenario)");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const scenarioName = await deps.scenarioGate(principal, scenarioParams[0]);
|
|
228
|
+
await serveMcpProbeFace(res, ctx, principal, mcpForScenario(deps.config.mcpServers, scenarioName) ?? [], "deployment");
|
|
221
229
|
return;
|
|
222
230
|
}
|
|
223
231
|
case "capability-mcp-probe": {
|
|
@@ -247,7 +255,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
247
255
|
name,
|
|
248
256
|
id: m.id,
|
|
249
257
|
provider: m.provider,
|
|
250
|
-
reasoning: m
|
|
258
|
+
...(modelReasoningDecl(m) !== undefined ? { reasoning: modelReasoningDecl(m) } : {}),
|
|
251
259
|
...(modelImageInput(m) !== "undeclared" ? { vision: modelImageInput(m) === "yes" } : {}),
|
|
252
260
|
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
253
261
|
...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -599,6 +599,14 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
599
599
|
};
|
|
600
600
|
const job = prior.then(async () => {
|
|
601
601
|
const cs = deps.checkpointStore;
|
|
602
|
+
const withdrawWakeCard = async (token, scope, arm) => {
|
|
603
|
+
try {
|
|
604
|
+
await cs.expire(token, scope);
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
deps.logger?.warn?.("steer_wake_card_withdraw_failed", { taskId, sessionId: run.sessionId, arm, error: redactHead(e instanceof Error ? e.message : String(e), 2000) });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
602
610
|
const reprobe = await tryPark();
|
|
603
611
|
if (reprobe === "parked") {
|
|
604
612
|
mintResult.parked = true;
|
|
@@ -624,7 +632,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
624
632
|
mintResult.noResumeCtx = true;
|
|
625
633
|
return;
|
|
626
634
|
}
|
|
627
|
-
const unservable = storedBodyReplayRefusal(resumeRead.ctx, deps.config
|
|
635
|
+
const unservable = storedBodyReplayRefusal(resumeRead.ctx, deps.config);
|
|
628
636
|
if (unservable !== undefined) {
|
|
629
637
|
mintResult.unservable = unservable;
|
|
630
638
|
return;
|
|
@@ -645,18 +653,18 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
645
653
|
});
|
|
646
654
|
const stillRead = await readResumeCtx(cs.getCtx(run.sessionId));
|
|
647
655
|
if (stillRead.kind === "corrupt") {
|
|
648
|
-
await
|
|
656
|
+
await withdrawWakeCard(wakeToken, cpScope, "resume_context_corrupt");
|
|
649
657
|
mintResult.corruptCtxWhere = stillRead.where;
|
|
650
658
|
return;
|
|
651
659
|
}
|
|
652
660
|
if (stillRead.kind !== "ok") {
|
|
653
|
-
await
|
|
661
|
+
await withdrawWakeCard(wakeToken, cpScope, "resume_context_absent");
|
|
654
662
|
mintResult.noResumeCtx = true;
|
|
655
663
|
return;
|
|
656
664
|
}
|
|
657
665
|
const winner = await cs.findPendingTokenBySession(run.sessionId, undefined, { gateKinds: TERMINAL_PARK_GATE_KINDS, pick: "oldest" }).catch(() => null);
|
|
658
666
|
if (winner !== null && winner !== wakeToken) {
|
|
659
|
-
await
|
|
667
|
+
await withdrawWakeCard(wakeToken, cpScope, "converged_on_winner");
|
|
660
668
|
await parkOnce(winner, cpScope);
|
|
661
669
|
return;
|
|
662
670
|
}
|
|
@@ -680,7 +688,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
680
688
|
return;
|
|
681
689
|
}
|
|
682
690
|
if (mintResult.corruptCtxWhere !== undefined) {
|
|
683
|
-
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read —
|
|
691
|
+
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
|
|
684
692
|
return;
|
|
685
693
|
}
|
|
686
694
|
if (mintResult.unservable !== undefined) {
|
|
@@ -735,10 +743,6 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
735
743
|
}
|
|
736
744
|
const interruptBody = interruptNormalized;
|
|
737
745
|
const interruptBare = !("text" in interruptBody);
|
|
738
|
-
if (interruptBare && Object.keys(interruptBody).length > 0) {
|
|
739
|
-
sendError(res, 400, "request.body_shape", "body must be {} (bare halt: cut the in-flight turn and stop the run at this boundary) or { text: string (non-empty) } (cut + steer: the text reaches the model after the cut)");
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
746
|
let text = "";
|
|
743
747
|
if (!interruptBare) {
|
|
744
748
|
if (typeof interruptBody.text !== "string" || interruptBody.text.length === 0) {
|
|
@@ -746,11 +750,11 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
746
750
|
return;
|
|
747
751
|
}
|
|
748
752
|
text = interruptBody.text;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
753
|
+
}
|
|
754
|
+
const keyIssue = steerBodyKeyIssue(interruptBody, "interrupt");
|
|
755
|
+
if (keyIssue) {
|
|
756
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
757
|
+
return;
|
|
754
758
|
}
|
|
755
759
|
const interruptIdemRaw = headerStr(req.headers["idempotency-key"]);
|
|
756
760
|
const interruptInputId = interruptIdemRaw ? `idem-${idemKeyOf(interruptIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : uuidv7();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SessionError, materializeA2aTools, materializeMcpTools, sanitizePathComponent } from "@sema-agent/core";
|
|
2
2
|
import { a2aForScenario, mcpForScenario } from "../../web-admin-client/facade.js";
|
|
3
|
-
import { isUuidShape, legacyUnownedSessionOk } from "../../security.js";
|
|
3
|
+
import { HttpError, isUuidShape, legacyUnownedSessionOk } from "../../security.js";
|
|
4
4
|
import { windowMessages, truncateMessageBlobs, redactSessionViewForRead } from "../../audit.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
@@ -8,7 +8,7 @@ import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
|
|
|
8
8
|
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
9
9
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
10
10
|
import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
|
|
11
|
-
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
11
|
+
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
|
|
12
12
|
import { gatedPrincipal, explicitOperatorOk, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
13
13
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
14
14
|
import * as facts from "../../capability-facts.js";
|
|
@@ -144,10 +144,21 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
const lastLegMcp = await lastLegMcpFor(deps, safeDecode(match.params.sessionId), principal);
|
|
147
|
-
const
|
|
148
|
-
|
|
147
|
+
const ledgerHalf = lastLegMcp === undefined ? {} : { lastLegMcp };
|
|
148
|
+
let scenarioName;
|
|
149
|
+
try {
|
|
150
|
+
scenarioName = await deps.scenarioGate(principal, undefined);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
if (!(e instanceof HttpError))
|
|
154
|
+
throw e;
|
|
155
|
+
sendJson(res, 200, { asOf: new Date().toISOString(), ...ledgerHalf, scenarioRefusal: httpErrorBody(e) });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const specs = mcpForScenario(deps.config.mcpServers, scenarioName);
|
|
159
|
+
const cacheKey = `${scenarioName}::${principal ?? ""}`;
|
|
149
160
|
if (!specs || specs.length === 0) {
|
|
150
|
-
sendJson(res, 200, { servers: [], asOf: new Date().toISOString(), ...
|
|
161
|
+
sendJson(res, 200, { servers: [], asOf: new Date().toISOString(), ...ledgerHalf });
|
|
151
162
|
return;
|
|
152
163
|
}
|
|
153
164
|
const cached = mcpStatusCache.get(cacheKey);
|
|
@@ -179,23 +190,24 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
179
190
|
})();
|
|
180
191
|
mcpStatusCache.set(cacheKey, { at: Date.now(), panel });
|
|
181
192
|
}
|
|
182
|
-
sendJson(res, 200, { ...(await mcpStatusCache.get(cacheKey).panel), ...
|
|
193
|
+
sendJson(res, 200, { ...(await mcpStatusCache.get(cacheKey).panel), ...ledgerHalf });
|
|
183
194
|
return;
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
{
|
|
187
198
|
if (match.decl.id === "session-a2a") {
|
|
188
|
-
|
|
199
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
200
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
189
201
|
sendPrincipalRefusal(res, req, deps.config);
|
|
190
202
|
return;
|
|
191
203
|
}
|
|
192
|
-
const
|
|
204
|
+
const scenarioName = await deps.scenarioGate(principal, undefined);
|
|
205
|
+
const specs = a2aForScenario(deps.config.a2aPeers, scenarioName);
|
|
193
206
|
if (!specs || specs.length === 0) {
|
|
194
207
|
sendJson(res, 200, { peers: [], asOf: new Date().toISOString() });
|
|
195
208
|
return;
|
|
196
209
|
}
|
|
197
|
-
const
|
|
198
|
-
const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
|
|
210
|
+
const cacheKey = `${scenarioName}::${principal ?? ""}`;
|
|
199
211
|
const cached = a2aStatusCache.get(cacheKey);
|
|
200
212
|
if (!cached || Date.now() - cached.at > A2A_STATUS_TTL_MS) {
|
|
201
213
|
if (a2aStatusCache.size > 1024)
|