@sema-agent/server 7.102.0 → 7.103.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 (77) hide show
  1. package/MIGRATION.md +91 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/USAGE.md +107 -6
  5. package/dist/background-session-reap.d.ts +4 -1
  6. package/dist/bake-runner/main.d.ts +21 -3
  7. package/dist/bake-runner/main.js +18 -11
  8. package/dist/boot/budget-tracing.d.ts +1 -0
  9. package/dist/boot/budget-tracing.js +3 -2
  10. package/dist/boot/config-center.d.ts +12 -2
  11. package/dist/boot/config-center.js +33 -21
  12. package/dist/boot/stage-01-config.js +2 -0
  13. package/dist/boot/stage-04-stores.d.ts +1 -0
  14. package/dist/boot/stage-04-stores.js +2 -2
  15. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  16. package/dist/boot/stage-06-runners.d.ts +1 -0
  17. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  18. package/dist/boot/stage-08-reapers.d.ts +1 -0
  19. package/dist/boot/stage-08-reapers.js +2 -2
  20. package/dist/boot/stage-09-leader.d.ts +1 -0
  21. package/dist/boot/stage-10-http-server.d.ts +1 -0
  22. package/dist/boot/stage-10-http-server.js +6 -1
  23. package/dist/brain.d.ts +60 -9
  24. package/dist/brain.js +44 -8
  25. package/dist/capabilities/center-plugins.d.ts +9 -3
  26. package/dist/capabilities/center-plugins.js +5 -10
  27. package/dist/config-catalog.js +9 -5
  28. package/dist/config-center/apply-effective.d.ts +40 -2
  29. package/dist/config-center/apply-effective.js +32 -12
  30. package/dist/config-center/facade.d.ts +1 -1
  31. package/dist/config-center/facade.js +1 -1
  32. package/dist/config-center/mcp-revocation.d.ts +2 -2
  33. package/dist/config-center/mcp-revocation.js +1 -1
  34. package/dist/config-center/restart-signal.d.ts +39 -12
  35. package/dist/config-center/restart-signal.js +13 -24
  36. package/dist/config-types.d.ts +35 -17
  37. package/dist/config-types.js +3 -3
  38. package/dist/config.d.ts +7 -0
  39. package/dist/config.js +43 -17
  40. package/dist/env-name-allowlist-knobs.d.ts +21 -7
  41. package/dist/env-name-allowlist-knobs.js +7 -2
  42. package/dist/fleet-client.d.ts +9 -3
  43. package/dist/fleet-client.js +2 -0
  44. package/dist/host-lsp-manager.d.ts +4 -3
  45. package/dist/host-lsp-manager.js +2 -6
  46. package/dist/http/dispatch.js +4 -0
  47. package/dist/http/server.d.ts +4 -0
  48. package/dist/leader/wire.d.ts +8 -1
  49. package/dist/leader/wire.js +4 -3
  50. package/dist/model-entry-refusal.d.ts +32 -9
  51. package/dist/model-entry-refusal.js +22 -10
  52. package/dist/model-route-endpoint.d.ts +10 -0
  53. package/dist/model-route-endpoint.js +6 -2
  54. package/dist/observability/secret-env-scrub.d.ts +13 -1
  55. package/dist/observability/secret-env-scrub.js +15 -2
  56. package/dist/outbound-dispatcher.d.ts +125 -0
  57. package/dist/outbound-dispatcher.js +211 -0
  58. package/dist/plugins/host-platform.d.ts +6 -1
  59. package/dist/plugins/remote-env-adb.d.ts +10 -3
  60. package/dist/plugins/remote-env-adb.js +24 -20
  61. package/dist/plugins/remote-env-host.js +3 -11
  62. package/dist/plugins/remote-env-k8s.d.ts +8 -1
  63. package/dist/plugins/remote-env-k8s.js +31 -24
  64. package/dist/plugins/remote-env-ssh.d.ts +8 -1
  65. package/dist/plugins/remote-env-ssh.js +24 -18
  66. package/dist/plugins/remote-shell.d.ts +109 -9
  67. package/dist/plugins/remote-shell.js +83 -0
  68. package/dist/project-memory.js +3 -6
  69. package/dist/run-local.js +2 -0
  70. package/dist/server-secret-env.d.ts +89 -24
  71. package/dist/server-secret-env.js +21 -1
  72. package/dist/trace/core-keyset-guard.d.ts +6 -1
  73. package/dist/trace/project.js +13 -0
  74. package/dist/trace/redact.d.ts +11 -0
  75. package/dist/trace/redact.js +1 -0
  76. package/package.json +4 -3
  77. package/skills/update-config.md +23 -5
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { loadRemoteExec } from "@sema-agent/settings-schema/node";
6
6
  import { CENTER_CREDENTIAL_ENV, skillContentHash } from "@sema-agent/settings-schema";
7
+ import { stableStringify } from "@sema-agent/settings-schema/hash";
8
+ import { buildModelRefusalsSignal } from "../model-entry-refusal.js";
7
9
  import { CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, } from "@sema-agent/core";
8
10
  import { buildPricing } from "../budget.js";
9
11
  import { applyCenterPlugins } from "../capabilities/center-plugins.js";
@@ -19,7 +21,7 @@ import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../con
19
21
  import { executionLaneCaps, executionLaneOf } from "../execution-lane-caps.js";
20
22
  import { createKeyResolver } from "../key-resolver.js";
21
23
  import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
22
- import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, buildEnvOnlyModelPlane, fetchPromptArtifact } from "../config-center/facade.js";
24
+ import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, withModelPlaneOf, bootSnapshotOf, fullyLandedForLkg, fetchPromptArtifact } from "../config-center/facade.js";
23
25
  import { createMcpRevocationLedger, enabledMcpEntries } from "../config-center/mcp-revocation.js";
24
26
  import { SHA256_HEX_RE } from "../digest-form.js";
25
27
  import { createConfigApplyLedger, modelPlaneCommitted, refusedGroups } from "../config-center/apply-ledger.js";
@@ -62,6 +64,7 @@ export async function createConfigCenterRuntime(ctx) {
62
64
  let planeDeferredNoHandoff;
63
65
  let bootSkillStale = false;
64
66
  let pendingRestart;
67
+ let modelRefusals;
65
68
  const cc = config.configCenter;
66
69
  const bootCredential = cc?.credential.read();
67
70
  const bootVerdict = bootCredential === undefined ? undefined : judgeCenterCredentialAtBoot(bootCredential);
@@ -162,6 +165,16 @@ export async function createConfigCenterRuntime(ctx) {
162
165
  if (!same)
163
166
  notifySelfReport("restart_signal");
164
167
  };
168
+ const publishModelRefusals = (next) => {
169
+ const same = stableStringify(modelRefusals ?? null) === stableStringify(next ?? null);
170
+ modelRefusals = next;
171
+ if (!same)
172
+ notifySelfReport("model_refusals");
173
+ };
174
+ const recordApply = (rep) => {
175
+ applyLedger.recordApply(rep);
176
+ publishModelRefusals(buildModelRefusalsSignal(rep.modelRefusals));
177
+ };
165
178
  let centerPrompts;
166
179
  let pendingCatalogFaces;
167
180
  const retryPendingCatalog = async (phase) => {
@@ -402,7 +415,7 @@ export async function createConfigCenterRuntime(ctx) {
402
415
  generation: lkgGen,
403
416
  report: (rep) => {
404
417
  lkgReport = rep;
405
- applyLedger.recordApply(rep);
418
+ recordApply(rep);
406
419
  },
407
420
  });
408
421
  applyLedger.applied(lkgGen, lkgR.effective.version);
@@ -410,12 +423,14 @@ export async function createConfigCenterRuntime(ctx) {
410
423
  const lkgPlane = modelPlaneCommitted(lkgReport);
411
424
  if (lkgPlane)
412
425
  markRosterLanded(lkgR.effective);
413
- effective = lkgR.effective;
426
+ effective = bootSnapshotOf(lkgR.effective, lkgPlane ? lkgR.effective : undefined);
414
427
  if (lkgPlane)
415
428
  appliedPlaneEff = lkgR.effective;
416
- latestEffective = lkgR.effective;
417
- if (lkgR.etag !== undefined)
418
- ccEtag = lkgR.etag;
429
+ if (fullyLandedForLkg(lkgR, lkgReport)) {
430
+ latestEffective = lkgR.effective;
431
+ if (lkgR.etag !== undefined)
432
+ ccEtag = lkgR.etag;
433
+ }
419
434
  const cfgModels = (lkgR.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
420
435
  logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
421
436
  };
@@ -450,15 +465,12 @@ export async function createConfigCenterRuntime(ctx) {
450
465
  const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
451
466
  if (r) {
452
467
  bootApplyPhase = true;
453
- const bootPromptsRaw = r.effective.prompts;
454
- const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
455
- let bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
456
468
  for (const de of r.domainErrors ?? [])
457
469
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
458
470
  warnDroppedEntries(r, "boot");
459
471
  if (dryRun) {
460
472
  logEffectiveDiff(config, r.effective, logger);
461
- if (bootClean)
473
+ if (fullyLandedForLkg(r, undefined))
462
474
  ccEtag = r.etag;
463
475
  }
464
476
  else {
@@ -471,12 +483,11 @@ export async function createConfigCenterRuntime(ctx) {
471
483
  generation: bootGen,
472
484
  report: (rep) => {
473
485
  bootReport = rep;
474
- applyLedger.recordApply(rep);
486
+ recordApply(rep);
475
487
  },
476
488
  });
477
489
  applyLedger.applied(bootGen, r.effective.version);
478
- if (bootReport?.fullyApplied === false)
479
- bootClean = false;
490
+ const bootClean = fullyLandedForLkg(r, bootReport);
480
491
  if (bootClean)
481
492
  ccEtag = r.etag;
482
493
  await adoptCenterPrompts(r.effective, "boot");
@@ -494,7 +505,7 @@ export async function createConfigCenterRuntime(ctx) {
494
505
  }
495
506
  if (bootPlaneEff !== undefined)
496
507
  markRosterLanded(bootPlaneEff);
497
- effective = bootPlaneEff === r.effective ? r.effective : withModelPlaneOf(r.effective, bootPlaneEff ?? buildEnvOnlyModelPlane());
508
+ effective = bootSnapshotOf(r.effective, bootPlaneEff);
498
509
  if (bootPlaneEff !== undefined)
499
510
  appliedPlaneEff = bootPlaneEff;
500
511
  if (bootClean)
@@ -553,8 +564,8 @@ export async function createConfigCenterRuntime(ctx) {
553
564
  let keyResolver;
554
565
  const mcpRevocations = createMcpRevocationLedger((ev) => logger.info("mcp_revocation_ledger", { ...(ev.revoked.length > 0 ? { revoked: ev.revoked } : {}), ...(ev.restored.length > 0 ? { restored: ev.restored } : {}) }));
555
566
  let mcpRevocationWired = false;
556
- const restartCtx = (consolidationSeat) => ({
557
- ...(config.degrade?.reactive ? { reactiveDegradeTo: config.degrade.to } : {}),
567
+ const restartCtx = (consolidationSeat, degradeRoute) => ({
568
+ degradeRoute,
558
569
  pluginsAllowHosts: config.pluginsAllowHosts,
559
570
  ...(consolidationSeat !== undefined ? { consolidationSeat } : {}),
560
571
  ...(readFaceEnvHeld.length > 0 ? { readFaceEnvHeld } : {}),
@@ -592,6 +603,7 @@ export async function createConfigCenterRuntime(ctx) {
592
603
  selfReportListeners.push(listener);
593
604
  },
594
605
  restartState: () => pendingRestart,
606
+ modelRefusalsState: () => modelRefusals,
595
607
  planeDeferredState: () => planeDeferredNoHandoff,
596
608
  configApplyState: () => (configProvider ? applyLedger.view() : undefined),
597
609
  centerManagedKeys: () => (configProvider ? centerManagedConfigKeys(latestEffective) : undefined),
@@ -724,7 +736,7 @@ export async function createConfigCenterRuntime(ctx) {
724
736
  }
725
737
  },
726
738
  startRefreshLoop(a) {
727
- const { pricing, limitSync, swapRunnerDeps, ceilingVerdict, consolidationSeat, checkpointStorePresent } = a;
739
+ const { pricing, limitSync, swapRunnerDeps, ceilingVerdict, consolidationSeat, degradeRoute, checkpointStorePresent } = a;
728
740
  if (configProvider) {
729
741
  const ccRef = config.configCenter;
730
742
  let refreshInFlight = false;
@@ -800,7 +812,7 @@ export async function createConfigCenterRuntime(ctx) {
800
812
  generation: tickGen,
801
813
  report: (rep) => {
802
814
  applyReport = rep;
803
- applyLedger.recordApply(rep);
815
+ recordApply(rep);
804
816
  },
805
817
  ...(planeChanged ? { swapPlane: swapRunnerDeps } : {}),
806
818
  });
@@ -833,7 +845,7 @@ export async function createConfigCenterRuntime(ctx) {
833
845
  return "ok";
834
846
  }
835
847
  const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
836
- const reasons = restartReasons(effective, r.effective, restartCtx(consolidationSeat?.read()));
848
+ const reasons = restartReasons(effective, r.effective, restartCtx(consolidationSeat?.read(), degradeRoute.read()));
837
849
  const skillsChangeDetected = reasons.includes("skills");
838
850
  if (bootSkillStale && !skillsChangeDetected)
839
851
  reasons.push("skills");
@@ -932,7 +944,7 @@ export async function createConfigCenterRuntime(ctx) {
932
944
  generation: lateGen,
933
945
  report: (rep) => {
934
946
  applyReportLate = rep;
935
- applyLedger.recordApply(rep);
947
+ recordApply(rep);
936
948
  },
937
949
  ...(planeChangedLate ? { swapPlane: swapRunnerDeps } : {}),
938
950
  });
@@ -967,7 +979,7 @@ export async function createConfigCenterRuntime(ctx) {
967
979
  latestEffective = r.effective;
968
980
  ccEtag = r.etag;
969
981
  const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag);
970
- const reasons = restartReasons(undefined, r.effective, restartCtx(consolidationSeat?.read()));
982
+ const reasons = restartReasons(undefined, r.effective, restartCtx(consolidationSeat?.read(), degradeRoute.read()));
971
983
  if (lkgSurvivesRestart && !lkgPersistedLate) {
972
984
  const blocked = reasons.splice(0, reasons.length);
973
985
  planeDeferredNoHandoff = { version: r.effective.version, since: Date.now(), blocked, candidate: r.effective, ...(r.etag !== undefined ? { candidateEtag: r.etag } : {}) };
@@ -12,6 +12,7 @@ import { installFileHistoryFailureRecorder } from "../file-history-disclosure.js
12
12
  import { installSecretEnvScrubRecorder } from "../observability/secret-env-scrub.js";
13
13
  import { setRunTerminalLogObserver } from "../observability/run-terminal.js";
14
14
  import { setRedactionObserver } from "../trace/redact.js";
15
+ import { installOutboundDispatcher } from "../outbound-dispatcher.js";
15
16
  export function runStage01() {
16
17
  const config = loadConfig();
17
18
  const runStaleFailure = crossInvariant("run-stale-heartbeat").check({ live: config, next: config });
@@ -56,6 +57,7 @@ export function runStage01() {
56
57
  note: "LOCAL_DATA_ROOT moved the local backend's DATA root, but config.d / remote-exec.json are still read from the config root (LOCAL_DATA_ROOT is not consulted there). If you meant to move both, set CONFIG_LOCAL_DIR to the same directory.",
57
58
  });
58
59
  }
60
+ installOutboundDispatcher(logger);
59
61
  return { config, localRoot, logger, metrics };
60
62
  }
61
63
  //# sourceMappingURL=stage-01-config.js.map
@@ -2,6 +2,7 @@ import type { runStage03 } from "./stage-03-store-backend.js";
2
2
  export declare function runStage04(ctx: Awaited<ReturnType<typeof runStage03>>): Promise<{
3
3
  brain: import("@sema-agent/core").Brain;
4
4
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
5
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
5
6
  counterDegradeHook: (info: {
6
7
  table: string;
7
8
  kind: string;
@@ -27,7 +27,7 @@ export async function runStage04(ctx) {
27
27
  if (adoptionBoot.scanned > 0 || adoptionBoot.error !== undefined) {
28
28
  logger.info("adoption_boot_scan", { ...adoptionBoot });
29
29
  }
30
- const { brain, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, fileHistoryErasure, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
30
+ const { brain, degradeRouteWatch, pricing, counterDegradeHook, costQuota, modelUsageTracker, promptManifestTracker, fleetUsage, fleetLease, tracer, sideQueryAccounting, toolResultStore, sessionPolicyStore, fileHistoryStore, fileHistoryErasure, } = createBudgetAndTracing({ config, logger, metrics, backend, breakerState });
31
31
  const permissionRuleStores = config.permissionRulesEnabled && backend ? backend.permissionRule() : undefined;
32
32
  const permissionRuleProvider = permissionRuleStores
33
33
  ? createPermissionRuleStoreProvider({ durable: permissionRuleStores.durable })
@@ -44,6 +44,6 @@ export async function runStage04(ctx) {
44
44
  });
45
45
  }
46
46
  await auditDormantPermissionRules({ stores: permissionRuleStores, logger, explicit: config.permissionRulesEnabledExplicit });
47
- return { ...ctx, brain, costQuota, counterDegradeHook, fileHistoryErasure, fileHistoryStore, fleetLease, fleetUsage, memoryBundleFaces, memoryComplianceFaces, memoryOriginFaces, modelUsageTracker, permissionRuleProvider, permissionRuleStores, pricing, promptManifestTracker, ruleConsent, sessionMemoryStatusFace, sessionPolicyStore, sideQueryAccounting, toolResultStore, tracer };
47
+ return { ...ctx, brain, costQuota, degradeRouteWatch, counterDegradeHook, fileHistoryErasure, fileHistoryStore, fleetLease, fleetUsage, memoryBundleFaces, memoryComplianceFaces, memoryOriginFaces, modelUsageTracker, permissionRuleProvider, permissionRuleStores, pricing, promptManifestTracker, ruleConsent, sessionMemoryStatusFace, sessionPolicyStore, sideQueryAccounting, toolResultStore, tracer };
48
48
  }
49
49
  //# sourceMappingURL=stage-04-stores.js.map
@@ -15,6 +15,7 @@ export declare function runStage05(ctx: Awaited<ReturnType<typeof runStage04>>):
15
15
  worktreeReap: (() => Promise<void>) | undefined;
16
16
  brain: import("@sema-agent/core").Brain;
17
17
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
18
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
18
19
  counterDegradeHook: (info: {
19
20
  table: string;
20
21
  kind: string;
@@ -76,6 +76,7 @@ export declare function runStage06(ctx: Awaited<ReturnType<typeof runStage05>>):
76
76
  worktreeReap: (() => Promise<void>) | undefined;
77
77
  brain: import("@sema-agent/core").Brain;
78
78
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
79
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
79
80
  counterDegradeHook: (info: {
80
81
  table: string;
81
82
  kind: string;
@@ -90,6 +90,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
90
90
  worktreeReap: (() => Promise<void>) | undefined;
91
91
  brain: import("@sema-agent/core").Brain;
92
92
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
93
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
93
94
  counterDegradeHook: (info: {
94
95
  table: string;
95
96
  kind: string;
@@ -103,6 +103,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
103
103
  worktreeReap: (() => Promise<void>) | undefined;
104
104
  brain: import("@sema-agent/core").Brain;
105
105
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
106
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
106
107
  counterDegradeHook: (info: {
107
108
  table: string;
108
109
  kind: string;
@@ -5,7 +5,7 @@ import { startRetentionLane, RETENTION_LEASE_TTL_FACTOR } from "./retention-lane
5
5
  import { createLimitSync } from "./limit-sync.js";
6
6
  import { runDenySweep } from "./late-binds.js";
7
7
  export function runStage08(ctx) {
8
- const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
8
+ const { approvalNonceStore, backend, backgroundAgentStore, ceilingVerdict, checkpointStore, config, configCenter, consolidationSeatWatch, costQuota, degradeRouteWatch, fileHistoryErasure, handslessRunner, handslessSubRunner, hookAgentRunner, imageBakes, instanceId, localRoot, logger, mailboxStore, metrics, permissionRuleStores, pricing, rateLimiter, retentionExecutor, retentionLaneStore, rosterStore, runStore, runner, sqlWorkflowRunStore, subRunner, taskAttachmentStore, toolApproval, toolResultStore, workflowJournalStore, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, worktreeReap } = ctx;
9
9
  const reaper = startReapers({
10
10
  config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
11
11
  rateLimiter, costQuota, toolResultStore, fileHistoryErasure, taskAttachmentStore, imageBakes, worktreeReap,
@@ -71,7 +71,7 @@ export function runStage08(ctx) {
71
71
  }
72
72
  }
73
73
  };
74
- configCenter.startRefreshLoop({ pricing, limitSync, swapRunnerDeps, ceilingVerdict, checkpointStorePresent: checkpointStore !== undefined, ...(consolidationSeatWatch !== undefined ? { consolidationSeat: consolidationSeatWatch } : {}) });
74
+ configCenter.startRefreshLoop({ pricing, limitSync, swapRunnerDeps, ceilingVerdict, checkpointStorePresent: checkpointStore !== undefined, degradeRoute: degradeRouteWatch, ...(consolidationSeatWatch !== undefined ? { consolidationSeat: consolidationSeatWatch } : {}) });
75
75
  configCenter.initKeyResolver();
76
76
  return { ...ctx, otelExporter, reaper, retentionLaneStarted };
77
77
  }
@@ -137,6 +137,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
137
137
  worktreeReap: (() => Promise<void>) | undefined;
138
138
  brain: import("@sema-agent/core").Brain;
139
139
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
140
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
140
141
  counterDegradeHook: (info: {
141
142
  table: string;
142
143
  kind: string;
@@ -137,6 +137,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
137
137
  worktreeReap: (() => Promise<void>) | undefined;
138
138
  brain: import("@sema-agent/core").Brain;
139
139
  costQuota: import("../observability/cost-quota.js").CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
140
+ degradeRouteWatch: import("../brain.js").DegradeRouteWatch;
140
141
  counterDegradeHook: (info: {
141
142
  table: string;
142
143
  kind: string;
@@ -1,6 +1,6 @@
1
1
  import { autoRunOnRecommendationEffective, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { posIntEnv } from "../session-watch.js";
3
- import { brainSummary } from "../brain.js";
3
+ import { brainSummary, buildFailoverFirstByteNotice } from "../brain.js";
4
4
  import { resolveBindHost } from "../config.js";
5
5
  import { buildWriteProtectionPosture } from "../write-protection.js";
6
6
  import { isPricingConfigured } from "../budget.js";
@@ -151,6 +151,7 @@ export async function runStage10(ctx) {
151
151
  planeDeferredState: () => configCenter.planeDeferredState(),
152
152
  configApplyState: () => configCenter.configApplyState(),
153
153
  centerCredentialState: () => configCenter.centerCredentialState(),
154
+ modelRefusalsState: () => configCenter.modelRefusalsState(),
154
155
  mcpRevocations,
155
156
  mcpRevocationState: () => configCenter.mcpRevocationState(),
156
157
  configRefreshNow: () => configCenter.refreshNow(),
@@ -241,12 +242,16 @@ export async function runStage10(ctx) {
241
242
  version: serviceVersion(),
242
243
  readiness: () => readinessOf(deployment),
243
244
  restart: () => deployment.restartState?.(),
245
+ modelRefusals: () => deployment.modelRefusalsState?.(),
244
246
  ...(fleetUsage ? { usage: fleetUsage } : {}),
245
247
  logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}) },
246
248
  });
247
249
  configCenter.onSelfReportChange(() => void fleetClient?.announceNow());
248
250
  if (fleetClient)
249
251
  logger.info("fleet_client_started", { worker: config.configCenter?.worker, address: buildDisplaySafeUrl(process.env.FLEET_ADVERTISE_ADDRESS ?? "") });
252
+ const failoverFirstByte = buildFailoverFirstByteNotice(config);
253
+ if (failoverFirstByte)
254
+ logger.warn("failover_first_byte_lane", failoverFirstByte);
250
255
  logger.info("listening", {
251
256
  port: config.port,
252
257
  bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
package/dist/brain.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type Brain, type BreakerState } from "@sema-agent/core";
1
+ import { type Brain, type BreakerState, type Model } from "@sema-agent/core";
2
2
  import type { ServiceConfig } from "./config-types.js";
3
+ import { type RestartSliceCtx } from "./config-center/restart-signal.js";
3
4
  /**
4
5
  * 配了快切层(failover ≥2 路 / 断路器)时,**每路**的重试上限。
5
6
  *
@@ -12,12 +13,14 @@ export declare const FAST_FAIL_MAX_RETRIES = 2;
12
13
  * Build the external "Brain" (LLM) from config.
13
14
  *
14
15
  * Composition (A1 + the 1.38 resilience stack, ordered timeout-innermost → breaker → failover-outermost):
15
- * - **Tiered timeouts** (1.38, `connectTimeoutMs`/`firstTokenTimeoutMs`) are passed into each
16
- * openai-compatible brain at creation — they bound "gateway never accepts the connection" and
17
- * "stream opened but hangs without a token" as retryable `[network]` failures. Default ON
18
- * (connect 30s per the fleet-audit recommendation; first-token 600s since S-202 — a self-hosted backend's long
19
- * prefill legitimately takes minutes to the first byte, and 120s turned that into a retry handing the
20
- * same prompt back to the same queue. env =0 disables; env =120000 restores the old posture).
16
+ * - **Tiered timeouts** (1.38; re-cut by core 7.33.0 along the segments of a call) are passed into each
17
+ * brain at creation: `firstByteTimeoutMs` (fetch() → response headers; S-753: **passed only when the
18
+ * operator set `MODEL_FIRST_BYTE_TIMEOUT_MS`** — absent = no key, core's own default, which carries an
19
+ * upload allowance and one widened retry of its own), `firstTokenTimeoutMs` (headers → first delta,
20
+ * 600s since S-202 — a self-hosted backend's long prefill legitimately takes minutes, and 120s turned that
21
+ * into a retry handing the same prompt back to the same queue) and `idleTimeoutMs`. The TCP + TLS connect
22
+ * is the transport's own clock (core 7.33.0), not a knob here. env =0 = no engine clock on that segment
23
+ * and the transport's header / body clocks off too (the connect keeps the transport's own clock).
21
24
  * - **Circuit breaker** (`createCircuitBreakerBrain`) wraps the *primary* gateway route(s) when
22
25
  * `MODEL_CIRCUIT_BREAKER=true`: after N transient failures it opens and fast-fails, so failover
23
26
  * switches to the bare backup immediately instead of waiting out each timeout. The last gateway
@@ -30,14 +33,62 @@ export declare const FAST_FAIL_MAX_RETRIES = 2;
30
33
  * ↔ vLLM is a *routing* choice, not failover.)
31
34
  *
32
35
  * When no fallback URL, no Anthropic key, and no resilience thresholds are configured, this returns the
33
- * bare openai brain (carrying the default-on watchdog timeouts — set the three env knobs to 0 for the
34
- * pre-[ref] fully-bare shape). The Model itself (id / contextWindow / maxTokens / provider) flows
36
+ * bare openai brain (carrying the watchdogs: core's own first-byte default unless `MODEL_FIRST_BYTE_TIMEOUT_MS`
37
+ * is set, plus the server's first-token / idle defaults; a knob at 0 = no engine clock on that segment and the
38
+ * transport's header / body clocks off too). The Model itself (id / contextWindow / maxTokens / provider) flows
35
39
  * per-request through `Runner({ models })`.
36
40
  */
37
41
  export declare function createBrain(config: ServiceConfig, deps?: {
38
42
  fetchImpl?: typeof fetch;
39
43
  breakerState?: BreakerState;
40
44
  }): Brain;
45
+ /** {@link degradeRouteFingerprint} 读的那几张表(全是活引用:refresh 就地换代,读数每次现取)。 */
46
+ export type DegradeRoutePlane = Pick<ServiceConfig, "degrade" | "models" | "gatewayBaseUrl" | "modelApiKeyEnv" | "modelApiKeys">;
47
+ /**
48
+ * S-794:反应式降级壳焊的是哪一只目标 —— **唯一判据**(createBrain 组壳、启动摘要、`degrade-route` 片的两侧读数同读这一只)。
49
+ * 车道没开(非 reactive)或表里没有叫 `degrade.to` 的条目 ⇒ `undefined`(createBrain 不装降级壳)。
50
+ */
51
+ export declare function degradeTargetOf(config: Pick<ServiceConfig, "degrade" | "models">): Model | undefined;
52
+ /**
53
+ * S-794:降级壳的**路由指纹**(纯数据)—— `degrade-route` 重启片 `baked` / `current` 两侧读数的唯一读法。
54
+ *
55
+ * 壳在 createBrain 那一刻焊死目标那只 `Model` 对象(env 派生或目录派生):请求打 `model.baseUrl`(缺席回网关根)、body 带
56
+ * `model.id` / `maxTokens` / `compat` / …,走哪条腿看 `provider`;refresh 就地换的是表里的**值**,够不到那只对象。于是指纹 =
57
+ * · 那只 Model 经 `catalogEntryFingerprint`(与 swap 门同一张热字段减法:单价**数值**热、在场性不热;其余字段一律算 —— deny-list,
58
+ * 漂移的失败形是多签一次,绝不漏签);
59
+ * · 有效端点根(S-623 同一只解析器 `openaiRouteBaseUrl(baseUrl, gatewayBaseUrl)`);
60
+ * · 它配的凭据**身份**:`apiKeyEnv`(变量名,不是值)与托管密钥**在场位**(不取值:值是热的,且是秘密)。两次查表的键与
61
+ * `fbOwnApiKey` 同(目标的真名,再 `degrade.to`)。
62
+ * 没有焊壳(车道没开 / 没有目标)⇒ `null`。在场 ↔ 缺席的翻转因此自然是一次变化。
63
+ */
64
+ export declare function degradeRouteFingerprint(config: DegradeRoutePlane): unknown;
65
+ /** `degrade-route` 片的席:boot 焊进去的那一只 vs 活配置此刻的同一只目标(`restartReasons` 的 `degradeRoute` 两侧读数)。 */
66
+ export interface DegradeRouteWatch {
67
+ read(): NonNullable<RestartSliceCtx["degradeRoute"]>;
68
+ }
69
+ /**
70
+ * `create*`:返回带捕获状态(组壳那一刻的指纹)的活对象。**须在 {@link createBrain} 的同一时刻、同一份 config 上调**
71
+ * (生产:boot/budget-tracing.ts 紧随 createBrain)—— 否则 `baked` 就不是壳真焊进去的那一只。与 boot/memory-consolidation.ts
72
+ * `createConsolidationSeatWatch` 同形。
73
+ */
74
+ export declare function createDegradeRouteWatch(config: DegradeRoutePlane): DegradeRouteWatch;
75
+ /**
76
+ * S-753 复审(opus F2 + codex C1)—— **failover 拓扑 × 首字节段没有部署窗**的启动期披露(纯数据,`build*`;告警由 boot 出)。
77
+ *
78
+ * 缺省下首字节段不铸键(core 自缺省当家,[ref])、`0` = 该段不设引擎钟;而切备只发生在一次调用**失败之后**。于是主网关接了连接
79
+ * 却不回响应头时:缺省 ⇒ 要等 core 的首字节 lane 走完(缺省首窗 + 一次加宽重试;本仓不铸 `firstByteRetries`,S-753 车PA)才切备,
80
+ * 时间预算短于那一段的调用在备网关被尝试之前就被取消 —— 以 core 7.33.x 缺省计 lane ≈ 15 min,code-review council 每个镜头的 180 s 与 team
81
+ * discuss 的 600 s 都在其内(车PA codex r1:600 s 在主网关的加宽重试窗里到点,不是「只剩余量」);
82
+ * `0` ⇒ 首字节 lane 永不切备。启动日志里同行的 `gatewayRetryCap:"fast-fail:…"` 只管共享重试梯子,读起来却像「会快切」—— 这条披露把它补齐。
83
+ * 显式设了正数窗 ⇒ `undefined`(部署已表态);单路由 ⇒ `undefined`(没有备可切)。**不替运维铸窗**:出路是旋钮,不是本仓的数。
84
+ */
85
+ export declare function buildFailoverFirstByteNotice(config: ServiceConfig): {
86
+ readonly knob: "MODEL_FIRST_BYTE_TIMEOUT_MS";
87
+ readonly fallbacks: number;
88
+ readonly firstByteTimeoutMs: number | null;
89
+ readonly effect: string;
90
+ readonly fix: string;
91
+ } | undefined;
41
92
  /** One-line summary of the active brain composition, for the startup log. */
42
93
  export declare function brainSummary(config: ServiceConfig): Record<string, unknown>;
43
94
  //# sourceMappingURL=brain.d.ts.map
package/dist/brain.js CHANGED
@@ -4,6 +4,7 @@ import { buildDefaultResilience } from "./config-types.js";
4
4
  import { resolveModelApiKey } from "./key-resolver.js";
5
5
  import { dispatchBrainLane } from "./model-provider.js";
6
6
  import { openaiRouteBaseUrl } from "./model-route-endpoint.js";
7
+ import { catalogEntryFingerprint } from "./config-center/restart-signal.js";
7
8
  import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
8
9
  function isOffDeploymentRoot(baseUrl, config) {
9
10
  if (!baseUrl)
@@ -38,7 +39,7 @@ export function createBrain(config, deps = {}) {
38
39
  const { fetchImpl, breakerState } = deps;
39
40
  const r = config.resilience ?? DEFAULT_RESILIENCE;
40
41
  const timeouts = {
41
- connectTimeoutMs: r.connectTimeoutMs,
42
+ ...(r.firstByteTimeoutMs !== undefined ? { firstByteTimeoutMs: r.firstByteTimeoutMs } : {}),
42
43
  firstTokenTimeoutMs: r.firstTokenTimeoutMs,
43
44
  idleTimeoutMs: r.idleTimeoutMs,
44
45
  };
@@ -93,7 +94,7 @@ export function createBrain(config, deps = {}) {
93
94
  : gatewayBrain;
94
95
  if (config.degrade?.reactive) {
95
96
  const degradeTo = config.degrade.to;
96
- const fallbackModel = config.models[degradeTo];
97
+ const fallbackModel = degradeTargetOf(config);
97
98
  if (fallbackModel) {
98
99
  const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
99
100
  resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
@@ -135,11 +136,46 @@ export function createBrain(config, deps = {}) {
135
136
  }
136
137
  return brain;
137
138
  }
139
+ export function degradeTargetOf(config) {
140
+ return config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
141
+ }
142
+ export function degradeRouteFingerprint(config) {
143
+ const target = degradeTargetOf(config);
144
+ if (target === undefined || config.degrade === undefined)
145
+ return null;
146
+ const to = config.degrade.to;
147
+ return {
148
+ model: catalogEntryFingerprint(target),
149
+ endpoint: openaiRouteBaseUrl(target.baseUrl, config.gatewayBaseUrl) ?? null,
150
+ apiKeyEnv: config.modelApiKeyEnv?.[target.name] ?? config.modelApiKeyEnv?.[to] ?? null,
151
+ sealedKey: config.modelApiKeys?.[target.name] !== undefined || config.modelApiKeys?.[to] !== undefined,
152
+ };
153
+ }
154
+ export function createDegradeRouteWatch(config) {
155
+ const baked = degradeRouteFingerprint(config);
156
+ return { read: () => ({ baked, current: degradeRouteFingerprint(config) }) };
157
+ }
158
+ export function buildFailoverFirstByteNotice(config) {
159
+ const r = config.resilience ?? DEFAULT_RESILIENCE;
160
+ const fallbacks = config.gatewayFallbackUrls.length;
161
+ if (fallbacks === 0 || (r.firstByteTimeoutMs !== undefined && r.firstByteTimeoutMs > 0))
162
+ return undefined;
163
+ const zero = r.firstByteTimeoutMs === 0;
164
+ return {
165
+ knob: "MODEL_FIRST_BYTE_TIMEOUT_MS",
166
+ fallbacks,
167
+ firstByteTimeoutMs: r.firstByteTimeoutMs ?? null,
168
+ effect: zero
169
+ ? "the first-byte lane never fails over a primary that accepts the connection but sends no response headers (0 = no engine clock on that segment); only a run-level limit ends that wait"
170
+ : "a primary that accepts the connection but sends no response headers is failed over only after core's default first-byte lane runs out (the first window, then one widened retry); a call with a time budget shorter than that — the code-review council's 180 s lenses and team discuss's 600 s, with core 7.33.x defaults — is cancelled before the backup is ever tried",
171
+ fix: "set MODEL_FIRST_BYTE_TIMEOUT_MS to the longest response-header wait a healthy primary needs (queue + prefill), so a hung primary is cut over after that window and its one widened retry",
172
+ };
173
+ }
138
174
  export function brainSummary(config) {
139
175
  const r = config.resilience ?? DEFAULT_RESILIENCE;
140
176
  const hasFailover = config.gatewayFallbackUrls.length > 0;
141
177
  const breakerActive = r.circuitBreaker && hasFailover;
142
- const degradeTarget = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
178
+ const degradeTarget = degradeTargetOf(config);
143
179
  const degradeEndpoint = degradeTarget ? openaiRouteBaseUrl(degradeTarget.baseUrl, config.gatewayBaseUrl) : undefined;
144
180
  return {
145
181
  gateway: buildDisplaySafeUrl(config.gatewayBaseUrl ?? "") || null,
@@ -147,9 +183,9 @@ export function brainSummary(config) {
147
183
  failover: hasFailover,
148
184
  anthropicRoute: Boolean(config.anthropic),
149
185
  anthropicCacheBreakpoints: config.anthropic?.cacheBreakpoints ?? null,
150
- connectTimeoutMs: r.connectTimeoutMs || null,
151
- firstTokenTimeoutMs: r.firstTokenTimeoutMs || null,
152
- idleTimeoutMs: r.idleTimeoutMs || null,
186
+ firstByteTimeoutMs: r.firstByteTimeoutMs ?? null,
187
+ firstTokenTimeoutMs: r.firstTokenTimeoutMs,
188
+ idleTimeoutMs: r.idleTimeoutMs,
153
189
  circuitBreaker: breakerActive,
154
190
  circuitBreakerRequested: r.circuitBreaker,
155
191
  gatewayRetryCap: config.gatewayMaxRetries !== undefined
@@ -158,10 +194,10 @@ export function brainSummary(config) {
158
194
  ? `fast-fail:${FAST_FAIL_MAX_RETRIES}`
159
195
  : "engine-default",
160
196
  circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
161
- reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
197
+ reactiveDegradeTo: degradeTarget !== undefined ? (config.degrade?.to ?? null) : null,
162
198
  reactiveDegradeBaseUrl: buildDisplaySafeUrl(degradeEndpoint ?? "") || null,
163
199
  ...((() => {
164
- const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
200
+ const target = degradeTarget;
165
201
  if (!target)
166
202
  return { reactiveDegradeRoutePairing: null, reactiveDegradeRouteRefusal: null };
167
203
  if (dispatchBrainLane(target.provider, config.anthropic !== undefined) === "anthropic") {
@@ -39,14 +39,19 @@ export type PluginValidation = {
39
39
  };
40
40
  /** 条款 1/2/4:https-only、host 全等白名单、sha 40hex。纯函数,worker 侧纵深复验(不信任 wire 已验过)。 */
41
41
  export declare function validatePluginDecl(decl: PluginDeclWire, allowHosts: readonly string[]): PluginValidation;
42
- /** 条款 3/5:clone 计划(argv 序列 + 隔离 env)。纯数据,让"姿势"可被锚定。 */
42
+ /**
43
+ * 条款 3:clone 计划(argv 序列)。纯数据,让"步骤"可被锚定。
44
+ *
45
+ * 7.103.0(S-684 残余):计划**只**产 argv。隔离 env(收紧③:GIT_CONFIG_GLOBAL/SYSTEM=/dev/null、GIT_TERMINAL_PROMPT=0)的唯一属主是
46
+ * `gitSubprocessEnv({ isolateConfig: true })` —— 修前这三键写在这里、由 `runGit` 叠在 `{ ...process.env }` 上,一件姿势两个写者,
47
+ * 而剥密恰好落在两者之外。锚点随之移到真 spawn 的 env 上(`test/secret-env-scrub-host-legs.test.ts` R2:假 git 读到的整份 env)。
48
+ */
43
49
  export declare function gitClonePlan(o: {
44
50
  url: string;
45
51
  dest: string;
46
52
  sha?: string;
47
53
  }): {
48
54
  steps: string[][];
49
- env: Record<string, string>;
50
55
  };
51
56
  /** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
52
57
  * 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
@@ -101,6 +106,7 @@ export declare function applyCenterPlugins(baseline: LoadedSkill[], eff: {
101
106
  /** 测试口:跳过 validate(materialize 只接受已验声明;真 git 腿用本地 bare repo,validate 层的
102
107
  * https-only 单独测——两层分开,谁也不掩护谁)。生产恒 false/缺席。 */
103
108
  skipValidate?: boolean;
104
- runGit?: (args: string[], env: Record<string, string>) => Promise<void>;
109
+ /** 测试口:替换真 git(生产恒缺席)。env 不在这一格上 —— 缺省腿自己经唯一构造取(替身腿本来就不起 git)。 */
110
+ runGit?: (args: string[]) => Promise<void>;
105
111
  }): Promise<ApplyPluginsResult>;
106
112
  //# sourceMappingURL=center-plugins.d.ts.map
@@ -4,6 +4,7 @@ import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, sta
4
4
  import { join, resolve, isAbsolute, sep } from "node:path";
5
5
  import { assertConfined, loadSkills } from "./skills.js";
6
6
  import { buildDisplaySafeUrl, redactSecrets } from "../trace/redact.js";
7
+ import { gitSubprocessEnv } from "../server-secret-env.js";
7
8
  const execFileP = promisify(execFile);
8
9
  export const MAX_PLUGIN_SOURCE_URL_CHARS = 2048;
9
10
  export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
@@ -53,13 +54,8 @@ export function validatePluginDecl(decl, allowHosts) {
53
54
  return { ok: true, name, url };
54
55
  }
55
56
  export function gitClonePlan(o) {
56
- const env = {
57
- GIT_CONFIG_GLOBAL: "/dev/null",
58
- GIT_CONFIG_SYSTEM: "/dev/null",
59
- GIT_TERMINAL_PROMPT: "0",
60
- };
61
57
  if (o.sha === undefined) {
62
- return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]], env };
58
+ return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]] };
63
59
  }
64
60
  return {
65
61
  steps: [
@@ -67,7 +63,6 @@ export function gitClonePlan(o) {
67
63
  ["-C", o.dest, "fetch", "--depth", "1", "--quiet", o.url, o.sha],
68
64
  ["-C", o.dest, "checkout", "--quiet", o.sha],
69
65
  ],
70
- env,
71
66
  };
72
67
  }
73
68
  export const PLUGIN_MANIFEST_MAX_BYTES = 64 * 1024;
@@ -130,8 +125,8 @@ export async function applyCenterPlugins(baseline, eff, opts) {
130
125
  const out = { skills: [...baseline], failures: [], letGo: [], lkgUsed: [] };
131
126
  if (declared.length === 0)
132
127
  return out;
133
- const runGit = opts.runGit ?? (async (args, env) => {
134
- await execFileP("git", args, { env: { ...process.env, ...env }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
128
+ const runGit = opts.runGit ?? (async (args) => {
129
+ await execFileP("git", args, { env: gitSubprocessEnv({ site: "server.center-plugins.git-clone", isolateConfig: true }), timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
135
130
  });
136
131
  const taken = new Set(baseline.map((s) => s.spec.name));
137
132
  for (const decl of declared) {
@@ -155,7 +150,7 @@ export async function applyCenterPlugins(baseline, eff, opts) {
155
150
  await fsp.mkdir(staging, { recursive: true });
156
151
  const plan = gitClonePlan({ url: v.url, dest: staging, sha: v.sha });
157
152
  for (const step of plan.steps)
158
- await runGit(step, plan.env);
153
+ await runGit(step);
159
154
  await fsp.rm(liveDir, { recursive: true, force: true });
160
155
  await fsp.mkdir(join(opts.cacheRoot, v.name), { recursive: true });
161
156
  await fsp.rename(staging, liveDir);