@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.
Files changed (117) hide show
  1. package/MIGRATION.md +102 -0
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +48 -9
  5. package/dist/audit.js +4 -1
  6. package/dist/bench/s1/live-deps.js +2 -0
  7. package/dist/boot/resolve-spec.js +43 -16
  8. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  9. package/dist/boot/stage-10-http-server.d.ts +1 -1
  10. package/dist/boot/stage-10-http-server.js +7 -2
  11. package/dist/capabilities/builtin-tools.js +2 -1
  12. package/dist/capabilities/composite-members.d.ts +1 -0
  13. package/dist/capabilities/composite-members.js +1 -0
  14. package/dist/capabilities/hands-lane.js +2 -2
  15. package/dist/capabilities/host-tool-names.d.ts +17 -0
  16. package/dist/capabilities/host-tool-names.js +15 -0
  17. package/dist/capabilities/mcp-probe.js +19 -6
  18. package/dist/capabilities/repo-tools.js +4 -3
  19. package/dist/capabilities/scenarios.d.ts +34 -0
  20. package/dist/capabilities/scenarios.js +20 -4
  21. package/dist/capabilities/select-environment-tool.js +2 -1
  22. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  23. package/dist/capabilities/send-user-file-tool.js +2 -2
  24. package/dist/capabilities/skills.js +1 -1
  25. package/dist/capabilities/tool-defer.js +2 -1
  26. package/dist/config-catalog.js +16 -5
  27. package/dist/config-invariants.js +2 -2
  28. package/dist/config-provider.js +2 -2
  29. package/dist/config-types.d.ts +6 -0
  30. package/dist/config.d.ts +2 -2
  31. package/dist/config.js +18 -17
  32. package/dist/elicitation.d.ts +7 -0
  33. package/dist/elicitation.js +20 -5
  34. package/dist/exec-bounds.d.ts +6 -6
  35. package/dist/hooks/hook-llm.d.ts +11 -1
  36. package/dist/hooks/hook-llm.js +11 -14
  37. package/dist/hooks/hook-runner.d.ts +55 -9
  38. package/dist/hooks/hook-runner.js +86 -16
  39. package/dist/http/admission.js +2 -2
  40. package/dist/http/approval-sweeps.js +2 -2
  41. package/dist/http/checkpoint-ctx.d.ts +12 -11
  42. package/dist/http/checkpoint-ctx.js +4 -4
  43. package/dist/http/dispatch.js +1 -1
  44. package/dist/http/resume-legs.js +6 -5
  45. package/dist/http/routes/approvals-assistant.js +1 -1
  46. package/dist/http/routes/capabilities.js +13 -5
  47. package/dist/http/routes/runs.js +18 -14
  48. package/dist/http/routes/sessions.js +22 -10
  49. package/dist/http/routes/tasks.js +16 -11
  50. package/dist/http/scenario-read-face.d.ts +14 -0
  51. package/dist/http/scenario-read-face.js +5 -0
  52. package/dist/http/send.d.ts +10 -0
  53. package/dist/http/send.js +6 -0
  54. package/dist/http/server.d.ts +16 -4
  55. package/dist/http/server.js +2 -2
  56. package/dist/http/stored-body-replay.d.ts +10 -3
  57. package/dist/http/stored-body-replay.js +27 -6
  58. package/dist/http/wire-types.d.ts +18 -5
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/mcp-assertion-header.d.ts +2 -2
  62. package/dist/mcp-assertion-header.js +3 -3
  63. package/dist/model-entry-refusal.d.ts +1 -7
  64. package/dist/model-route-endpoint.d.ts +51 -11
  65. package/dist/model-route-endpoint.js +45 -23
  66. package/dist/model-route-gap.d.ts +5 -4
  67. package/dist/model-route-gap.js +1 -1
  68. package/dist/model-select.d.ts +58 -0
  69. package/dist/model-select.js +31 -0
  70. package/dist/namespaced-rule-name.d.ts +7 -4
  71. package/dist/namespaced-rule-name.js +4 -3
  72. package/dist/observability/fail-open.d.ts +16 -7
  73. package/dist/observability/fail-open.js +10 -2
  74. package/dist/plugins/local-session-store.js +2 -2
  75. package/dist/plugins/one-time-migrations.d.ts +21 -1
  76. package/dist/plugins/one-time-migrations.js +33 -1
  77. package/dist/plugins/remote-env-adb.js +5 -4
  78. package/dist/plugins/remote-env-local-docker.js +5 -4
  79. package/dist/plugins/remote-env-ssh.js +5 -4
  80. package/dist/question.d.ts +18 -9
  81. package/dist/question.js +21 -13
  82. package/dist/request-key-closure.d.ts +2 -2
  83. package/dist/request-key-closure.js +16 -9
  84. package/dist/rule-entry-list.d.ts +124 -17
  85. package/dist/rule-entry-list.js +140 -24
  86. package/dist/run-local.d.ts +3 -3
  87. package/dist/run-local.js +8 -7
  88. package/dist/runs.d.ts +42 -2
  89. package/dist/runs.js +45 -2
  90. package/dist/runtime-caps-resolver.d.ts +7 -0
  91. package/dist/runtime-caps-resolver.js +1 -0
  92. package/dist/session-titler.d.ts +4 -1
  93. package/dist/session-titler.js +13 -3
  94. package/dist/spec-fields.d.ts +8 -4
  95. package/dist/stored-request-hooks-m20.d.ts +23 -0
  96. package/dist/stored-request-hooks-m20.js +124 -0
  97. package/dist/stored-request-tool-axes-m19.d.ts +29 -0
  98. package/dist/stored-request-tool-axes-m19.js +27 -0
  99. package/dist/task-mcp.d.ts +55 -60
  100. package/dist/task-mcp.js +197 -96
  101. package/dist/task-settings.d.ts +41 -5
  102. package/dist/task-settings.js +39 -25
  103. package/dist/tool-axis-words.d.ts +14 -17
  104. package/dist/trace/core-keyset-guard.d.ts +2 -2
  105. package/dist/trace/ledger-sink.d.ts +29 -0
  106. package/dist/trace/ledger-sink.js +11 -0
  107. package/dist/trace/project.d.ts +6 -0
  108. package/dist/trace/project.js +11 -0
  109. package/dist/trace/wire-projection-faces.d.ts +1 -1
  110. package/dist/trace/wire-projection-faces.js +1 -1
  111. package/dist/user-authored-text.d.ts +7 -0
  112. package/dist/user-authored-text.js +3 -0
  113. package/dist/web-admin-client/apply-effective.js +10 -5
  114. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  115. package/dist/web-admin-client/types.d.ts +1 -1
  116. package/package.json +2 -2
  117. package/scripts/drain-check-local.mjs +3 -2
@@ -2,10 +2,11 @@ import { realpathSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, WRITE_EXEMPT_DIR_COVERS_ROOT_CODE, deriveAskEffective, expandTiers, modePolicyOf, resolveAskSeamForm } from "@sema-agent/core";
4
4
  import { cappedCeiling } from "../budget.js";
5
+ import { recordFailOpen } from "../observability/fail-open.js";
5
6
  import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
6
7
  import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
7
8
  import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
8
- import { gateScenarioRequest, mergeUserSkills, selectScenario } from "../capabilities/scenarios.js";
9
+ import { createScenarioGate, mergeUserSkills, selectScenario } from "../capabilities/scenarios.js";
9
10
  import { stampHandsLane, toolPolicyForHands } from "../capabilities/hands-lane.js";
10
11
  import { buildNoticeLaneFacts, noPersistentMemoryNotice, withNoPersistentMemoryNotice } from "../capabilities/memory-notice.js";
11
12
  import { applyLongtailDefer } from "../capabilities/tool-defer.js";
@@ -35,9 +36,11 @@ import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, norma
35
36
  import { isAbsoluteHostPath } from "@sema-agent/settings-schema";
36
37
  import { cwdHonored, deviceCwdHonored, effectiveHostWorkspace, inProcessSingleUserLane, parseAdditionalDirectories, satisfiedByProcessCwd, shellEnvMismatchCount } from "../task-cwd.js";
37
38
  import { assertRequestA2aUnlocked, resolveRequestA2a } from "../task-a2a.js";
38
- import { assertRequestMcpContentOrigin, assertRequestMcpIdentityHeaders, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
39
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, taskHooksHonored, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
40
- import { skillDenyInertEntryOf } from "../rule-entry-list.js";
39
+ import { assertRequestMcpEntries, assertRequestMcpUnlocked, resolveRequestMcp } from "../task-mcp.js";
40
+ import { storedToolAxesRereadM19 } from "../stored-request-tool-axes-m19.js";
41
+ import { storedHooksRereadM20 } from "../stored-request-hooks-m20.js";
42
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, REQUEST_SETTINGS_REFUSAL_PREFIX, acceptAppendSystemPrompt, applyTaskSettings, effectivePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, taskHooksHonored, providerDropsAppend, settingsPermissionBucketIssue, withPermissionMode } from "../task-settings.js";
43
+ import { inertToolListEntryOf } from "../rule-entry-list.js";
41
44
  import { TENANCY_WALL_CLOCK_BASE_SEC, enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
42
45
  import { redactSecrets } from "../trace/redact.js";
43
46
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
@@ -47,6 +50,7 @@ import { withUnattendedLeg } from "../unattended-permission-mode.js";
47
50
  import { memoryFaceMounted } from "../memory-scope.js";
48
51
  export function createResolveSpec(ctx) {
49
52
  const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
53
+ const scenarioGate = createScenarioGate({ config, scenarios, principalCaps, logger }, "enforce");
50
54
  assertGuardPatternsUsable(config);
51
55
  const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, {
52
56
  approvalSeat: durableEnabled ? "durable" : "absent",
@@ -69,9 +73,7 @@ export function createResolveSpec(ctx) {
69
73
  throw createCodedHttpError(501, "capability.checkpoint_store_required", seatRefusal);
70
74
  assertRequestMcpUnlocked(body.mcpServers, lockedKeys);
71
75
  if (opts?.leg === "fresh")
72
- assertRequestMcpContentOrigin(body.mcpServers);
73
- if (opts?.leg === "fresh")
74
- assertRequestMcpIdentityHeaders(body.mcpServers);
76
+ assertRequestMcpEntries(body.mcpServers);
75
77
  if (opts?.leg === "fresh") {
76
78
  const conflict = scratchpadCoveringTaskRoot({
77
79
  cwd: body.cwd,
@@ -111,9 +113,21 @@ export function createResolveSpec(ctx) {
111
113
  throw new HttpError(400, bucketIssue.message, { code: "request.field_invalid", extra: bucketIssue.fields });
112
114
  }
113
115
  {
114
- const skillDeny = skillDenyInertEntryOf(body);
115
- if (skillDeny !== undefined)
116
- throw new HttpError(400, skillDeny.message, { code: "request.field_invalid" });
116
+ const replay = opts?.leg === "resume";
117
+ const inertList = inertToolListEntryOf(body, replay ? ["deny"] : undefined);
118
+ if (inertList !== undefined)
119
+ throw new HttpError(400, inertList.message, { code: inertList.code, ...(inertList.extra !== undefined ? { extra: { ...inertList.extra } } : {}) });
120
+ if (replay) {
121
+ const stale = inertToolListEntryOf(body, ["allow"]);
122
+ if (stale !== undefined) {
123
+ logger.warn("stored_allow_list_inert_resumed", {
124
+ sessionId: auth?.sessionId ?? null,
125
+ layer: stale.layer,
126
+ sentence: stale.message,
127
+ note: "this resumed task's stored request carries an allow-list entry the engine reads as nothing (a retired name, a case twin, an unreadable spelling or a content form): the entry stays as written and grants nothing — the task ran that way before the upgrade, so it continues; fix the entry in a new submission to get what it was meant to allow",
128
+ });
129
+ }
130
+ }
117
131
  }
118
132
  assertRequestA2aUnlocked(body.a2aPeers, lockedKeys);
119
133
  const gated = await gateScenarioAndAppend(body, auth, opts);
@@ -136,7 +150,7 @@ export function createResolveSpec(ctx) {
136
150
  return stamped;
137
151
  };
138
152
  const gateScenarioAndAppend = async (body, auth, opts) => {
139
- const scenarioName = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario, Object.keys(scenarios), logger, auth?.principal);
153
+ const scenarioName = await scenarioGate(auth?.principal, body.scenario);
140
154
  gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), executionLaneOf(config.remoteExec?.provider), logger);
141
155
  const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal, auth?.sessionId !== undefined ? { sessionId: auth.sessionId } : {});
142
156
  const centerDecls = getCenterPrompts()?.declarations;
@@ -145,7 +159,7 @@ export function createResolveSpec(ctx) {
145
159
  : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
146
160
  const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
147
161
  const objective = typeof body.objective === "string" ? body.objective : "";
148
- const parsedSettings = parseTaskSettings(body.settings);
162
+ const parsedSettings = parseTaskSettings(body.settings, opts?.leg === "fresh" || !taskHooksHonored(config) ? {} : { storedHooksReread: storedHooksRereadM20(new Date().toISOString().slice(0, 10)) });
149
163
  if (opts?.leg === "fresh" && appendLessPack) {
150
164
  const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
151
165
  if (riderPresent || parsedSettings.settings?.outputStyle) {
@@ -155,6 +169,17 @@ export function createResolveSpec(ctx) {
155
169
  if (parsedSettings.deferred.length > 0) {
156
170
  logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
157
171
  }
172
+ if (parsedSettings.hooksReread !== undefined) {
173
+ logger.warn("hooks_read_as_before_upgrade", {
174
+ migration: "M20",
175
+ sessionId: auth?.sessionId ?? null,
176
+ stripped: parsedSettings.hooksReread.stripped,
177
+ note: "this replayed request was stored before the upgrade and its settings.hooks carries a shape the hooks reader now refuses (a key the reader does not know that hides a guard-event member, or a JSON `__proto__` event key); while one-time migration M20 is open the keys named in `stripped` are dropped — what the pre-upgrade reader dropped — and the rest goes through the same reader again, so the guard hooks keep running; once the window closes the same request has its whole settings.hooks left out",
178
+ });
179
+ }
180
+ if (parsedSettings.deferred.includes("hooks") && taskHooksHonored(config)) {
181
+ recordFailOpen("server.hooks.resume-reader-refused", `leg=${opts?.leg ?? "resume"}`);
182
+ }
158
183
  let attachmentNotice;
159
184
  {
160
185
  const reqAtt = body.attachmentIds;
@@ -285,6 +310,7 @@ export function createResolveSpec(ctx) {
285
310
  }
286
311
  return { taskHooks, additionalDirectories, additionalReadDirectories, wireCatalog, picked };
287
312
  };
313
+ const NO_ANCHOR_HINT = "no rewind anchor is recorded under that handle (a prompt that carried attachments records none when its attachment frames were interrupted or their count did not match)";
288
314
  const resolveHistoryAnchors = async (body, auth) => {
289
315
  let resumeAtEntryId;
290
316
  if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
@@ -294,7 +320,7 @@ export function createResolveSpec(ctx) {
294
320
  throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
295
321
  resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
296
322
  if (resumeAtEntryId === undefined)
297
- throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)", { code: "resume_at.unknown_event" });
323
+ throw new HttpError(404, `resumeAt: no such message in this session (resume_at.unknown_event) — ${NO_ANCHOR_HINT}; give an explicit resumeAt`, { code: "resume_at.unknown_event" });
298
324
  }
299
325
  let rewindFilesToEntryId;
300
326
  if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
@@ -304,7 +330,7 @@ export function createResolveSpec(ctx) {
304
330
  throw new HttpError(501, "rewind-files-to is not available on this worker (no session-store backend for the anchor map)");
305
331
  rewindFilesToEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.rewindFilesTo, auth.principal ?? null);
306
332
  if (rewindFilesToEntryId === undefined)
307
- throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)", { code: "rewind_files_to.unknown_event" });
333
+ throw new HttpError(404, `rewindFilesTo: no such message in this session (rewind_files_to.unknown_event) — ${NO_ANCHOR_HINT}; name another prompt`, { code: "rewind_files_to.unknown_event" });
308
334
  }
309
335
  const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId.toLowerCase() : undefined);
310
336
  const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
@@ -334,6 +360,7 @@ export function createResolveSpec(ctx) {
334
360
  sessionId: auth?.sessionId,
335
361
  principal: auth?.principal,
336
362
  images: body.images,
363
+ ...(body.userAttachments !== undefined ? { userAttachments: body.userAttachments } : {}),
337
364
  ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
338
365
  ...(() => {
339
366
  const ct = body.compaction?.clampTolerance;
@@ -456,7 +483,7 @@ export function createResolveSpec(ctx) {
456
483
  ...(() => { const v = toolNameListFromBody(body.userInvokedSkills); return v ? { userInvokedSkills: v } : {}; })(),
457
484
  ...(parsedSettings.settings?.skillListingBudgetFraction !== undefined ? { skillListingBudgetFraction: parsedSettings.settings.skillListingBudgetFraction } : {}),
458
485
  ...(parsedSettings.settings?.skillListingMaxDescChars !== undefined ? { skillListingMaxDescChars: parsedSettings.settings.skillListingMaxDescChars } : {}),
459
- mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined),
486
+ mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger, opts?.onNotice !== undefined ? { sessionId: auth?.sessionId, emit: opts.onNotice } : undefined, opts?.leg === "fresh" ? undefined : storedToolAxesRereadM19(new Date().toISOString().slice(0, 10))),
460
487
  a2a: resolveRequestA2a(a2aForScenario(config.a2aPeers, scenarioName), body.a2aPeers, config, logger),
461
488
  promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
462
489
  toolPolicy: toolPolicyForHands(hands, durableEnabled
@@ -596,7 +623,7 @@ export function createResolveSpec(ctx) {
596
623
  ...(e.detail !== undefined ? { extra: { writeExemptDirCoversRoot: e.detail } } : {}),
597
624
  });
598
625
  }
599
- throw new HttpError(422, `request settings cannot be applied as written: ${e.message}`);
626
+ throw new HttpError(422, `${REQUEST_SETTINGS_REFUSAL_PREFIX}${e.message}`);
600
627
  }
601
628
  }
602
629
  const notice = noPersistentMemoryNotice({
@@ -4,6 +4,9 @@ import { composeExecutionLane, FILE_SOURCED_LANES } from "./execution-lane.js";
4
4
  import { serviceConfigSourceKind } from "../config-provider.js";
5
5
  import { logConfigDirMigration, migrateLocalConfigDirectory } from "../config-d-migration.js";
6
6
  import { defaultModelRefusal, refusedNameMintedRefusal } from "../model-entry-refusal.js";
7
+ import { isRosterGateArmed } from "../config.js";
8
+ import { judgeServedModelRoute } from "../model-route-endpoint.js";
9
+ import { servedModelTableLeg } from "../model-select.js";
7
10
  export async function runStage02(ctx) {
8
11
  const { config, localRoot, logger, metrics } = ctx;
9
12
  const configSource = serviceConfigSourceKind(config);
@@ -11,10 +14,15 @@ export async function runStage02(ctx) {
11
14
  logConfigDirMigration(logger, await migrateLocalConfigDirectory(localRoot));
12
15
  await composeExecutionLane(config, { configSource, localRoot, logger, drivesFromFile: FILE_SOURCED_LANES });
13
16
  const webAdmin = await createWebAdminRuntime({ config, logger, metrics, localRoot });
14
- const noDefaultModel = defaultModelRefusal(config, "env");
17
+ const servedLeg = servedModelTableLeg(config);
18
+ const noDefaultModel = defaultModelRefusal(config, servedLeg);
15
19
  if (noDefaultModel !== undefined) {
16
20
  throw new Error(noDefaultModel);
17
21
  }
22
+ const routeVerdict = judgeServedModelRoute(config, { envTableIsPlaceholder: isRosterGateArmed(config.webAdmin) });
23
+ if (routeVerdict !== undefined) {
24
+ throw new Error(routeVerdict.reason);
25
+ }
18
26
  const mintedRefused = refusedNameMintedRefusal(config);
19
27
  if (mintedRefused !== undefined) {
20
28
  throw new Error(mintedRefused);
@@ -56,7 +56,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
56
56
  resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
57
57
  runStore: import("../plugins/store-backend.js").RunStore | undefined;
58
58
  scenarioDetails: Record<string, import("../capabilities/scenarios.js").ScenarioDetail>;
59
- scenarios: Record<string, import("../index.js").Scenario>;
59
+ scenarios: Record<string, import("../capabilities/scenarios.js").Scenario>;
60
60
  selectEnvTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
61
61
  taskLimitCaps: {
62
62
  timeoutSec: number | undefined;
@@ -19,6 +19,7 @@ import { mintEngineSignal } from "../fleet/engine-signal.js";
19
19
  import { createServingLeaseClient } from "../fleet/serving-lease.js";
20
20
  import { fleetFenceLaneOf } from "../config.js";
21
21
  import { createResolveSpec } from "./resolve-spec.js";
22
+ import { createScenarioGate } from "../capabilities/scenarios.js";
22
23
  import { executionLaneOf } from "../execution-lane-caps.js";
23
24
  import { startFleetReconciler } from "../fleet/fleet-reconciler.js";
24
25
  import { createSideQueryLane } from "./side-query-lane.js";
@@ -210,7 +211,8 @@ export async function runStage10(ctx) {
210
211
  });
211
212
  const runnerFor = createHandsRunnerSelector({ full: runner, handsless: handslessRunner });
212
213
  const sideQuery = createSideQueryLane({ brain, config, getKeyResolver: () => webAdmin.getKeyResolver() });
213
- const server = createHttpServer({ runner, runnerFor, sideQuery, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
214
+ const scenarioGate = createScenarioGate({ config, scenarios, principalCaps, logger }, "peek");
215
+ const server = createHttpServer({ runner, runnerFor, sideQuery, scenarioGate, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
214
216
  bindServerLateSeats({ config, logger, server });
215
217
  const fleetReconcile = startFleetReconciler({
216
218
  bus: fleetBus,
@@ -282,8 +284,11 @@ export async function runStage10(ctx) {
282
284
  const failoverFirstByte = buildFailoverFirstByteNotice(config);
283
285
  if (failoverFirstByte)
284
286
  logger.warn("failover_first_byte_lane", failoverFirstByte);
287
+ const bound = server.address();
288
+ if (bound === null || typeof bound === "string")
289
+ throw new Error(`http server listening without a TCP address (server.address() = ${JSON.stringify(bound)})`);
285
290
  logger.info("listening", {
286
- port: config.port,
291
+ port: bound.port,
287
292
  bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
288
293
  ...(bindHost === "127.0.0.1" && !config.bindHost
289
294
  ? { bindHostNote: "auto-narrowed to loopback: write face is unauthenticated (ALLOW_UNAUTHED_WRITES with no service token). Set BIND_HOST explicitly to override." }
@@ -1,7 +1,8 @@
1
1
  import { Type } from "typebox";
2
+ import { NOW_TOOL_NAME } from "./host-tool-names.js";
2
3
  export function nowTool() {
3
4
  return {
4
- name: "Now",
5
+ name: NOW_TOOL_NAME,
5
6
  description: "Return the current date-time as an ISO-8601 UTC string. Use when the task involves dates/deadlines/'today'.",
6
7
  effect: "read",
7
8
  parameters: Type.Object({}),
@@ -54,6 +54,7 @@ export declare const COMPOSITE_CAPABILITY_MEMBERS: {
54
54
  readonly workflowsGate: readonly ["engineCan", "denial"];
55
55
  readonly sealedKey: readonly ["alg", "publicKeyId", "publicKey"];
56
56
  readonly workspaceSync: readonly ["persist", "keep", "timeoutMs", "cache"];
57
+ readonly userAttachments: readonly ["entries", "kinds", "supplies", "limits"];
57
58
  };
58
59
  type CompositeTable = typeof COMPOSITE_CAPABILITY_MEMBERS;
59
60
  /** 值的**具名成员对象臂**(标量 / 字面词 / `false` / `null` / `undefined` / 数组 / 函数臂剥掉)。 */
@@ -15,6 +15,7 @@ export const COMPOSITE_CAPABILITY_MEMBERS = {
15
15
  workflowsGate: ["engineCan", "denial"],
16
16
  sealedKey: ["alg", "publicKeyId", "publicKey"],
17
17
  workspaceSync: ["persist", "keep", "timeoutMs", "cache"],
18
+ userAttachments: ["entries", "kinds", "supplies", "limits"],
18
19
  };
19
20
  export function gateCompositeMembers(body) {
20
21
  return body;
@@ -1,5 +1,5 @@
1
1
  import { handBandEffects, judgeRuleToolName, combinePolicies, createAllowDenyPolicy } from "@sema-agent/core";
2
- import { NO_DECLARED_TOOL_NAMES } from "../namespaced-rule-name.js";
2
+ import { TOOL_NAME_ROSTER } from "../namespaced-rule-name.js";
3
3
  export function withoutHands(deps) {
4
4
  const { executionEnvFactory: _handsDropped, lspManager: _lspDropped, ...rest } = deps;
5
5
  return rest;
@@ -20,7 +20,7 @@ export const HANDS_BAND_TOOL_NAMES = Object.freeze([
20
20
  "EnterWorktree",
21
21
  "ExitWorktree",
22
22
  "ListAgents",
23
- ].filter((n, i, all) => judgeRuleToolName(n, NO_DECLARED_TOOL_NAMES).kind === "current" && all.indexOf(n) === i));
23
+ ].filter((n, i, all) => judgeRuleToolName(n, TOOL_NAME_ROSTER).kind === "current" && all.indexOf(n) === i));
24
24
  export const HANDS_DUAL_USE_TOOL_NAMES = Object.freeze(["TaskOutput", "TaskStop", "SendMessage", "AgentTranscript", "ListAgents"]);
25
25
  export const HANDS_EXCLUSIVE_TOOL_NAMES = Object.freeze(HANDS_BAND_TOOL_NAMES.filter((n) => !HANDS_DUAL_USE_TOOL_NAMES.includes(n)));
26
26
  export function handsDenyPolicy() {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * **本仓自己铸给模型的工具的名字** —— 唯一的名字源(叶模块,零依赖)。铸它们的地方(`builtin-tools.ts` / `send-user-file-tool.ts` / `select-environment-tool.ts` / `repo-tools.ts`)
3
+ * 与读它们的地方(工具名清单的受理判:`rule-entry-list.ts` 的名册;场景详情的工具名单;长尾延迟披露集)都从这里取,不手抄。
4
+ *
5
+ * 为什么受理口要知道它们:请求的 `allowTools` / `excludeTools` / 子代的 `allowTools` / `denyTools` 里把 `SendUserFile` 写成 `SENDUSERFILE`,引擎只在开跑时按这一 run 真挂着的名册告运维;
6
+ * 受理口读不到那份名册,读得到的是「引擎目录 ∪ 本仓自铸的这几只」—— 不把这几只放进名册,它们的孪生就是受理口漏掉的同一类静默失效。
7
+ * 宿主之外的调用方工具、MCP / A2A 对端的工具不在其中(前者本仓不挂,后者是协议名,由协议段判)。
8
+ */
9
+ export declare const NOW_TOOL_NAME = "Now";
10
+ export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
11
+ export declare const SELECT_ENVIRONMENT_TOOL_NAME = "SelectEnvironment";
12
+ export declare const REPO_TREE_TOOL_NAME = "repo_tree";
13
+ export declare const REPO_READ_FILE_TOOL_NAME = "repo_read_file";
14
+ export declare const REPO_PULL_DIFF_TOOL_NAME = "repo_pull_diff";
15
+ /** 本仓自铸的全部工具名(受理口工具名清单的名册成员;加一只自铸工具 ⇒ 这里加一行)。 */
16
+ export declare const HOST_TOOL_NAMES: ReadonlySet<string>;
17
+ //# sourceMappingURL=host-tool-names.d.ts.map
@@ -0,0 +1,15 @@
1
+ export const NOW_TOOL_NAME = "Now";
2
+ export const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
3
+ export const SELECT_ENVIRONMENT_TOOL_NAME = "SelectEnvironment";
4
+ export const REPO_TREE_TOOL_NAME = "repo_tree";
5
+ export const REPO_READ_FILE_TOOL_NAME = "repo_read_file";
6
+ export const REPO_PULL_DIFF_TOOL_NAME = "repo_pull_diff";
7
+ export const HOST_TOOL_NAMES = new Set([
8
+ NOW_TOOL_NAME,
9
+ SEND_USER_FILE_TOOL_NAME,
10
+ SELECT_ENVIRONMENT_TOOL_NAME,
11
+ REPO_TREE_TOOL_NAME,
12
+ REPO_READ_FILE_TOOL_NAME,
13
+ REPO_PULL_DIFF_TOOL_NAME,
14
+ ]);
15
+ //# sourceMappingURL=host-tool-names.js.map
@@ -1,10 +1,10 @@
1
- import { probeMcpServers } from "@sema-agent/core";
1
+ import { probeMcpServers, readMcpToolAxes } from "@sema-agent/core";
2
2
  import { createHash } from "node:crypto";
3
3
  import { createFixedWindowRateLimiter } from "../device-enrollment.js";
4
4
  import { canonicalJson } from "../canonical-json.js";
5
5
  import { echoKeyName, keyClosureClause, scanKeyClosure } from "../request-key-closure.js";
6
6
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "../mcp-content-origin.js";
7
- import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, requestEntryIdentityRefusal, validateRequestMcp, } from "../task-mcp.js";
7
+ import { HTTP_MCP_TRANSPORT_KEYS, MAX_REQUEST_MCP_SERVERS, MCP_ENTRY_EMPTY_ARRAY_KEYS, MCP_ENTRY_EMPTY_RECORD_KEYS, MCP_SERVER_ENTRY_KEYS, MCP_TRANSPORT_EMPTY_ARRAY_KEYS, MCP_TRANSPORT_EMPTY_RECORD_KEYS, STDIO_MCP_TRANSPORT_KEYS, requestEntryIdentityRefusal, requestEntryToolAxesRefusal, validateRequestMcp, } from "../task-mcp.js";
8
8
  import { toCoreMcpServerSpec } from "../mcp-assertion-header.js";
9
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
10
10
  import { projectWiringManifestMcpRows } from "../trace/project.js";
@@ -53,7 +53,7 @@ function entryKeyIssue(entry, at) {
53
53
  scanKeyClosure(t, `${at}.transport.`, new Set(keys), NO_TOLERATED_KEYS, buckets);
54
54
  }
55
55
  if (buckets.unknown.length === 0) {
56
- const refusal = requestEntryIdentityRefusal(entry);
56
+ const refusal = requestEntryIdentityRefusal(entry, at);
57
57
  if (refusal === undefined)
58
58
  return null;
59
59
  return refusal.errorCode === "request.body_shape" ? shapeRefusal(refusal.message) : fieldRefusal(refusal.message);
@@ -64,6 +64,9 @@ function entryKeyIssue(entry, at) {
64
64
  `Accepted per entry: ${[...MCP_SERVER_ENTRY_KEYS].sort().join(",")} (transport.kind "stdio": ${[...STDIO_MCP_TRANSPORT_KEYS].sort().join(",")}; "http": ${[...HTTP_MCP_TRANSPORT_KEYS].sort().join(",")})`, buckets.unknown);
65
65
  }
66
66
  function entryValueIssue(entry, at) {
67
+ const axesRefusal = requestEntryToolAxesRefusal(entry, at);
68
+ if (axesRefusal !== undefined)
69
+ return fieldRefusal(axesRefusal.message);
67
70
  const read = validateRequestMcp([entry]);
68
71
  if (read === null)
69
72
  return fieldRefusal(`${at} is not a readable MCP declaration`);
@@ -71,7 +74,7 @@ function entryValueIssue(entry, at) {
71
74
  if (dropped !== undefined || read.ok.length !== 1) {
72
75
  const field = dropped?.field ?? "entry";
73
76
  return fieldRefusal(`${at}.${field} is not a usable value${urlEchoFor(entry, field)} — this endpoint refuses the whole list rather than dialing a silently shortened one` +
74
- ` (a task submission would instead drop just this declaration and mount its siblings; a probe answering "can I use these" must not quietly answer about fewer)`);
77
+ ` (a probe answering "can I use these" must not quietly answer about fewer)`);
75
78
  }
76
79
  if (read.badContentOrigin.length > 0) {
77
80
  return fieldRefusal(`${at}.contentOrigin must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(entry.contentOrigin)} (omit the key for the default: this server's tools classify as external content)`);
@@ -82,12 +85,22 @@ function entryValueIssue(entry, at) {
82
85
  function declarationDowngradeIssue(entry, spec, at) {
83
86
  if (spec === undefined)
84
87
  return fieldRefusal(`${at} is not a readable MCP declaration`);
85
- const diff = jsonPathDiff(stripEquivalentEmpties(entry), stripEquivalentEmpties(spec), at);
88
+ const diff = jsonPathDiff(stripEquivalentEmpties(declaredAsJudged(entry)), stripEquivalentEmpties(spec), at);
86
89
  if (diff.length === 0)
87
90
  return null;
88
91
  diff.sort();
89
92
  return fieldRefusal(`${keyClosureClause(diff, "unusable value(s)")} — this deployment's declaration reader could not use these exactly as written, and a probe must not dial a configuration that differs from the one you declared` +
90
- " (a task submission would instead keep the server and silently drop just these, which is why the two faces disagree here on purpose; this endpoint refuses, because the reading would then describe a server you did not ask about)");
93
+ " (this endpoint refuses, because the reading would then describe a server you did not ask about)");
94
+ }
95
+ function declaredAsJudged(entry) {
96
+ const declared = entry;
97
+ if (!("toolAxes" in declared))
98
+ return declared;
99
+ const read = readMcpToolAxes(declared.toolAxes);
100
+ if (!read.ok)
101
+ return declared;
102
+ const { toolAxes: _declaredAxes, ...rest } = declared;
103
+ return read.toolAxes === undefined ? rest : { ...rest, toolAxes: read.toolAxes };
91
104
  }
92
105
  function stripEquivalentEmpties(entry) {
93
106
  const emptyRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
@@ -1,5 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { z } from "zod";
3
+ import { REPO_PULL_DIFF_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_TREE_TOOL_NAME } from "./host-tool-names.js";
3
4
  export function parseRepo(input) {
4
5
  const s = input.trim().replace(/\.git$/, "");
5
6
  const m = /^[a-z]+:\/\/[^/]+\/(.+)$/i.exec(s);
@@ -160,7 +161,7 @@ export function repoToolsFor(client, coords) {
160
161
  const ref = (r) => (r ? Promise.resolve(r) : client.defaultBranch(coords));
161
162
  return [
162
163
  {
163
- name: "repo_tree",
164
+ name: REPO_TREE_TOOL_NAME,
164
165
  description: "List file paths in the repository (recursive). Use this first to understand structure, then read specific files.",
165
166
  effect: "read",
166
167
  parameters: Type.Object({ ref: Type.Optional(Type.String({ description: "branch/tag/sha; default = repo default branch" })) }),
@@ -171,7 +172,7 @@ export function repoToolsFor(client, coords) {
171
172
  },
172
173
  },
173
174
  {
174
- name: "repo_read_file",
175
+ name: REPO_READ_FILE_TOOL_NAME,
175
176
  description: "Read one file's text from the repository. Read only the files you need (just-in-time).",
176
177
  effect: "read",
177
178
  parameters: Type.Object({ path: Type.String(), ref: Type.Optional(Type.String()) }),
@@ -181,7 +182,7 @@ export function repoToolsFor(client, coords) {
181
182
  },
182
183
  },
183
184
  {
184
- name: "repo_pull_diff",
185
+ name: REPO_PULL_DIFF_TOOL_NAME,
185
186
  description: "Get the unified diff of a pull request by its number (for reviewing a PR's changes).",
186
187
  effect: "read",
187
188
  parameters: Type.Object({ index: Type.Integer({ description: "the PR number" }) }),
@@ -295,4 +295,38 @@ export declare function danglingScenarioRefusal(name: string, source: ScenarioNa
295
295
  * ({@link danglingScenarioRefusal})。`principal` 只进那一行运维日志,不参与判定。
296
296
  */
297
297
  export declare function gateScenarioRequest(ruling: ScenarioRuling | undefined, bodyScenario: unknown, defaultScenario: string, known: readonly string[], logger?: Logger, principal?: string): string;
298
+ /**
299
+ * 一条请求的场景名过门 —— {@link gateScenarioRequest} 的**唯一一处实参装配**。
300
+ *
301
+ * 门的六个实参各有来源:中心裁定(`principalCaps.scenarioRuling`,按 principal)、请求点名的名字、部署缺省场景、**在册词表**
302
+ * (`Object.keys(scenarios)` —— 场景表活引用,不抄快照)、日志与 principal。「此主体在此场景能看见什么」这类问题有不止一个读口
303
+ * (任务提交腿 `resolveSpec`、`run-local`、`GET /v1/capabilities/mcp` 的状态面、`GET /v1/sessions/:id/mcp` / `…/a2a` 两张会话面板),
304
+ * 每个读口自己拼一遍这六个实参 = 第二只门:两口对「在册」「放行」「缺席取谁」各判各的,读口就开始对不上 run。
305
+ * 所以装配只在这里,读口持有的是**绑好的门**;凡回答这类问题的口,场景名都由门给 —— 点名与缺席都问。
306
+ *
307
+ * 契约:`(principal, requested) => 在册的场景名`(返回值一定在册;被拒 ⇒ 抛 {@link gateScenarioRequest} 的那几只 typed 错,
308
+ * 读口不改写)。`requested` 非串(缺席)⇒ 门走指派链(ruling 胜者,否则部署缺省),与 run 时同一读法。
309
+ * `principalCaps` 缺席(无中心 / 干跑 / 嵌入方 / `run-local`)⇒ 无 ruling,门退回「请求名或部署缺省」那条现状链。
310
+ */
311
+ export interface ScenarioGateSources {
312
+ readonly config: {
313
+ readonly defaultScenario: string;
314
+ };
315
+ /** 场景表(活引用):词表按调用时的自有键取,与表同生同灭。 */
316
+ readonly scenarios: Readonly<Record<string, unknown>>;
317
+ readonly principalCaps: {
318
+ scenarioRuling(principal: string | undefined): Promise<ScenarioRuling | undefined>;
319
+ peekScenarioRuling(principal: string | undefined): Promise<ScenarioRuling | undefined>;
320
+ } | undefined;
321
+ readonly logger?: Logger;
322
+ }
323
+ export type ScenarioGate = (principal: string | undefined, requested: unknown) => Promise<string>;
324
+ /**
325
+ * 门问中心裁定走哪条腿(闭集;新增一词 ⇒ 下面的穷尽 `switch` 编译红)—— 与 `resolveRuntimeCaps` / `peekRuntimeCaps` 是同一对:
326
+ * · `enforce` —— 任务提交腿:中心裁定走执法腿,center 硬失败铸下的 fail-closed 降级项两腿都认;
327
+ * · `peek` —— 读面(`GET /v1/capabilities/mcp` 状态面、`GET /v1/sessions/:id/mcp` / `…/a2a` 两张会话面板):只读腿,同一只缓存 / 同一条抓取 / 同一份 fail-open 判决,但硬失败铸的降级项只对读面自己算数,
328
+ * 不替这个主体的真提交留下 `{allowWorkflows:false, allowFork:false, autoMode:false}` 的短窗 deny(读面不许成为别人被拒的原因)。
329
+ */
330
+ export type ScenarioGateLeg = "enforce" | "peek";
331
+ export declare function createScenarioGate(sources: ScenarioGateSources, leg: ScenarioGateLeg): ScenarioGate;
298
332
  //# sourceMappingURL=scenarios.d.ts.map
@@ -2,6 +2,7 @@ import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, WEB_FETCH
2
2
  import { HttpError, createCodedHttpError } from "../security.js";
3
3
  import { createSubagentExtraTools, extraToolsErrorFields, nowTool } from "./builtin-tools.js";
4
4
  import { parseRepo, repoToolsFor } from "./repo-tools.js";
5
+ import { NOW_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_TREE_TOOL_NAME } from "./host-tool-names.js";
5
6
  import { stablePrompt } from "./prompt.js";
6
7
  import { createWebSearchBackend, judgeRequestWebSearch } from "../plugins/web-search.js";
7
8
  import { skillsForScenario } from "./skills.js";
@@ -119,16 +120,16 @@ export function builtinScenarioDetails(scenarios, deps) {
119
120
  return fallback;
120
121
  }
121
122
  };
122
- const REPO_TOOLS = ["repo_tree", "repo_read_file", "repo_pull_diff", "Now"];
123
+ const REPO_TOOLS = [REPO_TREE_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, NOW_TOOL_NAME];
123
124
  const mk = (name, summary, toolset, tools, promptSummary) => ({
124
125
  name, source: "builtin", builtin: true, summary, toolset, tools, promptSummary, enabled: true,
125
126
  ...availabilityFields(deps, toolset),
126
127
  });
127
128
  const details = {
128
- code: mk("code", "编码主场景([891] 出厂缺省;CC 编码 persona 蒸馏版,default 全量工具面;终验旋钮独立)", "full-body", probe("code", {}, undefined, ["Now"]), deps.brandIdentity
129
+ code: mk("code", "编码主场景([891] 出厂缺省;CC 编码 persona 蒸馏版,default 全量工具面;终验旋钮独立)", "full-body", probe("code", {}, undefined, [NOW_TOOL_NAME]), deps.brandIdentity
129
130
  ? "Sema 产品身份 + core 编码完全体提示词(CODE_AGENT persona+自治自检;CC 血统对表)"
130
131
  : "core 编码完全体提示词(CODE_AGENT persona+自治自检;CC 血统对表)"),
131
- default: mk("default", "通用/助理场景(域中性 persona,CC agent loop 全量工具面;[891] 起非出厂缺省,显式可选)", "full-body", probe("default", {}, undefined, ["Now"]), deps.brandIdentity ? "Sema 产品身份 + core 默认宪法(结构层由引擎合成)" : "core 默认宪法(中性,无场景附加 persona)"),
132
+ default: mk("default", "通用/助理场景(域中性 persona,CC agent loop 全量工具面;[891] 起非出厂缺省,显式可选)", "full-body", probe("default", {}, undefined, [NOW_TOOL_NAME]), deps.brandIdentity ? "Sema 产品身份 + core 默认宪法(结构层由引擎合成)" : "core 默认宪法(中性,无场景附加 persona)"),
132
133
  "code-review": mk("code-review", "代码评审(clone-free 只读仓库工具)", "repo-readonly", probe("code-review", { repo: "probe/probe" }, undefined, REPO_TOOLS), "评审员提示词(产出契约)"),
133
134
  scan: mk("scan", "中性只读仓库扫描(输出契约由 objective+skills 决定)", "repo-readonly", probe("scan", { repo: "probe/probe" }, undefined, REPO_TOOLS), "无场景附加提示词(objective+center skills 拥有输出契约)"),
134
135
  };
@@ -149,7 +150,7 @@ export function centerScenarioDetails(specs, builtinNames, deps) {
149
150
  builtin: builtinNames.includes(spec.name),
150
151
  summary: "",
151
152
  toolset: spec.toolset,
152
- tools: spec.toolset === "repo-readonly" ? ["repo_tree", "repo_read_file", "repo_pull_diff", "Now"] : ["Now"],
153
+ tools: spec.toolset === "repo-readonly" ? [REPO_TREE_TOOL_NAME, REPO_READ_FILE_TOOL_NAME, REPO_PULL_DIFF_TOOL_NAME, NOW_TOOL_NAME] : [NOW_TOOL_NAME],
153
154
  promptSummary: "",
154
155
  enabled: true,
155
156
  ...availabilityFields(deps, spec.toolset),
@@ -297,4 +298,19 @@ export function gateScenarioRequest(ruling, bodyScenario, defaultScenario, known
297
298
  }
298
299
  return name;
299
300
  }
301
+ export function createScenarioGate(sources, leg) {
302
+ const rulingOf = (principal) => {
303
+ switch (leg) {
304
+ case "enforce":
305
+ return sources.principalCaps?.scenarioRuling(principal);
306
+ case "peek":
307
+ return sources.principalCaps?.peekScenarioRuling(principal);
308
+ default: {
309
+ const unhandled = leg;
310
+ return unhandled;
311
+ }
312
+ }
313
+ };
314
+ return async (principal, requested) => gateScenarioRequest(await rulingOf(principal), requested, sources.config.defaultScenario, Object.keys(sources.scenarios), sources.logger, principal);
315
+ }
300
316
  //# sourceMappingURL=scenarios.js.map
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { SELECT_ENVIRONMENT_TOOL_NAME } from "./host-tool-names.js";
2
3
  import { BoundedSessionMap } from "../bounded-session-map.js";
3
4
  import { resolveSandboxImageRef } from "../per-task-image.js";
4
5
  export class SessionEnvironmentSelection {
@@ -22,7 +23,7 @@ async function availableProfiles(catalog, viewer) {
22
23
  }
23
24
  export function selectEnvironmentTool(deps) {
24
25
  return {
25
- name: "SelectEnvironment",
26
+ name: SELECT_ENVIRONMENT_TOOL_NAME,
26
27
  description: "Switch this session's sandbox environment (image profile) or discover available ones. " +
27
28
  "Call with no arguments to list available profiles and their capabilities. " +
28
29
  "Call with `profile` (optionally `capabilitiesNeeded`) to select one. " +
@@ -43,7 +43,6 @@ export declare function sendUserFileTool(deps: {
43
43
  }) => Promise<IssuedFileLink>;
44
44
  emitter: SendUserFileEmitter;
45
45
  }): ToolSpec;
46
- export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
47
46
  /**
48
47
  * The deployment's group for the delegated-child injection (`createSubagentExtraTools` in `builtin-tools.ts` composes it with
49
48
  * `Now` into the delegation tool's `extraTools` factory AND its `extraToolNames` roster — one list, read twice): SendUserFile,
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { Type } from "typebox";
3
3
  import { builtinAgentDefinitions } from "@sema-agent/core";
4
+ import { SEND_USER_FILE_TOOL_NAME } from "./host-tool-names.js";
4
5
  export class SendUserFileEmitter {
5
6
  als = new AsyncLocalStorage();
6
7
  runWithContext(ctx, fn) {
@@ -19,7 +20,7 @@ export const SEND_USER_FILE_MAX_BYTES = 100 * 1024 * 1024;
19
20
  export const SEND_USER_FILE_MAX_CAPTION = 500;
20
21
  export function sendUserFileTool(deps) {
21
22
  return {
22
- name: "SendUserFile",
23
+ name: SEND_USER_FILE_TOOL_NAME,
23
24
  description: "Send file(s) to the user — surfaced as downloadable/inline cards in their pane. " +
24
25
  "Use when the file itself is the deliverable (generated charts, reports, screenshots, build artifacts) " +
25
26
  "and you want it surfaced, not just mentioned. Files must already exist on local disk (this tool sends " +
@@ -91,7 +92,6 @@ export function sendUserFileTool(deps) {
91
92
  },
92
93
  };
93
94
  }
94
- export const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
95
95
  const READONLY_BUILTIN_AGENT_TYPES = new Set(builtinAgentDefinitions().map((d) => d.name));
96
96
  export function subagentSendUserFileExtraTools(deps) {
97
97
  return { tools: [deps.spec], forSpawn: (ctx) => !(ctx.agentType !== undefined && READONLY_BUILTIN_AGENT_TYPES.has(ctx.agentType)) };
@@ -107,7 +107,7 @@ export function skillsForScenario(loaded, scenario) {
107
107
  return loaded.filter((k) => k.scenarios.length === 0 || k.scenarios.includes(scenario)).map((k) => k.spec);
108
108
  }
109
109
  export function parseFrontmatter(raw) {
110
- const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
110
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*\r?(?:\n|$)([\s\S]*)$/.exec(raw);
111
111
  if (!m)
112
112
  return { meta: {}, body: raw };
113
113
  const meta = {};
@@ -1,4 +1,5 @@
1
- export const LONGTAIL_DEFER_TOOLS = new Set(["SelectEnvironment"]);
1
+ import { SELECT_ENVIRONMENT_TOOL_NAME } from "./host-tool-names.js";
2
+ export const LONGTAIL_DEFER_TOOLS = new Set([SELECT_ENVIRONMENT_TOOL_NAME]);
2
3
  export function applyLongtailDefer(tools, enabled) {
3
4
  if (!enabled)
4
5
  return tools;