@sema-agent/server 7.127.2 → 7.129.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 (112) hide show
  1. package/MIGRATION.md +130 -0
  2. package/USAGE.md +137 -20
  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/audit.js +4 -1
  7. package/dist/bake-runner/main.js +2 -1
  8. package/dist/boot/execution-lane.d.ts +11 -0
  9. package/dist/boot/execution-lane.js +6 -0
  10. package/dist/boot/memory-consolidation.d.ts +46 -16
  11. package/dist/boot/memory-consolidation.js +15 -8
  12. package/dist/boot/resolve-spec.js +3 -1
  13. package/dist/boot/side-query-lane.d.ts +7 -20
  14. package/dist/boot/side-query-lane.js +0 -24
  15. package/dist/boot/stage-01-config.js +3 -0
  16. package/dist/boot/stage-06-runners.js +1 -1
  17. package/dist/boot/stores.js +2 -2
  18. package/dist/boot/web-admin.js +31 -20
  19. package/dist/boot/webfetch-summarize-lane.d.ts +10 -7
  20. package/dist/boot/webfetch-summarize-lane.js +2 -3
  21. package/dist/brain.js +6 -9
  22. package/dist/capabilities/builtin-tools.d.ts +2 -2
  23. package/dist/capabilities/engine-capabilities.d.ts +14 -0
  24. package/dist/capabilities/engine-capabilities.js +12 -0
  25. package/dist/capabilities/mcp-probe.js +11 -5
  26. package/dist/capability-facts.d.ts +3 -1
  27. package/dist/capability-facts.js +1 -0
  28. package/dist/config-catalog.js +1 -1
  29. package/dist/config-provider.d.ts +17 -4
  30. package/dist/config-provider.js +8 -6
  31. package/dist/config-types.d.ts +6 -5
  32. package/dist/egress-redirect.d.ts +12 -0
  33. package/dist/egress-redirect.js +7 -0
  34. package/dist/entry-mode-clamp-notice.d.ts +30 -0
  35. package/dist/entry-mode-clamp-notice.js +13 -0
  36. package/dist/fleet/fleet-bus.d.ts +7 -0
  37. package/dist/fleet/fleet-bus.js +1 -0
  38. package/dist/hooks/hook-llm.js +6 -2
  39. package/dist/hooks/hook-runner.d.ts +1 -0
  40. package/dist/hooks/hook-runner.js +10 -4
  41. package/dist/host-lsp-manager.d.ts +3 -3
  42. package/dist/http/admission.js +5 -2
  43. package/dist/http/resume-legs.js +46 -27
  44. package/dist/http/route-ctx.d.ts +1 -1
  45. package/dist/http/routes/approvals-assistant.js +6 -4
  46. package/dist/http/routes/capabilities.js +1 -0
  47. package/dist/http/routes/session-sync.js +3 -4
  48. package/dist/http/routes/sessions.js +4 -17
  49. package/dist/http/send.d.ts +1 -1
  50. package/dist/http/send.js +4 -3
  51. package/dist/http/wire-types.d.ts +14 -5
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/main.js +2 -0
  55. package/dist/mcp-assertion-header.d.ts +89 -0
  56. package/dist/mcp-assertion-header.js +49 -0
  57. package/dist/observability/fail-open.d.ts +7 -3
  58. package/dist/observability/fail-open.js +7 -3
  59. package/dist/observability/secret-env-scrub.d.ts +37 -3
  60. package/dist/observability/secret-env-scrub.js +2 -2
  61. package/dist/parked-decide.js +5 -2
  62. package/dist/plugins/host-platform.d.ts +43 -13
  63. package/dist/plugins/host-platform.js +67 -18
  64. package/dist/plugins/memory-embedder.js +5 -0
  65. package/dist/plugins/one-time-migrations.d.ts +180 -9
  66. package/dist/plugins/one-time-migrations.js +315 -3
  67. package/dist/plugins/pg-pool.js +3 -1
  68. package/dist/plugins/remote-env-host.d.ts +17 -20
  69. package/dist/plugins/remote-env-host.js +16 -28
  70. package/dist/plugins/session-policy-store-sql.d.ts +16 -4
  71. package/dist/plugins/session-policy-store-sql.js +17 -20
  72. package/dist/plugins/session-rules-column.d.ts +16 -0
  73. package/dist/plugins/session-rules-column.js +6 -0
  74. package/dist/plugins/tidb-pool.js +4 -1
  75. package/dist/read-face-posture.d.ts +2 -1
  76. package/dist/request-value-closure.js +1 -1
  77. package/dist/retired-card-words.d.ts +17 -0
  78. package/dist/retired-card-words.js +49 -0
  79. package/dist/rule-entry-list.js +2 -0
  80. package/dist/run-local.js +15 -6
  81. package/dist/server-secret-env.d.ts +77 -85
  82. package/dist/server-secret-env.js +40 -26
  83. package/dist/session-policy-wire.d.ts +61 -124
  84. package/dist/session-policy-wire.js +59 -121
  85. package/dist/session-sync.d.ts +8 -6
  86. package/dist/session-sync.js +6 -5
  87. package/dist/spec-fields.d.ts +3 -2
  88. package/dist/standing-answer-grant.d.ts +17 -9
  89. package/dist/standing-answer-grant.js +11 -8
  90. package/dist/task-mcp.d.ts +31 -3
  91. package/dist/task-mcp.js +42 -4
  92. package/dist/task-settings.d.ts +6 -1
  93. package/dist/task-settings.js +1 -1
  94. package/dist/tool-approval.d.ts +14 -7
  95. package/dist/tool-approval.js +23 -18
  96. package/dist/trace/core-keyset-guard.d.ts +3 -3
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/project.d.ts +7 -4
  100. package/dist/trace/project.js +7 -76
  101. package/dist/trace/tool-output-media.d.ts +10 -4
  102. package/dist/trace/tool-output-media.js +7 -29
  103. package/dist/web-admin-client/apply-effective.js +28 -11
  104. package/dist/web-admin-client/facade.d.ts +2 -2
  105. package/dist/web-admin-client/facade.js +2 -2
  106. package/dist/web-admin-client/http-client.d.ts +2 -2
  107. package/dist/web-admin-client/read-face.d.ts +73 -20
  108. package/dist/web-admin-client/read-face.js +76 -36
  109. package/dist/web-admin-client/skills-mcp.d.ts +20 -17
  110. package/dist/web-admin-client/skills-mcp.js +27 -8
  111. package/dist/web-admin-client/types.d.ts +18 -0
  112. package/package.json +3 -3
@@ -26,9 +26,10 @@
26
26
  * 降级成**结构性事实**(这条路上根本没有 wrapper);
27
27
  * · `headers` 半边从此有出路(wrapper 形只注得进 `apiKey`,core 的座位 `{apiKey, headers?}` 两半都收);
28
28
  * · 判据(`gatewayBaseUrl`)与 resolver 一样**按调用现取** —— 见下面 `createSideQueryLane` 的注。
29
- * `createPerModelKeyBrain` **不删**:它仍是 WebFetch 摘要面(`boot/webfetch-summarize-lane.ts`)的
30
- * key plane —— core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`,那一面没有座位可换
31
- * (亲验 core 5.46.0 `dist/tools/web.d.ts`)。
29
+ * `createPerModelKeyBrain` 当时**没删**:它还是 WebFetch 摘要面(`boot/webfetch-summarize-lane.ts`)的 key plane ——
30
+ * core 的 `WebFetchSummarizerOptions` 那时只有 `maxContentChars`。core 7.51.0 给摘要器补上了同一签名的座
31
+ * (`WebFetchSummarizerOptions.getApiKeyAndHeaders`)⇒ 包装退役,四面(任务 / side-query / 记忆整理 / WebFetch 摘要)
32
+ * 都经 core 的座位供给 per-model 凭据,本仓不再在任何一面包 brain。
32
33
  *
33
34
  * 🔴 **poison(声明的钥匙拿不到)上抛,绝不静默回落网关 key** —— `resolveModelApiKey` 抛
34
35
  * `ModelKeyPoisonedError` 是「这个模型配了自己的钥匙(托管密钥解不开 / S-896 起 `apiKeyEnv` 变量缺席)却拿不到」的响亮态;吞成 undefined 等于用共享
@@ -46,8 +47,9 @@
46
47
  * 是配对法的单一实现,三只一方 brain(openai / anthropic / open-responses)在 `buildRequest` 里逐次
47
48
  * 复判 —— 部署声明了 `config.baseUrl` 而 entry URL 不在那个根上、凭据又只有部署级那一份 ⇒
48
49
  * `route.credential_mismatch` 响亮拒;连部署级凭据都没有 ⇒ `route.credential_missing`。**三面同批**
49
- * (主推理链 / side-query / WebFetch 摘要走的是同一只 brain 栈),旋钮 = 给模型自己的凭据、或不声明
50
- * `config.baseUrl`(core 的 `"unpinned"` 快速起步姿态)。⇒ [ref] 要的「三面同批 + 旋钮」由上游一次交齐。
50
+ * (主推理链 / side-query / WebFetch 摘要走的是同一只 brain 栈),旋钮 = 给模型自己的凭据(当年还有第二条:不声明
51
+ * `config.baseUrl`,走 core 的 `"unpinned"` 快速起步姿态 —— core 7.51.0 #1531 起该姿态退役,一份部署凭据只配一个根)。
52
+ * ⇒ [ref] 要的「三面同批 + 旋钮」由上游一次交齐。
51
53
  *
52
54
  * 于是本席的 `isOffRouteBaseUrl` + `recordFailOpen("server.model-key.off-route-model-uses-gateway-key")`
53
55
  * 两件**一并撤线**:那条计数的语义是「这一次调用真把网关凭据发给了别家」,而在 5.57 上那次调用根本
@@ -97,21 +99,6 @@ export interface SideQueryLaneCtx {
97
99
  export declare function createPerModelAuthSeat(getKeyResolver: () => KeyResolver, opts?: {
98
100
  readonly upstream?: SideQuerySpec["getApiKeyAndHeaders"];
99
101
  }): PerModelAuthSeat;
100
- /**
101
- * per-model key 注入 brain wrapper —— **[ref] 后只服务 WebFetch 摘要面**
102
- * (`boot/webfetch-summarize-lane.ts`)。side-query 面已换到 core 的 spec 座位(见顶注「[ref] 换装」);
103
- * core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`(亲验 5.46.0 `dist/tools/web.d.ts`),
104
- * 那一面**没有座位可换**,只能继续在 brain 层补凭据。
105
- *
106
- * 语义(与 `createPerModelAuthSeat` 同一张表,只是注入位在 `options.apiKey`):
107
- * · `options.apiKey` **已在场** ⇒ 原样透传(上游/装饰器已经定了凭据,本层不抢);
108
- * · resolver 缺席 / 返回 undefined ⇒ 不注入 ⇒ 网关 key(additive 契约);
109
- * · resolver 抛(sealed key 中毒)⇒ **上抛**,本次调用整体失败,一个字节都不发。
110
- *
111
- * `complete` 在场时同样包一层:core 的 `Brain.complete` 是 compaction/摘要腿的直调口(WebFetch 摘要器
112
- * 正是**优先**走它),漏包等于给同一个病留第二条路。
113
- */
114
- export declare function createPerModelKeyBrain(brain: Brain, getKeyResolver: () => KeyResolver): Brain;
115
102
  /**
116
103
  * 装配 side-query 执行席 = 真 `runSideQuery` + core 的 per-model auth 座,models/roles 与 Runner 同源。
117
104
  *
@@ -8,30 +8,6 @@ export function createPerModelAuthSeat(getKeyResolver, opts = {}) {
8
8
  return resolve ? await resolve({ name: model.name }) : undefined;
9
9
  };
10
10
  }
11
- export function createPerModelKeyBrain(brain, getKeyResolver) {
12
- const seat = createPerModelAuthSeat(getKeyResolver);
13
- const keyFor = async (model, given) => {
14
- if (given !== undefined)
15
- return given;
16
- return (await seat(model))?.apiKey;
17
- };
18
- const stream = async (model, context, options) => {
19
- const apiKey = await keyFor(model, options?.apiKey);
20
- return brain.stream(model, context, apiKey === undefined ? options : { ...options, apiKey });
21
- };
22
- const complete = brain.complete?.bind(brain);
23
- return {
24
- stream,
25
- ...(complete
26
- ? {
27
- complete: async (model, context, options) => {
28
- const apiKey = await keyFor(model, options?.apiKey);
29
- return complete(model, context, apiKey === undefined ? options : { ...options, apiKey });
30
- },
31
- }
32
- : {}),
33
- };
34
- }
35
11
  export function createSideQueryLane(ctx) {
36
12
  return (spec) => runSideQuery({
37
13
  ...spec,
@@ -14,8 +14,11 @@ import { setRunTerminalLogObserver } from "../observability/run-terminal.js";
14
14
  import { setRedactionObserver } from "../trace/redact.js";
15
15
  import { installOutboundDispatcher } from "../outbound-dispatcher.js";
16
16
  import { provisionSemaConfigDir } from "../sema-config-dir.js";
17
+ import { bindOwnCredentialsConfig, declareServerWithheldEnv } from "../server-secret-env.js";
17
18
  export function runStage01() {
19
+ declareServerWithheldEnv();
18
20
  const config = loadConfig();
21
+ bindOwnCredentialsConfig(config);
19
22
  const runStaleFailure = crossInvariant("run-stale-heartbeat").check({ live: config, next: config });
20
23
  if (runStaleFailure !== undefined) {
21
24
  throw new Error(runStaleFailure);
@@ -76,7 +76,7 @@ export function runStage06(ctx) {
76
76
  onIncident: (err) => logger.warn("memory_consolidation_incident", { code: err.code ?? null, detail: err.message }),
77
77
  onStopDetail: (audit) => logger.warn("memory_consolidation_stop_detail", audit),
78
78
  ...(sessionCaptureRecordStore !== undefined ? { captureRecordStore: sessionCaptureRecordStore } : {}),
79
- writeJudge: createOperatorValveWriteJudge(),
79
+ writeJudge: createOperatorValveWriteJudge({ ruleStoreProvider: permissionRuleProvider, config }),
80
80
  onNotice: createEngineNoticeForwarder(logger, defaultEngineNoticeRouter, metrics),
81
81
  })
82
82
  : undefined;
@@ -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, applyCenterSkills, 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":
@@ -133,6 +134,7 @@ export async function createWebAdminRuntime(ctx) {
133
134
  ...(config.configProvider ? { provider: config.configProvider } : {}),
134
135
  ...(cc ? { webAdmin: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker, dryRun: cc.dryRun } } : {}),
135
136
  localDir: localRoot,
137
+ ...(config.configLkgEnabled ? { skillBodyCacheDir: defaultSkillCacheDir(config) } : {}),
136
138
  })
137
139
  : undefined;
138
140
  const localDirField = configProvider?.kind === "local" ? { dir: configProvider.root } : {};
@@ -153,16 +155,16 @@ export async function createWebAdminRuntime(ctx) {
153
155
  });
154
156
  }
155
157
  };
156
- const rejectionSource = () => {
158
+ const candidateSource = () => {
157
159
  if (configProvider === undefined)
158
- throw new Error("config candidate judged without a config provider (no pipeline ⇒ nothing was pulled)");
159
- return configProvider.kind;
160
+ throw new Error("config candidate handled without a config provider (no pipeline ⇒ nothing was pulled)");
161
+ return configProvider;
160
162
  };
161
163
  const announceCandidateRejected = (r, verdict, leg, ordinal) => {
162
164
  const domains = verdict.domains.join(",");
163
165
  if (candidateRejection?.version === r.effective.version && candidateRejection.domains.join(",") === domains)
164
166
  return;
165
- noteCandidateVerdict(ordinal, candidateRejectionOf(r, verdict, rejectionSource(), Date.now()));
167
+ noteCandidateVerdict(ordinal, candidateRejectionOf(r, verdict, candidateSource().kind, Date.now()));
166
168
  logger.warn("config_candidate_rejected", buildCandidateRejectedFields(r, verdict, leg, { lkgVersion: latestEffective?.version ?? "(env)", ...localDirField }));
167
169
  };
168
170
  const selfReportListeners = [];
@@ -372,11 +374,15 @@ export async function createWebAdminRuntime(ctx) {
372
374
  }
373
375
  const lkgPath = lkgPathFor(identity);
374
376
  try {
375
- if (effectiveObj.skills && config.webAdmin) {
376
- await applyCenterSkills([], effectiveObj.skills, config.webAdmin, logger, undefined, defaultSkillCacheDir(config));
377
+ const source = candidateSource();
378
+ const cacheDir = source.skillBodyCacheDir;
379
+ if (cacheDir === undefined) {
380
+ logger.warn("config_lkg_save_failed", { err: "the config source declares no skill body cache", note: "LKG NOT written (skill bodies on disk are part of the handoff contract and this source keeps none) — handoff NOT confirmed" });
381
+ return false;
377
382
  }
383
+ if (effectiveObj.skills)
384
+ await applySkillsManifest([], effectiveObj.skills, source, logger);
378
385
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
379
- const cacheDir = defaultSkillCacheDir(config);
380
386
  for (const m of enabledSkills) {
381
387
  const hex = m.contentHash.replace(/^sha256:/, "").toLowerCase();
382
388
  if (!SHA256_HEX_RE.test(hex))
@@ -626,7 +632,7 @@ export async function createWebAdminRuntime(ctx) {
626
632
  if (!dryRun && cc !== undefined && currentIdentity() !== promptStateIdentity)
627
633
  await rebasePromptState(currentIdentity(), "boot-end");
628
634
  }
629
- const readFaceBaseline = { ...(config.readFace !== undefined ? { readFace: config.readFace } : {}), readFaceSource: config.readFaceSource };
635
+ const readFaceBaseline = buildReadFaceBaseline(config);
630
636
  const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined).envHeld;
631
637
  if (!modelReadyState.ready)
632
638
  logger.warn("model_roster_pending", {
@@ -643,18 +649,21 @@ export async function createWebAdminRuntime(ctx) {
643
649
  ...(mcpRevocationWired && mcpRevocations.hasBaseline() ? { mcpRevocationLive: true } : {}),
644
650
  });
645
651
  const applyLiveReadFace = (eff, swap, phase) => {
646
- const before = config.readFace;
647
- const beforeSource = config.readFaceSource;
648
- const { faceChanged } = applyCenterReadFace(config, eff, logger, { phase: "live", baseline: readFaceBaseline });
649
- if (!faceChanged)
652
+ const before = buildReadFaceBaseline(config);
653
+ const { changedSeats } = applyCenterReadFace(config, eff, logger, { phase: "live", baseline: readFaceBaseline });
654
+ if (changedSeats.length === 0)
650
655
  return;
651
656
  try {
652
- swap({ readFace: config.readFace });
653
- 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
+ });
654
664
  }
655
665
  catch (err) {
656
- config.readFace = before;
657
- config.readFaceSource = beforeSource;
666
+ restoreReadFaceBaseline(config, before);
658
667
  throw err;
659
668
  }
660
669
  };
@@ -692,9 +701,8 @@ export async function createWebAdminRuntime(ctx) {
692
701
  return (await manualRefresh()) === "failed" ? "failed" : "triggered";
693
702
  },
694
703
  async applyCenterCapabilities(skills) {
695
- if (effective?.skills && config.webAdmin) {
696
- skills = await applyCenterSkills(skills, effective.skills, config.webAdmin, logger, undefined, config.configLkgEnabled ? defaultSkillCacheDir(config) : undefined);
697
- }
704
+ if (effective?.skills)
705
+ skills = await applySkillsManifest(skills, effective.skills, candidateSource(), logger);
698
706
  if (effective?.plugins !== undefined) {
699
707
  const pluginOut = await applyCenterPlugins(skills, effective, {
700
708
  cacheRoot: join(config.localDataRoot ?? join(homedir(), ".ai-agent"), "plugin-cache"),
@@ -972,6 +980,9 @@ export async function createWebAdminRuntime(ctx) {
972
980
  if (configProvider.kind === "remote")
973
981
  lastPull = "ok";
974
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
+ }
975
986
  finally {
976
987
  refreshInFlight = false;
977
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
@@ -4,7 +4,8 @@ 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, 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, validateRequestMcp, } from "../task-mcp.js";
8
+ import { toCoreMcpServerSpec } from "../mcp-assertion-header.js";
8
9
  import { buildDisplaySafeUrl } from "../trace/redact.js";
9
10
  import { projectWiringManifestMcpRows } from "../trace/project.js";
10
11
  export const MCP_PROBE_REFUSAL_CODES = [
@@ -51,8 +52,12 @@ function entryKeyIssue(entry, at) {
51
52
  if (keys !== undefined)
52
53
  scanKeyClosure(t, `${at}.transport.`, new Set(keys), NO_TOLERATED_KEYS, buckets);
53
54
  }
54
- if (buckets.unknown.length === 0)
55
- return null;
55
+ if (buckets.unknown.length === 0) {
56
+ const refusal = requestEntryIdentityRefusal(entry);
57
+ if (refusal === undefined)
58
+ return null;
59
+ return refusal.errorCode === "request.body_shape" ? shapeRefusal(refusal.message) : fieldRefusal(refusal.message);
60
+ }
56
61
  buckets.unknown.sort();
57
62
  return shapeRefusal(`body: unrecognized key(s) on an MCP declaration — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
58
63
  `${keyClosureClause(buckets.unknown, "unknown (not an mcpServers[] key — check the spelling)")}. ` +
@@ -71,7 +76,8 @@ function entryValueIssue(entry, at) {
71
76
  if (read.badContentOrigin.length > 0) {
72
77
  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)`);
73
78
  }
74
- return declarationDowngradeIssue(entry, read.ok[0], at);
79
+ const read0 = read.ok[0];
80
+ return declarationDowngradeIssue(entry, read0 === undefined ? undefined : toCoreMcpServerSpec(read0), at);
75
81
  }
76
82
  function declarationDowngradeIssue(entry, spec, at) {
77
83
  if (spec === undefined)
@@ -171,7 +177,7 @@ export function readMcpProbeBody(raw) {
171
177
  " so nothing in your body needs changing — if you see it, this deployment has a bug worth reporting"),
172
178
  };
173
179
  }
174
- return { ok: true, specs: validated.ok };
180
+ return { ok: true, specs: validated.ok.map(toCoreMcpServerSpec) };
175
181
  }
176
182
  export function buildMcpProbeIdentity(principal) {
177
183
  return principal !== undefined && principal.length > 0 ? principal : "local";
@@ -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
@@ -432,7 +432,7 @@ export const CONFIG_CATALOG = [
432
432
  r("SESSION_SNAPSHOT_PREFIX", "store", "string", "快照 blob key 前缀", { derivedDefaultNote: "缺席 ⇒ blobs/(MinioBlobBackend 默认)" }),
433
433
  r("SESSION_SNAPSHOT_TTL_SEC", "store", "number", "快照 presign TTL(秒;坏值省略走默认,绝不 NaN 签名)", { derivedDefaultNote: "缺席 ⇒ 3600(MinioBlobBackend 默认)" }),
434
434
  r("SIGHUP_IDLE_GRACE_MS", "limitsHttp", "number", "SIGHUP 空闲关停窗(ms;共享引擎形防误杀)", { staticDefault: "120000", configKey: "sighupIdleGraceMs" }),
435
- r("SKILLS_DIR", "integrations", "path", "出厂 skill 目录", { derivedDefaultNote: "缺席 ⇒ 包内 skills/(相对模块解析,不是 cwd)", configKey: "skillsDir" }),
435
+ r("SKILLS_DIR", "integrations", "path", "出厂 skill 目录(设了就整个替换包内 skills/,不叠加;配置源的技能再按名盖上)", { derivedDefaultNote: "缺席 ⇒ 包内 skills/(相对模块解析,不是 cwd)", configKey: "skillsDir" }),
436
436
  r("SNAPSHOT_BLOB_ALLOW_SQL_BYTES", "store", "boolean", "云形缺 MinIO 时显式接受 SQL 店放快照字节(严格 'true';安全姿势开关不收宽松真值)", { staticDefault: "false", danger: DATA, configKey: "snapshotBlobAllowSql" }),
437
437
  r("SNAPSHOT_BLOB_SQL_MAX_BYTES", "store", "number", "SQL 店单 blob 帽(字节)", { derivedDefaultNote: "缺席:mysql 引擎 ⇒ 内建默认帽;pg ⇒ 不设", configKey: "snapshotBlobSqlMaxBytes" }),
438
438
  r("SSH_HOST", "orchestration", "string", "ssh 车道目标主机(必需件)"),
@@ -78,8 +78,9 @@ export declare function raceBootFetch<T>(fetch: Promise<T>, budgetMs: number): P
78
78
  /**
79
79
  * The two transport calls the service makes to obtain config, behind a backend-selectable seam. Both
80
80
  * signatures and return shapes match `facade.fetchEffective` / `facade.fetchSkillContent`
81
- * EXACTLY so that, ONCE WIRED, the caller (main.ts) could swap remote↔local without touching applyEffective
82
- * et al. (main.ts does not swap on this seam yet — see the NOT-YET-WIRED note at the top of this module).
81
+ * EXACTLY, so every consumer (boot / refresh / late boot in `boot/web-admin.ts`, and `run-local`) runs one code
82
+ * path whichever backend answers: candidates through `fetchEffective`, skill bodies through `fetchSkillContent`
83
+ * (the skills read port `applySkillsManifest`), the body cache wherever the backend declares it.
83
84
  */
84
85
  export interface ConfigProvider {
85
86
  /** "remote" = web-admin HTTP; "local" = on-disk FileConfigStore. */
@@ -92,11 +93,18 @@ export interface ConfigProvider {
92
93
  fetchEffective(etag: string | undefined): Promise<FetchEffectiveResult>;
93
94
  /**
94
95
  * Fetch one skill body by content hash (`sha256:<hex>`). Remote: GET the content endpoint + verify the
95
- * hash. Local: content-addressed lookup over the local skills domain. Returns `undefined` when the hash
96
+ * hash. Local: content-addressed lookup over the skills of the candidate this provider last served (the
97
+ * same read that produced the manifest — S-1556). Returns `undefined` when the hash
96
98
  * is unknown (remote 404 / local miss) so the caller keeps its baseline; throws only on a hard error
97
99
  * (remote non-404 HTTP / hash mismatch).
98
100
  */
99
101
  fetchSkillContent(contentHash: string): Promise<string | undefined>;
102
+ /**
103
+ * S-1556:这只配置源给技能正文留的**盘缓存目录**(content-addressed;`applySkillsManifest` 的读序 内存 → 盘 → 本配置源,读到即回写)。
104
+ * 远程:开着 LKG(`CONFIG_LKG_ENABLED`)时 = 配置根下的 `skill-cache` —— 中心不可达时的 LKG 起服不卡在正文拉取上,LKG 落盘的正文完整性门
105
+ * 也验这同一个目录;关着 = 缺席(不落盘)。本地:恒缺席 —— 正文就内联在本机盘上的 config.d 里,再拷一份是第二份真源。
106
+ */
107
+ readonly skillBodyCacheDir: string | undefined;
100
108
  }
101
109
  /** The config-provider knobs read off ServiceConfig (a structural subset — avoids importing the whole
102
110
  * ServiceConfig type and keeps this module testable with a plain object). */
@@ -117,6 +125,8 @@ export interface ConfigProviderInput {
117
125
  * `process.cwd()`,而工厂只在 `CONFIG_PROVIDER === "local"` 字面上递它 ⇒ 另一条走到 local 的路上配置源读 cwd、
118
126
  * 合成步读 localRoot,两个根分家。一个远程源也收它:`remote` 而无中心 URL 的形照样落回 local。 */
119
127
  localDir: string;
128
+ /** S-1556:远程配置源的技能正文盘缓存目录(见 {@link ConfigProvider.skillBodyCacheDir});本地配置源不认它(正文本就在盘上)。缺席 = 不落盘。 */
129
+ skillBodyCacheDir?: string;
120
130
  }
121
131
  /**
122
132
  * RemoteConfigProvider — DELEGATES to the exported web-admin HTTP functions. No HTTP is reimplemented;
@@ -127,13 +137,14 @@ export interface ConfigProviderInput {
127
137
  export declare class RemoteConfigProvider implements ConfigProvider {
128
138
  private readonly cc;
129
139
  private readonly deps;
140
+ readonly skillBodyCacheDir: string | undefined;
130
141
  readonly kind: "remote";
131
142
  constructor(cc: CenterEndpoint & {
132
143
  worker?: string;
133
144
  }, deps?: {
134
145
  fetchEffective?: typeof remoteFetchEffective;
135
146
  fetchSkillContent?: typeof remoteFetchSkillContent;
136
- });
147
+ }, skillBodyCacheDir?: string | undefined);
137
148
  fetchEffective(etag: string | undefined): Promise<FetchEffectiveResult>;
138
149
  fetchSkillContent(contentHash: string): Promise<string | undefined>;
139
150
  }
@@ -146,6 +157,8 @@ export declare class RemoteConfigProvider implements ConfigProvider {
146
157
  export declare class LocalConfigProvider implements ConfigProvider {
147
158
  #private;
148
159
  readonly kind: "local";
160
+ /** 本地配置源不留正文盘缓存:正文就内联在 config.d 里(同一块盘),缓存它是第二份真源。 */
161
+ readonly skillBodyCacheDir: undefined;
149
162
  private readonly store;
150
163
  /** [ref]b 可观测性:config_loaded 日志要能回答「到底读的哪个目录」——复验实录:只设 LOCAL_DATA_ROOT 时
151
164
  * config.d 静默读不到且 0 模型无告警(CONFIG_LOCAL_DIR 才是本 lane 的选目录旋钮)。 */
@@ -38,10 +38,12 @@ export function raceBootFetch(fetch, budgetMs) {
38
38
  export class RemoteConfigProvider {
39
39
  cc;
40
40
  deps;
41
+ skillBodyCacheDir;
41
42
  kind = "remote";
42
- constructor(cc, deps = {}) {
43
+ constructor(cc, deps = {}, skillBodyCacheDir = undefined) {
43
44
  this.cc = cc;
44
45
  this.deps = deps;
46
+ this.skillBodyCacheDir = skillBodyCacheDir;
45
47
  }
46
48
  async fetchEffective(etag) {
47
49
  const fn = this.deps.fetchEffective ?? remoteFetchEffective;
@@ -68,6 +70,8 @@ export class RemoteConfigProvider {
68
70
  }
69
71
  export class LocalConfigProvider {
70
72
  kind = "local";
73
+ skillBodyCacheDir = undefined;
74
+ #servedSkills = { skills: [] };
71
75
  store;
72
76
  root;
73
77
  #injectedStore;
@@ -115,6 +119,7 @@ export class LocalConfigProvider {
115
119
  const parseErrors = folded.domainErrors.map((w) => ({ domain: w.domain, error: redactConfigError(w.error) }));
116
120
  const droppedEntries = folded.droppedEntries.map(redactDroppedEntry);
117
121
  const errors = [...domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })), ...parseErrors, ...refErrors];
122
+ this.#servedSkills = eff.skills;
118
123
  return {
119
124
  effective: withDroppedCatalogEntries(mapToServiceEffective(eff, eff.version), folded.droppedCatalogEntries),
120
125
  etag: tag,
@@ -126,10 +131,7 @@ export class LocalConfigProvider {
126
131
  };
127
132
  }
128
133
  async fetchSkillContent(contentHash) {
129
- const skills = await this.store.getDomain("skills");
130
- if (!skills)
131
- return undefined;
132
- return findSkillContent(skills, contentHash);
134
+ return findSkillContent(this.#servedSkills, contentHash);
133
135
  }
134
136
  }
135
137
  export function mapToServiceEffective(eff, version) {
@@ -234,7 +236,7 @@ export function mapToServiceEffective(eff, version) {
234
236
  }
235
237
  export function createConfigProvider(input) {
236
238
  return configProviderKind(input) === "remote" && input.webAdmin !== undefined
237
- ? new RemoteConfigProvider(input.webAdmin)
239
+ ? new RemoteConfigProvider(input.webAdmin, {}, input.skillBodyCacheDir)
238
240
  : new LocalConfigProvider(input.localDir);
239
241
  }
240
242
  export function configLocalRoot(config) {
@@ -274,10 +274,10 @@ export interface ScopedMcpServer {
274
274
  spec: McpServerSpec;
275
275
  }
276
276
  /** The deployment's MCP declaration, resolved (`resolveMcpServers`). `servers` = what actually MOUNTED (env-NAME refs
277
- * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` entries skipped, each with its own loud warn);
277
+ * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` / malformed-`toolAxes` entries skipped, each with its own loud warn);
278
278
  * `reservedNames` = EVERY name the deployment declared, mounted or not. A name the deployment declared is CLOSED to
279
279
  * request-side injection (`resolveRequestMcp`) whether or not it mounted — otherwise "skipped" would silently mean
280
- * "free for the caller to refill without the operator's `toolFaces` / `allowTools`" (车CF codex r2 [high]). */
280
+ * "free for the caller to refill without the operator's `toolFaces` / `toolAxes` / `allowTools`" (车CF codex r2 [high]). */
281
281
  export interface CenterMcp {
282
282
  servers: ScopedMcpServer[];
283
283
  reservedNames: readonly string[];
@@ -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. */
@@ -1759,7 +1760,7 @@ export interface ServiceConfigFlat {
1759
1760
  * its own meaning (the sandbox lane's opt-in above) and is unaffected. */
1760
1761
  lspHostEnabled: boolean;
1761
1762
  /** 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
1763
+ * 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
1764
  * (`allowlistedEnv`, the same "named keys pass" predicate as the host shell's `inheritEnv:[names]`) — the deployer
1764
1765
  * explicitly accepts handing that value to code the language server runs. Unnamed secret-shaped keys stay stripped.
1765
1766
  * `[]` = nothing put back (the default: unset / empty string). A malformed list (empty segment / not an env-var name /
@@ -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