@sema-agent/server 7.128.0 → 7.129.1

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 (105) hide show
  1. package/MIGRATION.md +140 -0
  2. package/USAGE.md +114 -18
  3. package/dist/approval-card.d.ts +53 -26
  4. package/dist/approval-card.js +40 -11
  5. package/dist/approval-crash-convergence.js +4 -4
  6. package/dist/bake-runner/main.js +2 -1
  7. package/dist/boot/execution-env.js +1 -0
  8. package/dist/boot/memory-consolidation.d.ts +46 -16
  9. package/dist/boot/memory-consolidation.js +15 -8
  10. package/dist/boot/side-query-lane.d.ts +7 -20
  11. package/dist/boot/side-query-lane.js +0 -24
  12. package/dist/boot/stage-01-config.js +3 -0
  13. package/dist/boot/stage-06-runners.js +1 -1
  14. package/dist/boot/stores.js +2 -2
  15. package/dist/boot/web-admin.js +17 -10
  16. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  17. package/dist/boot/webfetch-summarize-lane.js +2 -3
  18. package/dist/brain.js +6 -9
  19. package/dist/capabilities/builtin-tools.d.ts +2 -2
  20. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  21. package/dist/capabilities/engine-capabilities.js +12 -0
  22. package/dist/capability-facts.d.ts +3 -1
  23. package/dist/capability-facts.js +1 -0
  24. package/dist/config-catalog.js +5 -1
  25. package/dist/config-types.d.ts +7 -3
  26. package/dist/config.js +7 -1
  27. package/dist/egress-redirect.d.ts +12 -0
  28. package/dist/egress-redirect.js +7 -0
  29. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  30. package/dist/entry-mode-clamp-notice.js +13 -0
  31. package/dist/exec-bounds.d.ts +69 -0
  32. package/dist/exec-bounds.js +37 -0
  33. package/dist/fleet/fleet-bus.d.ts +7 -0
  34. package/dist/fleet/fleet-bus.js +1 -0
  35. package/dist/hooks/hook-llm.js +6 -2
  36. package/dist/host-lsp-manager.d.ts +3 -3
  37. package/dist/http/admission.js +5 -2
  38. package/dist/http/resume-legs.js +46 -27
  39. package/dist/http/route-ctx.d.ts +1 -1
  40. package/dist/http/routes/approvals-assistant.js +6 -4
  41. package/dist/http/routes/capabilities.js +1 -0
  42. package/dist/http/routes/session-sync.js +3 -4
  43. package/dist/http/routes/sessions.js +4 -17
  44. package/dist/http/send.d.ts +1 -1
  45. package/dist/http/send.js +4 -3
  46. package/dist/http/wire-types.d.ts +3 -2
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/observability/fail-open.d.ts +7 -3
  50. package/dist/observability/fail-open.js +7 -3
  51. package/dist/observability/secret-env-scrub.d.ts +37 -3
  52. package/dist/observability/secret-env-scrub.js +2 -2
  53. package/dist/parked-decide.js +5 -2
  54. package/dist/plugins/memory-embedder.js +5 -0
  55. package/dist/plugins/one-time-migrations.d.ts +180 -9
  56. package/dist/plugins/one-time-migrations.js +315 -3
  57. package/dist/plugins/pg-pool.js +3 -1
  58. package/dist/plugins/remote-env-e2b.d.ts +31 -9
  59. package/dist/plugins/remote-env-e2b.js +77 -26
  60. package/dist/plugins/remote-env-host.d.ts +17 -20
  61. package/dist/plugins/remote-env-host.js +20 -30
  62. package/dist/plugins/remote-env-k8s.d.ts +12 -4
  63. package/dist/plugins/remote-env-k8s.js +22 -31
  64. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  65. package/dist/plugins/session-policy-store-sql.js +17 -20
  66. package/dist/plugins/session-rules-column.d.ts +16 -0
  67. package/dist/plugins/session-rules-column.js +6 -0
  68. package/dist/plugins/tidb-pool.js +4 -1
  69. package/dist/read-face-posture.d.ts +2 -1
  70. package/dist/request-value-closure.js +1 -1
  71. package/dist/retired-card-words.d.ts +17 -0
  72. package/dist/retired-card-words.js +49 -0
  73. package/dist/rule-entry-list.js +2 -0
  74. package/dist/run-local.js +10 -3
  75. package/dist/server-secret-env.d.ts +77 -85
  76. package/dist/server-secret-env.js +40 -26
  77. package/dist/session-policy-wire.d.ts +61 -124
  78. package/dist/session-policy-wire.js +59 -121
  79. package/dist/session-sync.d.ts +8 -6
  80. package/dist/session-sync.js +6 -5
  81. package/dist/spec-fields.d.ts +3 -2
  82. package/dist/standing-answer-grant.d.ts +17 -9
  83. package/dist/standing-answer-grant.js +11 -8
  84. package/dist/task-settings.d.ts +6 -1
  85. package/dist/task-settings.js +1 -1
  86. package/dist/tool-approval.d.ts +14 -7
  87. package/dist/tool-approval.js +23 -18
  88. package/dist/trace/core-keyset-guard.d.ts +3 -3
  89. package/dist/trace/engine-notice-wire.d.ts +1 -1
  90. package/dist/trace/engine-notice-wire.js +1 -0
  91. package/dist/trace/project.d.ts +7 -4
  92. package/dist/trace/project.js +6 -75
  93. package/dist/trace/tool-output-media.d.ts +2 -1
  94. package/dist/trace/tool-output-media.js +3 -25
  95. package/dist/web-admin-client/apply-effective.js +28 -11
  96. package/dist/web-admin-client/facade.d.ts +1 -1
  97. package/dist/web-admin-client/facade.js +1 -1
  98. package/dist/web-admin-client/read-face.d.ts +73 -20
  99. package/dist/web-admin-client/read-face.js +76 -36
  100. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  101. package/dist/web-admin-client/skills-mcp.js +1 -1
  102. package/dist/web-admin-client/types.d.ts +3 -3
  103. package/package.json +3 -3
  104. package/dist/mcp-tool-axes.d.ts +0 -62
  105. package/dist/mcp-tool-axes.js +0 -47
@@ -18,7 +18,7 @@ import { PgRosterStore, TiDBRosterStore, ensurePgRosterSchema, ensureTiDBRosterS
18
18
  import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store.js";
19
19
  import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
20
20
  import { createSessionStore } from "../plugins/session-store.js";
21
- import { logOneTimeMigrations } from "../plugins/one-time-migrations.js";
21
+ import { runAndLogOneTimeMigrations } from "../plugins/one-time-migrations.js";
22
22
  import { createTaskListLane } from "./task-list-lane.js";
23
23
  import { ensurePgTaskListSchema, ensureTiDBTaskListSchema } from "../plugins/task-list-store-sql.js";
24
24
  import { assertDeviceAuditRebindEventSchema, ensurePgDeviceSchema, ensureTiDBDeviceSchema } from "../plugins/device-store-sql.js";
@@ -100,7 +100,7 @@ export async function openStores(ctx) {
100
100
  await assertFileHistorySchema(async (sql) => ({ rows: (await mysqlPool.query(sql))[0] }), "tidb");
101
101
  }
102
102
  if (backend)
103
- logOneTimeMigrations(logger, backend.kind, await backend.runOneTimeMigrations());
103
+ await runAndLogOneTimeMigrations(logger, backend);
104
104
  if ((process.env.REWIND_SNAPSHOT_MAX_MB ?? "") !== "") {
105
105
  logger.warn("rewind_snapshot_max_mb_retired", {
106
106
  note: "REWIND_SNAPSHOT_MAX_MB no longer has an effect: the whole-tree snapshot epoch is retired (S-15); per-edited-file history captures only files the agent touches, with no tree walk and no size gate. Remove the env var. Per-blob SQL sizing is governed by SNAPSHOT_BLOB_SQL_MAX_BYTES / object storage as before.",
@@ -19,13 +19,14 @@ import { centerManagedConfigKeys } from "../web-admin-client/apply-effective.js"
19
19
  import { createConfigProvider, raceBootFetch, serviceConfigSourceKind, BOOT_FETCH_DEFERRED } from "../config-provider.js";
20
20
  import { createKeyResolver } from "../key-resolver.js";
21
21
  import { buildSealedKeyPublicFace, ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
22
- import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applySkillsManifest, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, fullyLandedForLkg, fetchPromptArtifact } from "../web-admin-client/facade.js";
22
+ import { applyEffective, applyCenterReadFace, buildReadFaceBaseline, buildReadFaceSwapCandidate, restoreReadFaceBaseline, mutateInPlace, logEffectiveDiff, applySkillsManifest, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, fullyLandedForLkg, fetchPromptArtifact } from "../web-admin-client/facade.js";
23
23
  import { buildConfigHandoff, buildRestartSignal, configHandoffMissing } from "../fleet/engine-signal.js";
24
24
  import { createMcpRevocationLedger, enabledMcpEntries } from "../web-admin-client/mcp-revocation.js";
25
25
  import { buildUnreadConfigFileFields } from "../web-admin-client/read-warnings.js";
26
26
  import { CONFIG_DOMAIN_INVALID_NOTE, buildCandidateRejectedFields, candidateRejectionOf, judgeConfigCandidate } from "../web-admin-client/candidate-gate.js";
27
27
  import { SHA256_HEX_RE } from "../digest-form.js";
28
28
  import { BOOT_REJUDGE_OF_REFUSED_GROUP, createConfigApplyLedger, modelPlaneCommitted, refusedGroups } from "../web-admin-client/apply-ledger.js";
29
+ import { buildDeploymentPostureSeats } from "./runner-deps.js";
29
30
  function lkgReadNote(scope) {
30
31
  switch (scope) {
31
32
  case "whole":
@@ -631,7 +632,7 @@ export async function createWebAdminRuntime(ctx) {
631
632
  if (!dryRun && cc !== undefined && currentIdentity() !== promptStateIdentity)
632
633
  await rebasePromptState(currentIdentity(), "boot-end");
633
634
  }
634
- const readFaceBaseline = { ...(config.readFace !== undefined ? { readFace: config.readFace } : {}), readFaceSource: config.readFaceSource };
635
+ const readFaceBaseline = buildReadFaceBaseline(config);
635
636
  const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined).envHeld;
636
637
  if (!modelReadyState.ready)
637
638
  logger.warn("model_roster_pending", {
@@ -648,18 +649,21 @@ export async function createWebAdminRuntime(ctx) {
648
649
  ...(mcpRevocationWired && mcpRevocations.hasBaseline() ? { mcpRevocationLive: true } : {}),
649
650
  });
650
651
  const applyLiveReadFace = (eff, swap, phase) => {
651
- const before = config.readFace;
652
- const beforeSource = config.readFaceSource;
653
- const { faceChanged } = applyCenterReadFace(config, eff, logger, { phase: "live", baseline: readFaceBaseline });
654
- if (!faceChanged)
652
+ const before = buildReadFaceBaseline(config);
653
+ const { changedSeats } = applyCenterReadFace(config, eff, logger, { phase: "live", baseline: readFaceBaseline });
654
+ if (changedSeats.length === 0)
655
655
  return;
656
656
  try {
657
- swap({ readFace: config.readFace });
658
- logger.info("read_face_swapped", { from: before ?? null, to: config.readFace ?? null, phase, note: "hot seat (core swapDeps) — legs that already prepared keep the face they prepared under" });
657
+ swap(buildReadFaceSwapCandidate(buildDeploymentPostureSeats(config), changedSeats));
658
+ logger.info("read_face_swapped", {
659
+ seats: changedSeats,
660
+ ...(changedSeats.includes("readFace") ? { from: before.readFace ?? null, to: config.readFace ?? null } : {}),
661
+ phase,
662
+ note: "hot seats (core swapDeps) — legs that already prepared keep the read posture they prepared under",
663
+ });
659
664
  }
660
665
  catch (err) {
661
- config.readFace = before;
662
- config.readFaceSource = beforeSource;
666
+ restoreReadFaceBaseline(config, before);
663
667
  throw err;
664
668
  }
665
669
  };
@@ -976,6 +980,9 @@ export async function createWebAdminRuntime(ctx) {
976
980
  if (configProvider.kind === "remote")
977
981
  lastPull = "ok";
978
982
  }
983
+ catch (err) {
984
+ logger.warn("web_admin_refresh_failed", { err: String(err), leg: "boot-deferred", note: "etag NOT advanced — the refresh cadence re-fetches and re-applies the same candidate" });
985
+ }
979
986
  finally {
980
987
  refreshInFlight = false;
981
988
  }
@@ -21,13 +21,16 @@
21
21
  *
22
22
  * ## 修形 = 照 [ref] 的配方接第四面(不是新造语义)
23
23
  *
24
- * brain 换成 `createPerModelKeyBrain(brain, getKeyResolver)`:
25
- * · 有 per-model key ⇒ 注入 `options.apiKey`,请求带模型自己的凭据;
26
- * · 无 ⇒ 不注入 ⇒ 交回 core 按它自己的 key↔URL 配对法判(core 5.57.0 起:部署根声明了而 entry URL
27
- * 不在那个根上 ⇒ `route.credential_mismatch` 响亮拒;未声明部署根 = `"unpinned"` 快速起步姿态 ⇒
28
- * 照旧回落网关 key。[ref] 的具名残余与其 P-DEBT 腿已在源头清偿,见 side-query-lane.ts 顶注);
24
+ * 当年 core 的摘要器没有凭据座,只能在 brain 外面包一层注入 `options.apiKey`(`createPerModelKeyBrain`)。
25
+ * core 7.51.0 起 `WebFetchSummarizerOptions.getApiKeyAndHeaders` 是与 `TaskSpec` / `SideQuerySpec` 同一签名的座
26
+ * (每次摘要按摘要器的模型解一次,`{ apiKey, headers }` 两半都收)⇒ 那层包装退役:`RunnerDeps.brain` 原样交给
27
+ * 摘要器,座位由与 side-query / 记忆整理**同一只**座位工厂(`createPerModelAuthSeat`)填充:
28
+ * · 有 per-model key ⇒ 请求带模型自己的凭据;
29
+ * · 无 ⇒ 座位回 `undefined` ⇒ 交回 core 按它自己的 key↔URL 配对法判(部署根上的模型带网关 key;
30
+ * entry URL 不在部署根上 ⇒ `route.credential_mismatch` 响亮拒 —— core 7.51.0 起没有「未声明部署根」的
31
+ * `unpinned` 快速起步姿态了,一份部署凭据只配一个根。[ref] 的具名残余与其 P-DEBT 腿已在源头清偿,
32
+ * 见 side-query-lane.ts 顶注);
29
33
  * · 毒丸 ⇒ **上抛**,一个字节都不发(CLAUDE.md [ref] 安全轴 fail-closed)。
30
- * wrapper 只包本席这一只引用,`RunnerDeps.brain` 零扰动(与 side-query 席同形)。
31
34
  *
32
35
  * ## 为什么住在 `boot/` 而不是继续躺在 `main()` 的闭包里
33
36
  *
@@ -49,7 +52,7 @@ import { type KeyResolver } from "./side-query-lane.js";
49
52
  /** WebFetch 摘要执行席(= `ScenarioDeps.webFetchSummarize`,core 的 `WebFetchConfig.summarize` 逐字同形)。 */
50
53
  export type WebFetchSummarizeLane = NonNullable<NonNullable<WebFetchConfig["summarize"]>>;
51
54
  export interface WebFetchSummarizeLaneCtx {
52
- /** 与 `RunnerDeps.brain` **同一只**实例(wrapper 在外面包,不改这只)。 */
55
+ /** 与 `RunnerDeps.brain` **同一只**实例,原样交给摘要器(凭据走摘要器的座位,不包 brain)。 */
53
56
  brain: Brain;
54
57
  /** 活配置引用:目录/角色表被 web-admin 就地热应用(`mutateInPlace`),按调用取值才跟得上。 */
55
58
  config: ServiceConfig;
@@ -1,7 +1,6 @@
1
1
  import { createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel } from "@sema-agent/core";
2
- import { createPerModelKeyBrain } from "./side-query-lane.js";
2
+ import { createPerModelAuthSeat } from "./side-query-lane.js";
3
3
  export function createWebFetchSummarizeLane(ctx) {
4
- const brain = createPerModelKeyBrain(ctx.brain, ctx.getKeyResolver);
5
4
  return async (content, prompt, signal) => {
6
5
  const model = (() => {
7
6
  try {
@@ -14,7 +13,7 @@ export function createWebFetchSummarizeLane(ctx) {
14
13
  })();
15
14
  if (!model)
16
15
  throw new Error("summarize model unresolved for this deployment");
17
- return createWebFetchSummarizer(brain, model)(content, prompt, signal);
16
+ return createWebFetchSummarizer(ctx.brain, model, { getApiKeyAndHeaders: createPerModelAuthSeat(ctx.getKeyResolver) })(content, prompt, signal);
18
17
  };
19
18
  }
20
19
  //# sourceMappingURL=webfetch-summarize-lane.js.map
package/dist/brain.js CHANGED
@@ -54,11 +54,10 @@ export function createBrainAssembly(config, deps = {}) {
54
54
  : {};
55
55
  const routes = deploymentGatewayRoots(config);
56
56
  const openaiBrains = routes.map((baseUrl, i) => {
57
+ const gatewayKey = deploymentGatewayKey(config);
57
58
  const brain = constructRouteBrain(i === 0 ? "gateway route" : `gateway fallback route[${i - 1}]`, [baseUrl], () => createOpenAIBrain({
58
59
  baseUrl,
59
- get apiKey() {
60
- return deploymentGatewayKey(config);
61
- },
60
+ ...(gatewayKey !== undefined ? { apiKey: gatewayKey } : {}),
62
61
  ...retryCap,
63
62
  fetchImpl,
64
63
  ...timeouts,
@@ -106,16 +105,14 @@ export function createBrainAssembly(config, deps = {}) {
106
105
  const ownSealed = credential.sealed !== undefined ? { [fallbackModel.name]: credential.sealed } : {};
107
106
  const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, ownEnvRef, process.env, ownSealed);
108
107
  const anthropicLaneHop = dispatchBrainLane(fallbackModel.provider, anthropicBrain !== undefined) === "anthropic";
108
+ const fbDeclaredRoot = degradeDeclaredRoot(fallbackModel.baseUrl, config);
109
+ const fbDeploymentKey = deploymentGatewayKey(config);
109
110
  const fallback = anthropicLaneHop && anthropicBrain
110
111
  ?
111
112
  anthropicBrain
112
113
  : constructRouteBrain(`degrade target "${degradeTo}"`, [fallbackModel.baseUrl, config.gatewayBaseUrl], () => createOpenAIBrain({
113
- get baseUrl() {
114
- return degradeDeclaredRoot(fallbackModel.baseUrl, config);
115
- },
116
- get apiKey() {
117
- return deploymentGatewayKey(config) || undefined;
118
- },
114
+ ...(fbDeclaredRoot !== undefined ? { baseUrl: fbDeclaredRoot } : {}),
115
+ ...(fbDeploymentKey ? { apiKey: fbDeploymentKey } : {}),
119
116
  ...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
120
117
  fetchImpl,
121
118
  ...timeouts,
@@ -27,8 +27,8 @@ export interface SubagentExtraToolOptions {
27
27
  *
28
28
  * core 7.46.0 #1432 —— **工厂与名册一个来源**:返回委派工具的两键,`extraTools`(每次 spawn 交出 `forSpawn` 选中的那几只)与
29
29
  * `extraToolNames`(全部可能交出的那几只的名字 / 别名 / 内容来源,从同一批 `ToolSpec` 读出)。名册在场 ⇒ 引擎重新能列出子代的工具池:
30
- * 定义级 / 工具级 / `TaskSpec.agents` 清单里没有任何工具答应的名字(core 7.49.0 #1623 起含无人挂的名;退役拼法与大小写孪生 7.46.0 起)开跑前拒
31
- * (`config.agent.unknown_tool`),记忆内容来源读者按声明读这几只。
30
+ * 定义级 / 工具级 / `TaskSpec.agents` 清单里没有任何工具答应的名字由引擎按名册判(core 7.51.0 #1736 起不拒 run:条目原样保留、告知一次并带该写的拼法;
31
+ * 7.46.0–7.50.x 曾开跑前拒 `config.agent.unknown_tool`),记忆内容来源读者按声明读这几只。
32
32
  * 修前(core 7.32.0–7.45.x)工厂恒在场 ⇒ 池不可静态知 ⇒ 那一族拼法只告不拒、委派结果一律按「外部」读。
33
33
  * 工厂**不抛**:各组的 `tools` 是装配期就建好的对象;部署 `forSpawn` 的抛由 core 收成「这一 spawn 什么都不继承 + 模型面披露句」(onExtraToolsError 记 warn)。
34
34
  * 一种拼法出现在两只工具上(部署工具与 `Now` 撞名 / 别名撞)⇒ core 在建委派工具处抛 `config.extra_tool_names`(`shape`),不静默取其一。
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 引擎能力词 → `GET /v1/capabilities` 顶层布尔键(core 7.51.1 `ENGINE_CAPABILITIES`;[ref]:词由引擎定,投影形由本仓定)。
3
+ *
4
+ * 形:引擎的能力词**原样**作顶层键(`"inherit_env.own_credentials_withheld": true`),值 = 该词在**本进程**为真的事实。键集由 `ENGINE_CAPABILITIES`
5
+ * 机器派生(一只投影循环,不逐词写键);事实表 {@link ENGINE_CAPABILITY_FACTS} 是 `Record<EngineCapability, …>` 型 —— 引擎加词而本表没跟 = tsc 红,
6
+ * 本表多一个引擎没有的词 = tsc 红(闭集的双向门),所以「这台引擎说它能,本服务也真接上了」不靠人记得登记。
7
+ * 壳按词闸(cli 读顶层布尔 `true`),不按版本号猜;契约附录 B.1 与 `test/capabilities-doc-appendix.test.ts` 把这些键与引擎词集对拍。
8
+ */
9
+ import { type EngineCapability } from "@sema-agent/core";
10
+ /** 投影:引擎词集的每一词 → 本进程的事实(键集恒 = `ENGINE_CAPABILITIES`)。 */
11
+ export declare function engineCapabilityBits(): {
12
+ readonly [W in EngineCapability]: boolean;
13
+ };
14
+ //# sourceMappingURL=engine-capabilities.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { ENGINE_CAPABILITIES } from "@sema-agent/core";
2
+ import { ownCredentialsWithheld } from "../server-secret-env.js";
3
+ const ENGINE_CAPABILITY_FACTS = {
4
+ "inherit_env.own_credentials_withheld": ownCredentialsWithheld,
5
+ };
6
+ export function engineCapabilityBits() {
7
+ const out = {};
8
+ for (const word of ENGINE_CAPABILITIES)
9
+ out[word] = ENGINE_CAPABILITY_FACTS[word]();
10
+ return out;
11
+ }
12
+ //# sourceMappingURL=engine-capabilities.js.map
@@ -328,5 +328,7 @@ export declare function workspaceCache(c: Pick<ServiceConfig, "remoteExec">): Fa
328
328
  }>;
329
329
  /** 自唤醒调度器挂在宿主 env 上:`SCHEDULER_ENABLED` ∧ 单用户 ∧ host 车道。装配(`boot/execution-env.ts`)与能力位同调。 */
330
330
  export declare function hostScheduler(c: Pick<ServiceConfig, "schedulerEnabled" | "requirePrincipal" | "remoteExec">): Fact<"scheduler_disabled" | "multi_tenant_deployment" | "not_host_lane">;
331
- export {};
331
+ /** 引擎能力词的事实(core `ENGINE_CAPABILITIES` 每词一只谓词,值 = 本进程的事实;`/v1/capabilities` 以顶层布尔键展开它们)—— 住在
332
+ * `capabilities/engine-capabilities.ts`,这里只是把它纳入「能力位只投影事实」的同一张读面,静态门认 `facts.engineCapabilityBits()` 这一只展开。 */
333
+ export { engineCapabilityBits } from "./capabilities/engine-capabilities.js";
332
334
  //# sourceMappingURL=capability-facts.d.ts.map
@@ -378,4 +378,5 @@ export function hostScheduler(c) {
378
378
  return no("not_host_lane");
379
379
  return YES;
380
380
  }
381
+ export { engineCapabilityBits } from "./capabilities/engine-capabilities.js";
381
382
  //# sourceMappingURL=capability-facts.js.map
@@ -5,6 +5,7 @@ import { CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS, CENTER_CREDENTIAL_RENEW_LEAD_M
5
5
  import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
6
6
  import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS } from "./workspace-cache.js";
7
7
  import { WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE } from "./workspace-space.js";
8
+ import { LIVENESS_MS_DEFAULT } from "./exec-bounds.js";
8
9
  import { THINKING_FORMATS } from "./model-compat.js";
9
10
  import { AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, CENTER_BASE_URL_RULE, CONFIG_PROVIDER_WORDS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS, FLEET_LEASE_OUTAGE_MAX_DEFAULT_MS, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
10
11
  import { webSearchProviderFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
@@ -30,6 +31,8 @@ const RESTART_SLICE_SET = {
30
31
  };
31
32
  export const CATALOG_RESTART_SLICES = Object.keys(RESTART_SLICE_SET);
32
33
  const r = (name, domain, type, semantics, opts = {}) => ({ name, domain, type, semantics, ...opts });
34
+ const LIVENESS_KNOB_SEMANTICS = (lane) => `${lane} 命令静默活性上限(ms):无输出满这么久即判挂死切断,只管没有显式超时的命令 —— 带显式超时的命令只受那个超时约束(安静的长构建给命令一个显式超时即可,不必调大本值)`;
35
+ const LIVENESS_KNOB_DEFAULT_NOTE = `缺席 ⇒ adapter 默认 ${LIVENESS_MS_DEFAULT / 1000}s`;
33
36
  const cLimits = (path, over = {}) => ({
34
37
  domain: "limits",
35
38
  path,
@@ -138,7 +141,7 @@ export const CONFIG_CATALOG = [
138
141
  r("E2B_BG_DEFAULT_TIMEOUT_SEC", "orchestration", "number", "e2b 后台 shell 缺省超时(秒)", { staticDefault: "300" }),
139
142
  r("E2B_BG_MAX_CONCURRENT", "orchestration", "number", "e2b 后台 shell 并发帽", { staticDefault: "8" }),
140
143
  r("E2B_BG_MAX_TIMEOUT_SEC", "orchestration", "number", "e2b 后台 shell 超时上限(秒)", { staticDefault: "1800" }),
141
- r("E2B_LIVENESS_MS", "orchestration", "number", "e2b 单命令静默活性上限(长静默构建请调大;后台+轮询仍是首选)", { derivedDefaultNote: "缺席 ⇒ adapter 默认 120s" }),
144
+ r("E2B_LIVENESS_MS", "orchestration", "number", LIVENESS_KNOB_SEMANTICS("e2b"), { derivedDefaultNote: LIVENESS_KNOB_DEFAULT_NOTE }),
142
145
  r("E2B_MOUNT_PATH", "orchestration", "path", "e2b 工作区根(resume 用当前值,改前先 drain 挂起任务)"),
143
146
  r("E2B_ORPHAN_RECLAIM", "orchestration", "boolean", "崩溃遗留沙盒回收腿(boot 一次+周期;判据保守,拿不准恒不杀)", { staticDefault: "true", danger: BILL }),
144
147
  r("E2B_SANDBOX_ENV", "orchestration", "secret", "转发进 e2b 沙箱的本进程 env 点名表(NAME,NAME;点到的键从 worker env 原样取,值不进 spec/center/事件日志;与 LSP_ENV_ALLOW / DOCKER_SANDBOX_ENV 同一只解析器与「点名放行」谓词:形错 / 点名名单型旋钮拒启且不回显值;本进程环境里没有的名按序号 warn、不转发;只在本车道被选中时求值;原文按凭据类对待:只回在场位、不进任何子进程)", { danger: SEC }),
@@ -191,6 +194,7 @@ export const CONFIG_CATALOG = [
191
194
  r("K8S_CA_CERT_B64", "orchestration", "opaque", "k8s API CA 证书(base64;registry secrets 无换行约束)", { danger: SEC }),
192
195
  r("K8S_CPU", "orchestration", "string", "沙箱 pod CPU 限额(profile 数据)"),
193
196
  r("K8S_INSECURE_TLS", "orchestration", "boolean", "跳过 k8s API TLS 校验", { staticDefault: "false", danger: SEC }),
197
+ r("K8S_LIVENESS_MS", "orchestration", "number", LIVENESS_KNOB_SEMANTICS("k8s"), { derivedDefaultNote: LIVENESS_KNOB_DEFAULT_NOTE }),
194
198
  r("K8S_MEMORY", "orchestration", "string", "沙箱 pod 内存限额(2Gi 默认曾被真 OOM)"),
195
199
  r("K8S_MOUNT_PATH", "orchestration", "path", "k8s 工作区挂载路径"),
196
200
  r("K8S_NAMESPACE", "orchestration", "string", "沙箱 pod namespace"),
@@ -471,8 +471,9 @@ export interface ServiceConfigFlat {
471
471
  * 由 boot(config.ts)与热路径(apply-effective)同一只判据保证。 */
472
472
  gatewayBaseUrl?: string;
473
473
  /** `MODEL_API_KEY` —— **网关的**凭据,只与网关根配对([ref])。S-623:与 `gatewayBaseUrl` 同进同出 ——
474
- * 没有网关就不装载(否则 brain 的声明根为空 ⇒ core 判 `unpinned` ⇒ 这把 key 会被送到每条自带 baseUrl 的
475
- * openai 条目),由 `model_gateway_key_unpaired` 启动通知说出来。 */
474
+ * 没有网关就不装载(否则 brain 的声明根为空:core 7.51.0 起判 `route.credential_mismatch`,每条自带 baseUrl、没有
475
+ * per-model 凭据的 openai 条目被拒;7.51.0 前是 `unpinned`,key 随请求发往那些条目),由 `model_gateway_key_unpaired`
476
+ * 启动通知说出来。S-1561:brain 在构造时读它一次(core 收据律),本仓没有运行期写者 ⇒ 换值 = 重启。 */
476
477
  gatewayApiKey?: string;
477
478
  /** Secondary openai-compatible gateways for cross-gateway FAILOVER (same protocol + same model id).
478
479
  * Tried in order after the primary when a gateway fails before streaming any content. */
@@ -897,6 +898,9 @@ export interface ServiceConfigFlat {
897
898
  runtimeClass?: string;
898
899
  mountPath?: string;
899
900
  timeoutMs?: number;
901
+ /** `K8S_LIVENESS_MS` — the lane's liveness bound (ms; the `E2B_LIVENESS_MS` twin, same parser; absent ⇒ the adapter's
902
+ * default 120 s): cuts a command silent for this long ONLY when its caller stated no timeout (`exec-bounds.ts`). */
903
+ livenessMs?: number;
900
904
  /** Resource-profile knobs: sandbox pod limits (adapter defaults: 2Gi / 1 cpu). */
901
905
  memory?: string;
902
906
  cpu?: string;
@@ -1759,7 +1763,7 @@ export interface ServiceConfigFlat {
1759
1763
  * its own meaning (the sandbox lane's opt-in above) and is unaffected. */
1760
1764
  lspHostEnabled: boolean;
1761
1765
  /** S-699 —— host-lane language-server env **named allowlist** (`LSP_ENV_ALLOW=NAME,NAME`). The language server's env is
1762
- * `scrubServerSecrets(process.env)`; each name here is put back **verbatim** from the service's own env on top of that
1766
+ * the engine's `inheritedHostEnv("scrub")` (shape rules + this deployment's declared own credentials); each name here is put back **verbatim** from the service's own env on top of that
1763
1767
  * (`allowlistedEnv`, the same "named keys pass" predicate as the host shell's `inheritEnv:[names]`) — the deployer
1764
1768
  * explicitly accepts handing that value to code the language server runs. Unnamed secret-shaped keys stay stripped.
1765
1769
  * `[]` = nothing put back (the default: unset / empty string). A malformed list (empty segment / not an env-var name /
package/dist/config.js CHANGED
@@ -27,6 +27,7 @@ import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdent
27
27
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
28
28
  import { lkgPathOverrideOf } from "./config-lkg.js";
29
29
  import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude } from "./workspace-space.js";
30
+ import { laneLivenessOf } from "./exec-bounds.js";
30
31
  import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
31
32
  import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap } from "./workspace-cache.js";
32
33
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
@@ -333,6 +334,10 @@ function optNumEnvStrict(name) {
333
334
  return undefined;
334
335
  return parseNumOrFail(name, raw);
335
336
  }
337
+ function livenessMsEnv(name) {
338
+ const n = optNumEnvStrict(name);
339
+ return n === undefined ? undefined : laneLivenessOf(n, name).ms;
340
+ }
336
341
  function parseDeviceEnrollTokenTtlEnv() {
337
342
  const raw = process.env.DEVICE_ENROLL_TOKEN_TTL_SEC;
338
343
  if (raw === undefined || raw === "")
@@ -1836,7 +1841,7 @@ function parseOrchestrationDomain(ctx) {
1836
1841
  apiKey: process.env.E2B_API_KEY,
1837
1842
  ...(process.env.E2B_TEMPLATE ? { template: process.env.E2B_TEMPLATE } : {}),
1838
1843
  ...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("E2B_TIMEOUT_MS")),
1839
- ...((n) => (n !== undefined ? { livenessMs: n } : {}))(optNumEnvStrict("E2B_LIVENESS_MS")),
1844
+ ...((n) => (n !== undefined ? { livenessMs: n } : {}))(livenessMsEnv("E2B_LIVENESS_MS")),
1840
1845
  ...(process.env.E2B_ALLOW_NET ? { allowInternetAccess: boolEnv("E2B_ALLOW_NET", true) } : {}),
1841
1846
  ...((names) => (names !== undefined ? { sandboxEnv: allowlistedEnv(names, process.env) } : {}))(parseEnvNameAllowlist("E2B_SANDBOX_ENV", singleUserTurnkey)),
1842
1847
  ...(process.env.E2B_MOUNT_PATH ? { mountPath: process.env.E2B_MOUNT_PATH } : {}),
@@ -1855,6 +1860,7 @@ function parseOrchestrationDomain(ctx) {
1855
1860
  ...(process.env.K8S_RUNTIME_CLASS != null ? { runtimeClass: process.env.K8S_RUNTIME_CLASS } : {}),
1856
1861
  ...(process.env.K8S_MOUNT_PATH ? { mountPath: process.env.K8S_MOUNT_PATH } : {}),
1857
1862
  ...((n) => (n !== undefined ? { timeoutMs: n } : {}))(optNumEnvStrict("K8S_TIMEOUT_MS")),
1863
+ ...((n) => (n !== undefined ? { livenessMs: n } : {}))(livenessMsEnv("K8S_LIVENESS_MS")),
1858
1864
  ...(process.env.K8S_MEMORY ? { memory: process.env.K8S_MEMORY } : {}),
1859
1865
  ...(process.env.K8S_CPU ? { cpu: process.env.K8S_CPU } : {}),
1860
1866
  sandboxIpv6: enumEnv("K8S_SANDBOX_IPV6", SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS),
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 本服务**自己**发往配置端点的出站请求(hook 直连模型腿 `hooks/hook-llm.ts`、记忆 embedder `plugins/memory-embedder.ts`)的
3
+ * 重定向律 —— 与 core 7.51.0(#1531)一方 brain 的请求同一条:请求以 `redirect: "manual"` 发出,端点回重定向 ⇒ 拒;
4
+ * 不跟、不重试、拒句不点名目标。
5
+ *
6
+ * 为什么:请求只去被判过的那个根。传输层(undici)跟随 POST 307 / 308 时把请求体(用户内容 / 记忆正文)原样送到
7
+ * `Location`,跨 origin 只剥 `authorization` —— anthropic 形的 `x-api-key` 随行(`test/hook-llm.test.ts` 的 G1 格在
8
+ * 7.128.x 上实测送达重定向目标)。判据与拒句与 core 的 brain 请求门同形(`opaqueredirect` 或 3xx)。
9
+ */
10
+ /** 端点回的是重定向 ⇒ 拒句(调用方在前面冠上自己的面名);否则 `undefined`。命中时丢弃响应体(不读、不留连接)。 */
11
+ export declare function egressRedirectRefusal(res: Response, fix: string): string | undefined;
12
+ //# sourceMappingURL=egress-redirect.d.ts.map
@@ -0,0 +1,7 @@
1
+ export function egressRedirectRefusal(res, fix) {
2
+ if (res.type !== "opaqueredirect" && !(res.status >= 300 && res.status < 400))
3
+ return undefined;
4
+ void res.body?.cancel().catch(() => undefined);
5
+ return `endpoint answered with a redirect${res.status > 0 ? ` (HTTP ${res.status})` : ""}; it was not followed and no credential was sent anywhere else — ${fix}`;
6
+ }
7
+ //# sourceMappingURL=egress-redirect.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * S-1306 —— **入口地板钳制**通告(`engine_notice` 码 `permission.entry_mode_clamped`)的唯一铸点(core 7.51.0 #1527 入册:受众 user、
3
+ * 铸者 `NOTICE_MINTER` = `"server"`,引擎从不铸它)。
4
+ *
5
+ * ## 这件事为什么归本仓
6
+ * 运维 `AUTONOMY=plan` 的入口地板在**受理门**把调用方请求的模式词折回地板(`task-settings.ts` `stampEntryPermissionMode`,底下是
7
+ * `runtime-governance.ts` `entryPermissionMode` → core `clampPermissionMode`)—— 折叠发生在 run 到达引擎**之前**,所以引擎自己的
8
+ * `config.permission_mode_unavailable`(它只说它自己做的折叠)看不见这一折。修前 run 在另一个模式下起步,人一个字都看不到。
9
+ *
10
+ * ## 铸点(唯一)
11
+ * {@link buildEntryModeClampNotice} 是本码**唯一**的铸点;调用点只有一处:`http/admission.ts` 的 `prepareSpec`(提交入口,盖章之后、装配
12
+ * 之后 —— 那时会话 id 才定)。一次提交一次 ⇒ 一条 run 至多一帧(core 码册的去重单位「once per run」)。读的是盖章那一次求值的读数
13
+ * (`EntryPermissionMode`),不另判一遍「折没折」。续跑腿不铸:它们重放的是已盖过章的存储体,折叠只发生过一次。
14
+ * 装配期铸、腿开口那一拍投(`PreparedTaskSubmission.engineNotices` → `registerEngineNoticeLeg` 的 `pending`)—— 与请求腿 MCP 丢弃同一条投递路,
15
+ * 白名单 / 归属 / 消毒 / 隔离一个不绕。
16
+ *
17
+ * ## detail
18
+ * `{ requested, effective, ceiling, sessionId }` —— `requested` = 被折的那个词(体的生效词:顶层 `permissionMode` 或
19
+ * `settings.permissions.defaultMode`)、`effective` = run 起步的词、`ceiling` = 折它的地板,三个都是 core 的模式闭集词;`sessionId` = 归属键。
20
+ * 🔴 **无 `runId`**:钳制发生在受理门,引擎的 run 身份要到 prepare 才铸,这一刻不存在(core 码册那一行写了 `runId`;本仓不编造,
21
+ * 归属看它落在哪条 run 的流 / 账本上 —— 与 `instructions.source_changed` / `session.records_skipped` 同形)。
22
+ * 什么时候不铸:没有地板;没声明词(取地板本身不算改词,`clamped` 为假);请求的词不比地板松;会话 id 缺席(无处可投,宁缺不串)。
23
+ */
24
+ import type { EngineNotice, EngineNoticeCode } from "@sema-agent/core";
25
+ import type { EntryPermissionMode } from "./runtime-governance.js";
26
+ /** 码常量型 = core 码册(#1527 入册:受众 user、铸者 `"server"`)。 */
27
+ export declare const ENTRY_MODE_CLAMPED: EngineNoticeCode;
28
+ /** 一次受理门盖章的读数 → 至多一条通告(读法见文件头)。 */
29
+ export declare function buildEntryModeClampNotice(entry: EntryPermissionMode, sessionId: string | undefined): EngineNotice | undefined;
30
+ //# sourceMappingURL=entry-mode-clamp-notice.d.ts.map
@@ -0,0 +1,13 @@
1
+ export const ENTRY_MODE_CLAMPED = "permission.entry_mode_clamped";
2
+ export function buildEntryModeClampNotice(entry, sessionId) {
3
+ const { requested, effective, floor, clamped } = entry;
4
+ if (!clamped || requested === undefined || floor === undefined || effective === undefined || sessionId === undefined || sessionId.length === 0)
5
+ return undefined;
6
+ return {
7
+ code: ENTRY_MODE_CLAMPED,
8
+ message: `This run starts in permission mode "${effective}", not the requested "${requested}": ` +
9
+ `the deployment's entry floor ("${floor}") clamps every submitted run's mode to it.`,
10
+ detail: { requested, effective, ceiling: floor, sessionId },
11
+ };
12
+ }
13
+ //# sourceMappingURL=entry-mode-clamp-notice.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * 执行车道上「什么约束一条命令」的**唯一一条律**(host / e2b / k8s 三只 adapter 同读这一只;纯叶,零依赖):
3
+ *
4
+ * 命令由调用方**说出口**的界约束 —— 墙钟 `timeout`(buffered 与流式两面)与 / 或它自己的静默界 `readTimeoutMs`
5
+ * (流式面)。调用方**两样都没说**时,才由车道自己的活性界兜底:无输出满这么久即判挂死(k8s `K8S_LIVENESS_MS` /
6
+ * e2b `E2B_LIVENESS_MS`,缺省 {@link LIVENESS_MS_DEFAULT};host 没有这一界)—— 那时它是这条命令唯一的挂死护栏。
7
+ * 切断时报的是**真正触发的那一界**(活性切断的句子只在 {@link livenessCutMessage} 铸)。
8
+ *
9
+ * 为什么「说了墙钟就不再活性切断」:墙钟本身就是挂死护栏(它到点必切),而调用方说出一个墙钟,就是在说「这条命令可以跑
10
+ * 这么久」—— 安静的长构建(Gradle / R8 一段几分钟不出声)正是这种命令。两界并存时,活性界会先于墙钟切掉一条按约正在跑的
11
+ * 命令,而调用方(core 的 Bash 工具)只知道自己说过的墙钟,便把这次切断报成「跑满了 N 秒」。两界互斥之后,一次切断只可能
12
+ * 是它报的那一界。
13
+ *
14
+ * 与 core 远端契约(`ExecStreamOptions.readTimeoutMs`:「缺省也 SHOULD 有活性界,一条命令不得等到环境死」)的对齐:
15
+ * 说了墙钟的命令被墙钟兜住,不会无限等;两界都没说的命令仍有车道活性界。切断码沿用 core 的 `timeout`(core 远端码表
16
+ * 把墙钟与活性两种超界都归这一词);两者靠句子区分 —— 独立的活性码属于 core 的码表。
17
+ *
18
+ * 不在本律里的界(adapter 自己的上限,不是调用方说的话,不关活性界):k8s 文件读写的数据上限 `dataTimeoutMs`、e2b 交给
19
+ * 供应商的沙箱寿命上限。adapter 自己的整段数据搬运(k8s 快照 tar 上 / 下)天生静默,它们**自己说**一个墙钟(数据上限),
20
+ * 落在本律的第一支。
21
+ */
22
+ /** 车道活性界的缺省(ms)—— k8s / e2b 两只 adapter 与配置目录的「缺席 ⇒ 默认」说法同读这一只。 */
23
+ export declare const LIVENESS_MS_DEFAULT = 120000;
24
+ /** 有活性界的车道各自的旋钮名(闭集;切断句点名它,运维据此知道调哪一只)。 */
25
+ export type LivenessKnob = "K8S_LIVENESS_MS" | "E2B_LIVENESS_MS";
26
+ /** 一条车道自己的活性界:k8s / e2b 有,host 没有(传 `undefined`)。只经 {@link laneLivenessOf} 铸(值已判过)。 */
27
+ export interface LaneLiveness {
28
+ readonly ms: number;
29
+ readonly knob: LivenessKnob;
30
+ }
31
+ /**
32
+ * 车道活性界的唯一铸点与唯一判据:值必须是有限正数(ms),否则**拒**(抛,句子点名旋钮)。活性界是不说界的命令唯一的挂死护栏 ——
33
+ * `0` / 负数会把它整个撤掉(那条命令从此无界),非数无从计时。boot 的旋钮解析(`E2B_LIVENESS_MS` / `K8S_LIVENESS_MS`)与 adapter
34
+ * 构造(嵌入方直接传 `livenessMs`)都经这一只判,一个判据、一句拒词。
35
+ */
36
+ export declare function laneLivenessOf(ms: number, knob: LivenessKnob): LaneLiveness;
37
+ /** 本条命令的静默界:调用方说的(`readTimeoutMs`),或车道兜底的。 */
38
+ export type LivenessBound = {
39
+ readonly ms: number;
40
+ readonly setBy: "caller";
41
+ } | {
42
+ readonly ms: number;
43
+ readonly setBy: "lane";
44
+ readonly knob: LivenessKnob;
45
+ };
46
+ /** 一条命令最终受的界:墙钟(调用方说的)与静默界,各自可缺。 */
47
+ export interface CommandBounds {
48
+ readonly wallMs: number | undefined;
49
+ readonly liveness: LivenessBound | undefined;
50
+ }
51
+ /** 调用方说出口的界(ms)。`timeoutMs` = 墙钟;`readTimeoutMs` = 它自己的静默界。 */
52
+ export interface StatedBounds {
53
+ readonly timeoutMs?: number | undefined;
54
+ readonly readTimeoutMs?: number | undefined;
55
+ }
56
+ /** 本律的唯一实现(见文件头注)。 */
57
+ export declare function commandBoundsOf(stated: StatedBounds, lane: LaneLiveness | undefined): CommandBounds;
58
+ /**
59
+ * 一条**还没出过一个字节**的命令最早被哪一界切:墙钟与静默界里较早的那只(都从调用开始算)。供应商建命令那一段(命令还没起跑,
60
+ * 自然也没有输出)就受它约束 —— 说了墙钟的命令不会被车道活性界在建命令时切掉;没说界的命令建命令卡住按活性界切 —— 但供应商自己的建命令期限
61
+ * 先到就先切(e2b SDK 的 60 s 握手:到点报的是供应商那一界,不是本模块的任何一只)。两只都缺 ⇒ 无界(只剩供应商自己的界)。
62
+ */
63
+ export declare function firstBoundOf(bounds: CommandBounds): {
64
+ readonly ms: number;
65
+ readonly cut: "wall" | LivenessBound;
66
+ } | undefined;
67
+ /** 活性切断的句子 —— 三车道唯一铸点(车道兜底的界点名旋钮与出路;调用方自己说的界点名那一参数)。 */
68
+ export declare function livenessCutMessage(bound: LivenessBound): string;
69
+ //# sourceMappingURL=exec-bounds.d.ts.map
@@ -0,0 +1,37 @@
1
+ export const LIVENESS_MS_DEFAULT = 120_000;
2
+ export function laneLivenessOf(ms, knob) {
3
+ if (!(Number.isFinite(ms) && ms > 0)) {
4
+ throw new Error(`${knob}=${ms} must be a positive number of milliseconds — it is the only hang guard of a command whose caller states no timeout, and a zero, negative or non-numeric value would remove it`);
5
+ }
6
+ return { ms, knob };
7
+ }
8
+ const said = (ms) => (ms !== undefined && Number.isFinite(ms) && ms > 0 ? ms : undefined);
9
+ export function commandBoundsOf(stated, lane) {
10
+ const wallMs = said(stated.timeoutMs);
11
+ const idleMs = said(stated.readTimeoutMs);
12
+ if (idleMs !== undefined)
13
+ return { wallMs, liveness: { ms: idleMs, setBy: "caller" } };
14
+ if (wallMs !== undefined || lane === undefined)
15
+ return { wallMs, liveness: undefined };
16
+ return { wallMs, liveness: { ms: lane.ms, setBy: "lane", knob: lane.knob } };
17
+ }
18
+ export function firstBoundOf(bounds) {
19
+ const { wallMs, liveness } = bounds;
20
+ if (liveness !== undefined && (wallMs === undefined || liveness.ms <= wallMs))
21
+ return { ms: liveness.ms, cut: liveness };
22
+ return wallMs === undefined ? undefined : { ms: wallMs, cut: "wall" };
23
+ }
24
+ function spoken(ms) {
25
+ if (ms < 1000)
26
+ return `${ms} ms`;
27
+ return ms % 1000 === 0 ? `${ms / 1000} s` : `${(ms / 1000).toFixed(1)} s`;
28
+ }
29
+ export function livenessCutMessage(bound) {
30
+ switch (bound.setBy) {
31
+ case "lane":
32
+ return `no output for ${spoken(bound.ms)} — cut off by the exec liveness bound (set ${bound.knob} or give the command an explicit timeout)`;
33
+ case "caller":
34
+ return `no output for ${spoken(bound.ms)} — cut off by the command's own idle bound (readTimeoutMs)`;
35
+ }
36
+ }
37
+ //# sourceMappingURL=exec-bounds.js.map
@@ -420,6 +420,13 @@ export interface BgNotification {
420
420
  seq?: number;
421
421
  status: "completed" | "failed" | "killed";
422
422
  summary?: string;
423
+ /** S-1296(core 7.51.0 #1482 / #1528):the child's REPORT — the same string the completion frame the parent's model reads carries
424
+ * (core cuts it once at the frame's bound, redacting first), so a host that wakes an idle conversation reads it here instead of a
425
+ * second, differently-cut copy. UNTRUSTED child text ⇒ `redactSecrets` at this wire boundary, exactly like `summary` on this frame
426
+ * and `result` on the `task_notification` frame (one reading: the server redacts at its own boundary whatever upstream did).
427
+ * Notification-only (the task ROW is state and never carries it). ABSENT when the frame carries none (a killed / failed leg with no
428
+ * report, a withheld hand-back). */
429
+ result?: string;
423
430
  /** Background lifetime scope ([ref]), NOT the tenant scope. */
424
431
  scope: "task" | "session";
425
432
  parentTaskId?: string;
@@ -560,6 +560,7 @@ export function fleetBackgroundChildPublisher(bus, log) {
560
560
  ...(typeof e.seq === "number" ? { seq: e.seq } : {}),
561
561
  status: e.status ?? "failed",
562
562
  ...(e.summary ? { summary: redactSecrets(e.summary) } : {}),
563
+ ...(e.result !== undefined ? { result: redactSecrets(e.result) } : {}),
563
564
  scope: (m.sessionScoped || e.sessionScoped) ? "session" : "task",
564
565
  ...(m.parentTaskId ? { parentTaskId: m.parentTaskId } : m.resolvedParentId ? { parentTaskId: m.resolvedParentId } : {}),
565
566
  ...(e.completionId ? { completionId: e.completionId } : {}),
@@ -2,6 +2,7 @@ import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyO
2
2
  import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
5
+ import { egressRedirectRefusal } from "../egress-redirect.js";
5
6
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
6
7
  export function buildHookFailureText(raw) {
7
8
  if (raw === "")
@@ -70,7 +71,7 @@ export function createHookLlm(deps) {
70
71
  buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
71
72
  };
72
73
  }
73
- if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
74
+ if (verdict.posture === "keyless" && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
74
75
  return {
75
76
  ok: false,
76
77
  error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
@@ -131,7 +132,10 @@ export function createHookLlm(deps) {
131
132
  headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
132
133
  body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
133
134
  }
134
- const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal });
135
+ const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal, redirect: "manual" });
136
+ const redirected = egressRedirectRefusal(r, "declare the final endpoint as this route's base URL");
137
+ if (redirected !== undefined)
138
+ return { ok: false, error: `hook llm ${redirected}` };
135
139
  if (!r.ok)
136
140
  return { ok: false, error: `hook llm HTTP ${r.status}` };
137
141
  let text;
@@ -6,10 +6,10 @@
6
6
  * `process.env`,脑钥 / 仓库令牌 / 本仓自己的凭据键全在里面,而语言服务器会执行仓里的代码(构建脚本、插件、
7
7
  * 工作区配置)。7.30.1 起 core 默认改成 `scrubSecretEnv(process.env)`(按键名**形状**剥),并开了
8
8
  * `NodeLspManagerOptions.env` 一格让宿主自己交 env(core 帖 [ref] ④;core 确认本仓的正确读法 = 交 host 车道**同一只**
9
- * `scrubServerSecrets` 的结果)。
9
+ * 本仓唯一组合 `hostSubprocessEnv` 的结果 —— 引擎 `inheritedHostEnv("scrub")` ∪ 点名放回)。
10
10
  *
11
11
  * ## 规则(一句)
12
- * 语言服务器子进程的 env = `scrubServerSecrets`(process.env) ∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
12
+ * 语言服务器子进程的 env = 引擎 `inheritedHostEnv("scrub")`(形状剥 ∪ 本进程起服声明的自有凭据名)∪ 部署方点名放回的键(S-699,见下节;缺省空名单)——
13
13
  * 剥除与模型可驱动的 host shell(`inheritEnv:"scrub"`)
14
14
  * 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
15
15
  * 会漏掉形状规则认不出的本仓凭据键(当时是 `OTEL_EXPORTER_OTLP_HEADERS`,core 7.33.7 #1180 起 core 按名剥它;今天只剩名单型旋钮三只),那正是 S-684 当初的病形。
@@ -23,7 +23,7 @@
23
23
  * ## 点名放行(S-699,`LSP_ENV_ALLOW`)
24
24
  * 剥除会连语言服务器自己的真实依赖一起剥掉(`.npmrc` 的 `${NPM_TOKEN}` 插值、rust-analyzer `cargo metadata` 私有 registry 的
25
25
  * `CARGO_REGISTRIES_<名>_TOKEN` —— 形状规则按名判,分不出「给语言服务器用的令牌」与「编排器的令牌」)。出路 = 部署方点名:
26
- * env = `scrubServerSecrets`(process.env) 之上把 `config.lspEnvAllow` 点到的键经 `allowlistedEnv`(与 host shell
26
+ * env = 引擎 `inheritedHostEnv("scrub")` 之上把 `config.lspEnvAllow` 点到的键经 `allowlistedEnv`(与沙箱三腿
27
27
  * `inheritEnv:[names]` 同一只谓词)从原 env 原样放回;没点到的照旧剥 —— 7.103.0 起这件组合只有一只:{@link hostSubprocessEnv}
28
28
  * (`server-secret-env.ts`,bake-runner 的 `BAKE_ENV_ALLOW` 调同一只)。名单由 `config.ts` 解析(形错拒启;
29
29
  * `REQUIRE_PRINCIPAL=true` 下落空名单并 warn 一次),本函数只读 `config.lspEnvAllow`,不在这里再判一次姿态。