@sema-agent/server 7.97.0 → 7.99.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 (70) hide show
  1. package/MIGRATION.md +11 -0
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/USAGE.md +18 -9
  5. package/deploy/sema-up/cluster-up.sh +8 -4
  6. package/deploy/sema-up/kube-up.sh +40 -15
  7. package/deploy/sema-up/lib/envf.sh +241 -0
  8. package/deploy/sema-up/sema-up.sh +162 -101
  9. package/deploy/sema-up/smoke.sh +8 -1
  10. package/dist/boot/config-center.js +169 -24
  11. package/dist/boot/org-memory.d.ts +2 -1
  12. package/dist/boot/org-memory.js +9 -5
  13. package/dist/boot/resolve-spec.js +9 -4
  14. package/dist/boot/runtime-caps.js +4 -4
  15. package/dist/brain.js +28 -18
  16. package/dist/capabilities/center-prompts.d.ts +3 -0
  17. package/dist/capabilities/center-prompts.js +1 -1
  18. package/dist/center-credential.d.ts +112 -0
  19. package/dist/center-credential.js +131 -0
  20. package/dist/config-catalog.js +7 -6
  21. package/dist/config-center/apply-effective.js +7 -1
  22. package/dist/config-center/center-request.d.ts +60 -0
  23. package/dist/config-center/center-request.js +71 -0
  24. package/dist/config-center/facade.d.ts +2 -1
  25. package/dist/config-center/http-client.d.ts +17 -9
  26. package/dist/config-center/http-client.js +27 -36
  27. package/dist/config-center/skills-mcp.d.ts +4 -1
  28. package/dist/config-center/skills-mcp.js +2 -2
  29. package/dist/config-invariants.d.ts +2 -2
  30. package/dist/config-invariants.js +9 -9
  31. package/dist/config-lkg.d.ts +47 -8
  32. package/dist/config-lkg.js +107 -20
  33. package/dist/config-provider.d.ts +8 -7
  34. package/dist/config-provider.js +5 -5
  35. package/dist/config-types.d.ts +20 -6
  36. package/dist/config.d.ts +9 -0
  37. package/dist/config.js +53 -18
  38. package/dist/fleet-client.d.ts +8 -8
  39. package/dist/fleet-client.js +7 -7
  40. package/dist/fleet-lease.d.ts +11 -11
  41. package/dist/fleet-lease.js +17 -13
  42. package/dist/hooks/hook-llm.d.ts +1 -1
  43. package/dist/hooks/hook-llm.js +3 -1
  44. package/dist/hosted-posture.d.ts +49 -93
  45. package/dist/hosted-posture.js +1 -25
  46. package/dist/memory-scope.d.ts +62 -11
  47. package/dist/memory-scope.js +32 -3
  48. package/dist/model-provider.d.ts +13 -0
  49. package/dist/model-provider.js +3 -0
  50. package/dist/model-route-endpoint.d.ts +82 -0
  51. package/dist/model-route-endpoint.js +55 -0
  52. package/dist/observability/secret-env-scrub.d.ts +2 -26
  53. package/dist/observability/secret-env-scrub.js +2 -2
  54. package/dist/plugins/remote-env-host.d.ts +3 -1
  55. package/dist/plugins/remote-env-host.js +3 -2
  56. package/dist/project-identity.d.ts +29 -0
  57. package/dist/project-identity.js +120 -0
  58. package/dist/project-memory.d.ts +28 -1
  59. package/dist/project-memory.js +22 -22
  60. package/dist/run-local.d.ts +14 -4
  61. package/dist/run-local.js +8 -7
  62. package/dist/runtime-caps-resolver.d.ts +8 -8
  63. package/dist/runtime-caps-resolver.js +16 -14
  64. package/dist/sealed-key.js +3 -7
  65. package/dist/server-secret-env.d.ts +41 -0
  66. package/dist/server-secret-env.js +18 -0
  67. package/dist/tool-approval.d.ts +48 -1
  68. package/dist/tool-approval.js +10 -3
  69. package/dist/trace/redact.d.ts +1 -1
  70. package/package.json +4 -3
@@ -10,8 +10,9 @@ import { applyCenterPlugins } from "../capabilities/center-plugins.js";
10
10
  import { applyCatalogToSource, withPromptArtifactBackfill } from "../capabilities/center-prompts.js";
11
11
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
12
12
  import { centerScenarios, centerScenarioDetails } from "../capabilities/scenarios.js";
13
- import { configLkgEnabled } from "../config.js";
14
- import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../config-lkg.js";
13
+ import { configLkgEnabled, isRosterGateArmed } from "../config.js";
14
+ import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg, legacyV1LkgPath, migrateV1Lkg } from "../config-lkg.js";
15
+ import { answerStillCurrent, centerStateBin, credentialIdentityNow } from "../center-credential.js";
15
16
  import { centerManagedConfigKeys } from "../config-center/apply-effective.js";
16
17
  import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../config-provider.js";
17
18
  import { executionLaneCaps, executionLaneOf } from "../execution-lane-caps.js";
@@ -53,6 +54,43 @@ export async function createConfigCenterRuntime(ctx) {
53
54
  let bootSkillStale = false;
54
55
  let pendingRestart;
55
56
  const cc = config.configCenter;
57
+ const bootCredential = cc?.credential.read();
58
+ if (cc !== undefined && bootCredential !== undefined && !bootCredential.ok) {
59
+ logger.error("center_credential_unreadable", {
60
+ key: "SEMA_REGISTRY_TOKEN_FILE",
61
+ reason: bootCredential.reason,
62
+ effect: "every center request fails its own failure arm until the file is readable (config pull keeps env/LKG-less boot, caps fail-closed, lease takes the loud auth-error arm); no on-disk LKG / prompt state is read back (credential identity unknown)",
63
+ });
64
+ }
65
+ const currentIdentity = () => (cc === undefined ? undefined : credentialIdentityNow(cc.credential));
66
+ const candidateIdentity = new WeakMap();
67
+ const candidateIdentityOf = (eff) => candidateIdentity.get(eff);
68
+ const validatorIdentity = new Map();
69
+ const bindValidator = (etag, identity) => {
70
+ if (etag === undefined)
71
+ return;
72
+ validatorIdentity.delete(etag);
73
+ validatorIdentity.set(etag, identity);
74
+ const oldest = validatorIdentity.keys().next();
75
+ if (validatorIdentity.size > 16 && oldest.done !== true)
76
+ validatorIdentity.delete(oldest.value);
77
+ };
78
+ const pullEffective = async (provider, etag) => {
79
+ if (provider.kind === "local")
80
+ return provider.fetchEffective(etag);
81
+ const before = currentIdentity();
82
+ const conditional = etag !== undefined && before !== undefined && validatorIdentity.get(etag) === before ? etag : undefined;
83
+ const r = await provider.fetchEffective(conditional);
84
+ const used = r === null ? before : r.credentialIdentity;
85
+ if (cc === undefined || !answerStillCurrent(cc.credential, used)) {
86
+ throw new Error("center credential identity changed while the effective pull was in flight — response discarded; the next pull runs under the current credential");
87
+ }
88
+ if (r !== null) {
89
+ candidateIdentity.set(r.effective, used);
90
+ bindValidator(r.etag, used);
91
+ }
92
+ return r;
93
+ };
56
94
  let sealedKeys;
57
95
  try {
58
96
  sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
@@ -70,11 +108,11 @@ export async function createConfigCenterRuntime(ctx) {
70
108
  const configProvider = cc || config.configProvider === "local"
71
109
  ? createConfigProvider({
72
110
  ...(config.configProvider ? { provider: config.configProvider } : {}),
73
- ...(cc ? { configCenter: { baseUrl: cc.baseUrl, token: cc.token, worker: cc.worker, dryRun: cc.dryRun } } : {}),
111
+ ...(cc ? { configCenter: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker, dryRun: cc.dryRun } } : {}),
74
112
  ...(config.configProvider === "local" ? { localDir: localRoot } : {}),
75
113
  })
76
114
  : undefined;
77
- const rosterGateArmed = cc !== undefined && !cc.dryRun && process.env.CONFIG_REQUIRE_ROSTER === "true";
115
+ const rosterGateArmed = isRosterGateArmed(cc);
78
116
  const modelReadyState = { ready: !rosterGateArmed };
79
117
  const markRosterLanded = (eff) => {
80
118
  if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
@@ -88,41 +126,106 @@ export async function createConfigCenterRuntime(ctx) {
88
126
  if (!pendingCatalogFaces)
89
127
  return;
90
128
  const { target } = pendingCatalogFaces;
91
- if (await applyCatalogToSource(promptSource, target, centerPrompts, logger, phase)) {
129
+ if (await applyCatalogToSource(catalogSource, target, centerPrompts, logger, phase)) {
92
130
  centerPrompts = target;
93
131
  pendingCatalogFaces = undefined;
94
132
  }
95
133
  };
96
134
  const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
97
- const promptStateDir = join(promptEpochDir, "state", cc?.worker === undefined ? "global" : `worker-${createHash("sha256").update(cc.worker).digest("hex").slice(0, 16)}`);
98
- const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc.baseUrl, cc.token, d), logger) : inner;
135
+ const promptStateRoot = join(promptEpochDir, "state");
136
+ let promptStateIdentity = currentIdentity();
137
+ const promptStateBin = () => (cc === undefined ? "global" : promptStateIdentity === undefined ? undefined : centerStateBin(cc.worker, promptStateIdentity));
138
+ const storePerBin = (make) => {
139
+ const stores = new Map();
140
+ return (bin) => {
141
+ const hit = stores.get(bin);
142
+ if (hit !== undefined)
143
+ return hit;
144
+ const made = make(bin);
145
+ stores.set(bin, made);
146
+ return made;
147
+ };
148
+ };
149
+ const fileStateStoreFor = storePerBin((bin) => new FilePromptSourceStateStore(join(promptStateRoot, bin)));
150
+ const binnedPromptStateStore = (storeFor) => ({
151
+ load: async () => {
152
+ const bin = promptStateBin();
153
+ return bin === undefined ? undefined : storeFor(bin).load();
154
+ },
155
+ save: async (state) => {
156
+ const bin = promptStateBin();
157
+ if (bin === undefined)
158
+ throw new Error("center credential identity unknown (credential unreadable at adoption) — prompt source state not persisted");
159
+ await storeFor(bin).save(state);
160
+ },
161
+ });
162
+ let promptPersisted;
163
+ const recordingStateStore = (inner) => ({
164
+ load: async () => {
165
+ promptPersisted = await inner.load();
166
+ return promptPersisted;
167
+ },
168
+ save: async (state) => {
169
+ await inner.save(state);
170
+ promptPersisted = state;
171
+ },
172
+ });
173
+ const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc, d), logger) : inner;
99
174
  const promptSource = (() => {
100
175
  try {
176
+ const bootBin = promptStateBin();
177
+ if (bootBin !== undefined)
178
+ fileStateStoreFor(bootBin);
101
179
  return new CenterPromptSource({
102
180
  store: withByDigestBackfill(new FilePromptArtifactStore(promptEpochDir)),
103
- stateStore: new FilePromptSourceStateStore(promptStateDir),
181
+ stateStore: recordingStateStore(binnedPromptStateStore(fileStateStoreFor)),
104
182
  engineVersion: CORE_ENGINE_VERSION,
105
183
  });
106
184
  }
107
185
  catch (e) {
108
186
  logger.warn("center_prompts_store_unwritable", { dir: promptEpochDir, err: String(e), note: "file-backed prompt-epoch store unavailable — memory fallback (catalog candidate not restart-durable); built-ins/declaration axis unaffected" });
109
- return new CenterPromptSource({ store: withByDigestBackfill(new MemoryPromptArtifactStore()), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
187
+ return new CenterPromptSource({
188
+ store: withByDigestBackfill(new MemoryPromptArtifactStore()),
189
+ stateStore: recordingStateStore(binnedPromptStateStore(storePerBin(() => new MemoryPromptSourceStateStore()))),
190
+ engineVersion: CORE_ENGINE_VERSION,
191
+ });
110
192
  }
111
193
  })();
112
- if (configProvider && !cc?.dryRun) {
194
+ const catalogSource = {
195
+ applySnapshot: (raw) => promptSource.applySnapshot(raw),
196
+ applyDisabled: (sourceRevision) => promptSource.applyDisabled(sourceRevision),
197
+ current: () => promptSource.current(),
198
+ persistedActive: () => promptPersisted?.state === "active",
199
+ };
200
+ const rebasePromptState = async (identity, phase) => {
201
+ promptStateIdentity = identity;
202
+ centerPrompts = undefined;
203
+ pendingCatalogFaces = undefined;
204
+ promptPersisted = undefined;
113
205
  try {
114
206
  await promptSource.restore();
115
207
  }
116
208
  catch (e) {
117
- logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "disk LKG unavailable — catalog candidate absent until the next adopt; declaration axis and built-ins unaffected" });
209
+ logger.warn("center_prompts_catalog_restore_failed", { phase, err: String(e), note: "the persisted catalog state for this credential identity could not be loaded — catalog candidate absent until the next adopt; declaration axis and built-ins unaffected" });
118
210
  }
119
- }
211
+ if (cc !== undefined && identity !== undefined && currentIdentity() !== identity) {
212
+ promptStateIdentity = undefined;
213
+ await promptSource.restore();
214
+ logger.warn("center_prompts_catalog_identity_changed", { phase, note: "the center credential changed while the persisted catalog state was being read — that state is not used; the current identity's next candidate re-establishes it" });
215
+ }
216
+ };
217
+ if (configProvider && !cc?.dryRun)
218
+ await rebasePromptState(promptStateIdentity, "boot");
120
219
  const adoptCenterPrompts = async (eff, phase) => {
220
+ const adoptedIdentity = candidateIdentityOf(eff);
221
+ if (adoptedIdentity !== undefined && adoptedIdentity !== promptStateIdentity) {
222
+ await rebasePromptState(adoptedIdentity, phase);
223
+ }
121
224
  const raw = eff?.prompts;
122
225
  if (raw === undefined) {
123
226
  if (centerPrompts)
124
227
  logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
125
- const cleared = await applyCatalogToSource(promptSource, undefined, centerPrompts, logger, phase);
228
+ const cleared = await applyCatalogToSource(catalogSource, undefined, centerPrompts, logger, phase);
126
229
  if (cleared) {
127
230
  centerPrompts = undefined;
128
231
  pendingCatalogFaces = undefined;
@@ -148,7 +251,7 @@ export async function createConfigCenterRuntime(ctx) {
148
251
  note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned",
149
252
  });
150
253
  }
151
- const transitioned = await applyCatalogToSource(promptSource, v.value, centerPrompts, logger, phase);
254
+ const transitioned = await applyCatalogToSource(catalogSource, v.value, centerPrompts, logger, phase);
152
255
  if (transitioned) {
153
256
  centerPrompts = v.value;
154
257
  pendingCatalogFaces = undefined;
@@ -159,7 +262,7 @@ export async function createConfigCenterRuntime(ctx) {
159
262
  };
160
263
  let bootConfigPending;
161
264
  const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
162
- const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
265
+ const lkgPathFor = (identity) => process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker, identity);
163
266
  const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
164
267
  let lkgBooted = false;
165
268
  let bootLkgCandidate;
@@ -170,9 +273,15 @@ export async function createConfigCenterRuntime(ctx) {
170
273
  return true;
171
274
  if (!lkgEnabled)
172
275
  return false;
276
+ const identity = candidateIdentityOf(effectiveObj);
277
+ if (identity === undefined) {
278
+ logger.warn("config_lkg_save_failed", { err: "candidate carries no credential identity", note: "LKG NOT written (the bin is keyed by the identity that fetched the candidate) — handoff NOT confirmed" });
279
+ return false;
280
+ }
281
+ const lkgPath = lkgPathFor(identity);
173
282
  try {
174
283
  if (effectiveObj.skills && config.configCenter) {
175
- await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
284
+ await applyCenterSkills([], effectiveObj.skills, config.configCenter, logger, undefined, defaultSkillCacheDir());
176
285
  }
177
286
  const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
178
287
  const cacheDir = defaultSkillCacheDir();
@@ -193,7 +302,7 @@ export async function createConfigCenterRuntime(ctx) {
193
302
  }
194
303
  }
195
304
  lastPrewarmBodiesVerified = true;
196
- await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
305
+ await saveLkg(lkgPath, cc?.worker, identity, effectiveObj, etag);
197
306
  return lkgSurvivesRestart;
198
307
  }
199
308
  catch (err) {
@@ -206,14 +315,40 @@ export async function createConfigCenterRuntime(ctx) {
206
315
  const bootFromLkg = async (why) => {
207
316
  if (!lkgEnabled || dryRun)
208
317
  return null;
209
- const read = await loadLkg(lkgPath, cc?.worker);
318
+ const identity = currentIdentity();
319
+ if (identity === undefined) {
320
+ logger.warn("config_lkg_unusable", { reason: "center credential unreadable — credential identity unknown, no LKG bin is read back", note: "env fallback serves; the live pull retries on the refresh cadence once the credential is readable" });
321
+ return null;
322
+ }
323
+ const lkgPath = lkgPathFor(identity);
324
+ const legacy = await migrateV1Lkg({ legacyPath: process.env.CONFIG_LKG_PATH ?? legacyV1LkgPath(cc?.worker), binPath: lkgPath, worker: cc?.worker, identity });
325
+ switch (legacy.outcome) {
326
+ case "none":
327
+ break;
328
+ case "migrated":
329
+ logger.info("config_lkg_migrated", { from: legacy.from, retiredTo: legacy.retiredTo, to: lkgPath, note: "formatVersion 1 LKG (pull-token identity = this worker) migrated once into this credential identity's bin; the old file was renamed, not deleted" });
330
+ if (legacy.retireError !== undefined)
331
+ logger.warn("config_lkg_legacy_not_retired", { path: legacy.from, reason: legacy.retireError, note: "the bin was written and is used; only the old file's rename / copy failed" });
332
+ break;
333
+ case "not-migrated":
334
+ logger.warn("config_lkg_unusable", { path: legacy.path, reason: legacy.reason, note: "env fallback serves; the cache rewrites on the next clean candidate" });
335
+ return null;
336
+ }
337
+ const read = await loadLkg(lkgPath, cc?.worker, identity);
210
338
  if (!read.lkg) {
211
339
  if (read.reason !== "absent")
212
340
  logger.warn("config_lkg_unusable", { path: lkgPath, reason: read.reason, note: "ignoring the on-disk LKG — env fallback serves; the cache rewrites on the next clean candidate" });
213
341
  return null;
214
342
  }
343
+ if (currentIdentity() !== identity) {
344
+ logger.warn("config_lkg_unusable", { path: lkgPath, reason: "center credential identity changed while the LKG was being read — not used", note: "env fallback serves; the current identity's next pull re-establishes its own LKG" });
345
+ return null;
346
+ }
215
347
  logger.info("config_loaded_lkg", { path: lkgPath, savedAt: read.lkg.savedAt, why, note: "booting from the persisted last-known-good (all restart-to-apply faces included) — the live pull continues in the background and reconciles on arrival" });
216
- return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
348
+ const lkgEffective = read.lkg.effective;
349
+ candidateIdentity.set(lkgEffective, identity);
350
+ bindValidator(read.lkg.etag, identity);
351
+ return { effective: lkgEffective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
217
352
  };
218
353
  const bootApplyLkgInline = async (lkgR) => {
219
354
  const lkgGen = generationOf(lkgR.effective.version);
@@ -231,7 +366,7 @@ export async function createConfigCenterRuntime(ctx) {
231
366
  logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
232
367
  };
233
368
  try {
234
- const bootFetch = configProvider.fetchEffective(undefined);
369
+ const bootFetch = pullEffective(configProvider, undefined);
235
370
  const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
236
371
  let lkgFallback = null;
237
372
  if (raced === BOOT_FETCH_DEFERRED) {
@@ -327,6 +462,8 @@ export async function createConfigCenterRuntime(ctx) {
327
462
  }
328
463
  }
329
464
  }
465
+ if (!dryRun && cc !== undefined && currentIdentity() !== promptStateIdentity)
466
+ await rebasePromptState(currentIdentity(), "boot-end");
330
467
  }
331
468
  const readFaceBaseline = { ...(config.readFace !== undefined ? { readFace: config.readFace } : {}), readFaceSource: config.readFaceSource };
332
469
  const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined).envHeld;
@@ -438,7 +575,7 @@ export async function createConfigCenterRuntime(ctx) {
438
575
  },
439
576
  async applyCenterCapabilities(skills) {
440
577
  if (effective?.skills && config.configCenter) {
441
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
578
+ skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
442
579
  }
443
580
  if (effective?.plugins) {
444
581
  const pluginOut = await applyCenterPlugins(skills, effective, {
@@ -458,7 +595,7 @@ export async function createConfigCenterRuntime(ctx) {
458
595
  }
459
596
  if (bootLkgCandidate && lkgEnabled) {
460
597
  const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
461
- logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
598
+ logger.warn("config_lkg_save_failed", { err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
462
599
  return false;
463
600
  });
464
601
  {
@@ -521,7 +658,7 @@ export async function createConfigCenterRuntime(ctx) {
521
658
  metrics.inc("models_tiers_plane_deferred_stuck_total");
522
659
  logger.warn("models_tiers_deferred_no_handoff", { version: planeDeferredNoHandoff.version, since: planeDeferredNoHandoff.since, note: "candidate still retained without a next-boot handoff — mount restart-surviving LKG storage + CONFIG_LKG_DURABLE=true, or restart manually after ensuring the candidate is re-pulled at boot" });
523
660
  }
524
- const r = arrival !== undefined ? arrival.result : await configProvider.fetchEffective(ccEtag);
661
+ const r = arrival !== undefined ? arrival.result : await pullEffective(configProvider, ccEtag);
525
662
  if (r === null && planeDeferredNoHandoff) {
526
663
  const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
527
664
  const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
@@ -793,7 +930,15 @@ export async function createConfigCenterRuntime(ctx) {
793
930
  };
794
931
  if (bootConfigPending) {
795
932
  const arrivalGen = reserveArrivalGeneration();
796
- void bootConfigPending.then((r) => void (lkgBooted ? runTick({ result: r, ordinal: arrivalGen }) : deferredBootApply(r, arrivalGen)), () => { });
933
+ const continueArrival = (r) => void (lkgBooted ? runTick({ result: r, ordinal: arrivalGen }) : deferredBootApply(r, arrivalGen));
934
+ void bootConfigPending.then((r) => {
935
+ if (r !== null && configProvider.kind === "remote" && candidateIdentityOf(r.effective) !== currentIdentity()) {
936
+ logger.warn("config_boot_arrival_identity_changed", { note: "the late boot pull landed under a center credential that is no longer current — not adopted; re-pulling once under the current credential" });
937
+ void pullEffective(configProvider, undefined).then(continueArrival, (err) => logger.warn("config_boot_arrival_repull_failed", { err: String(err), note: "the refresh cadence retries under the current credential" }));
938
+ return;
939
+ }
940
+ continueArrival(r);
941
+ }, () => { });
797
942
  }
798
943
  }
799
944
  },
@@ -54,7 +54,8 @@ export interface OrgMemoryAdmissionWiring {
54
54
  /** §7 第二消费者收编:同一个目录实例也喂 memory-policy 面的 `org:` 属主门(`ServiceDeps.orgMemoryDirectory`)。
55
55
  * **同实例**是这条收编的全部意义——「谁属于 org:acme」在一个进程里必须只有一个答案,两面共享同一份
56
56
  * TTL 缓存/退避窗/gen 高水位;各建各的目录会让准入门与策略面在吊销窗内公开分歧。缺席=无目录源
57
- * (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。 */
57
+ * (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。
58
+ * 远程腿的这一个实例内部按中心凭证身份分箱([ref] E-15):两面问到的是同一身份的同一箱。 */
58
59
  directory: OrgMemoryDirectory | undefined;
59
60
  }
60
61
  /** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([ref] §1)。 */
@@ -1,4 +1,5 @@
1
1
  import { fetchPrincipalOrgMemory } from "../config-center/http-client.js";
2
+ import { createIdentityBins } from "../center-credential.js";
2
3
  import { createMemoryScopeAdmission, createOrgMemoryDirectory, parseOrgDirectoryStatic } from "../org-memory-admission.js";
3
4
  const ORG_PREFIX = "org:";
4
5
  function collectDeploymentOrgScopes(config) {
@@ -25,11 +26,14 @@ export function createOrgMemoryAdmissionWiring(opts) {
25
26
  });
26
27
  }
27
28
  const directory = centerLeg !== undefined
28
- ? createOrgMemoryDirectory({
29
- fetchSection: (principal) => fetchPrincipalOrgMemory(centerLeg.baseUrl, centerLeg.token, principal, fetch, centerLeg.worker),
30
- grantTtlMs: config.memoryOrgGrantTtlMs,
31
- unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
32
- })
29
+ ? (() => {
30
+ const directoryNow = createIdentityBins(centerLeg.credential, (credential) => createOrgMemoryDirectory({
31
+ fetchSection: (principal) => fetchPrincipalOrgMemory({ baseUrl: centerLeg.baseUrl, credential }, principal, fetch, centerLeg.worker),
32
+ grantTtlMs: config.memoryOrgGrantTtlMs,
33
+ unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
34
+ }));
35
+ return { lookup: (principal) => directoryNow().lookup(principal) };
36
+ })()
33
37
  : staticJson !== undefined
34
38
  ? createOrgMemoryDirectory({
35
39
  staticTable: parseOrgDirectoryStatic(staticJson),
@@ -25,7 +25,7 @@ import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../rou
25
25
  import { gateExecutionLane } from "../runtime-caps-resolver.js";
26
26
  import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
27
27
  import { HttpError, encodeCheckpointScope } from "../security.js";
28
- import { memorySpecForRequest } from "../memory-scope.js";
28
+ import { memoryProjectKeyFor, memorySpecForRequest } from "../memory-scope.js";
29
29
  import { singleMemberOffFieldIssue } from "../request-value-closure.js";
30
30
  import { a2aForScenario, mcpForScenario } from "../config-center/facade.js";
31
31
  import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
@@ -43,6 +43,12 @@ export function createResolveSpec(ctx) {
43
43
  const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
44
44
  assertGuardPatternsUsable(config);
45
45
  const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, { durableEnabled, singleUserAutoAcceptBaseline });
46
+ const realTaskRootOf = (auth) => {
47
+ if (isSandboxPathAdjudicationLane(config.remoteExec?.provider))
48
+ return undefined;
49
+ const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
50
+ return sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
51
+ };
46
52
  if (onlySensitiveBaseline)
47
53
  logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
48
54
  const resolveSpec = async (body, req, auth, opts) => {
@@ -391,7 +397,7 @@ export function createResolveSpec(ctx) {
391
397
  })(),
392
398
  ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
393
399
  memory: memoryEngine
394
- ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
400
+ ? memorySpecForRequest(auth?.memoryScope, memoryProjectKeyFor(config, auth?.memoryScope, realTaskRootOf(auth)), body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
395
401
  : undefined,
396
402
  ...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(effectiveMemoryPersistenceCapable(config)),
397
403
  tools: ((base) => {
@@ -450,8 +456,7 @@ export function createResolveSpec(ctx) {
450
456
  };
451
457
  const pathAdjudication = hostSemanticsLane
452
458
  ? (() => {
453
- const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
454
- const realRoot = sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
459
+ const realRoot = realTaskRootOf(auth);
455
460
  const gateCwd = realRoot ?? join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted");
456
461
  return {
457
462
  env: new NodeExecutionEnv({ cwd: gateCwd }),
@@ -1,4 +1,5 @@
1
- import { applyGovernedVerdictAbsenceWarning, applyLocalAutoModeDeny, applyMemoryOptOutGrant, applyObserverEnvOptIn, centerEntitlementSourceWired, createPrincipalEntitlementsClient, scopedTokenNeedsWorker, } from "../runtime-caps-resolver.js";
1
+ import { applyGovernedVerdictAbsenceWarning, applyLocalAutoModeDeny, applyMemoryOptOutGrant, applyObserverEnvOptIn, centerEntitlementSourceWired, createPrincipalEntitlementsClient, } from "../runtime-caps-resolver.js";
2
+ import { scopedTokenNeedsWorker } from "../center-credential.js";
2
3
  export function assertMemoryCapturePolicyWirable(input) {
3
4
  if (input.policy !== "governed")
4
5
  return;
@@ -36,15 +37,14 @@ export function createRuntimeCaps(ctx) {
36
37
  reason: "MEMORY_CAPTURE_POLICY=governed with NO local memory_optout_grant table: the only verdict source is the config center, and `allowMemoryOptOut` is an OPTIONAL entitlement key — for any principal whose caps omit it the verdict is ABSENT and core refuses that session's memory.capture:\"off\" (memory.capture_optout_denied, fail-closed). ANONYMOUS/system runs are worse off still: the center source is per-principal and is never consulted without an identity, so on this shape a principal-less run can never obtain a verdict at all. Verify the center emits allowMemoryOptOut for every principal, or wire a SQL backend (DB_BACKEND=mysql|pg) so the grant table answers (its deployment-default row also covers anonymous runs).",
37
38
  });
38
39
  }
39
- if (centerEntitlementSourceWired(config) && config.configCenter && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
40
+ if (centerEntitlementSourceWired(config) && config.configCenter && scopedTokenNeedsWorker(config.configCenter.credential.read(), config.configCenter.worker)) {
40
41
  logger.warn("runtime_caps_scoped_token_no_worker", {
41
42
  reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
42
43
  });
43
44
  }
44
45
  const principalCaps = centerEntitlementSourceWired(config) && config.configCenter
45
46
  ? createPrincipalEntitlementsClient({
46
- baseUrl: config.configCenter.baseUrl,
47
- token: config.configCenter.token,
47
+ center: config.configCenter,
48
48
  ...(config.configCenter.worker ? { worker: config.configCenter.worker } : {}),
49
49
  onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
50
50
  })
package/dist/brain.js CHANGED
@@ -2,7 +2,8 @@ import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain,
2
2
  import { routeKeyOf } from "@sema-agent/core";
3
3
  import { buildDefaultResilience } from "./config-types.js";
4
4
  import { resolveModelApiKey } from "./key-resolver.js";
5
- import { catalogBrainLane } from "./model-provider.js";
5
+ import { dispatchBrainLane } from "./model-provider.js";
6
+ import { openaiRouteBaseUrl } from "./model-route-endpoint.js";
6
7
  import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
7
8
  function isOffDeploymentRoot(baseUrl, config) {
8
9
  if (!baseUrl)
@@ -10,11 +11,17 @@ function isOffDeploymentRoot(baseUrl, config) {
10
11
  const entry = routeKeyOf(baseUrl);
11
12
  if (entry === "")
12
13
  return false;
13
- return ![config.gatewayBaseUrl, ...config.gatewayFallbackUrls].some((u) => routeKeyOf(u) === entry);
14
+ return !deploymentGatewayRoots(config).some((u) => routeKeyOf(u) === entry);
15
+ }
16
+ function deploymentGatewayRoots(config) {
17
+ return config.gatewayBaseUrl !== undefined ? [config.gatewayBaseUrl, ...config.gatewayFallbackUrls] : [];
18
+ }
19
+ function deploymentGatewayKey(config) {
20
+ return config.gatewayBaseUrl !== undefined ? config.gatewayApiKey : undefined;
14
21
  }
15
22
  function degradeDeclaredRoot(fallbackBaseUrl, config) {
16
- const armed = Boolean(config.gatewayApiKey) && isOffDeploymentRoot(fallbackBaseUrl, config);
17
- return armed ? config.gatewayBaseUrl : (fallbackBaseUrl || config.gatewayBaseUrl);
23
+ const armed = Boolean(deploymentGatewayKey(config)) && isOffDeploymentRoot(fallbackBaseUrl, config);
24
+ return armed ? config.gatewayBaseUrl : openaiRouteBaseUrl(fallbackBaseUrl, config.gatewayBaseUrl);
18
25
  }
19
26
  export const FAST_FAIL_MAX_RETRIES = 2;
20
27
  function constructRouteBrain(label, sourceUrls, construct) {
@@ -41,12 +48,12 @@ export function createBrain(config, deps = {}) {
41
48
  : fastFailTopology
42
49
  ? { maxRetries: FAST_FAIL_MAX_RETRIES }
43
50
  : {};
44
- const routes = [config.gatewayBaseUrl, ...config.gatewayFallbackUrls];
51
+ const routes = deploymentGatewayRoots(config);
45
52
  const openaiBrains = routes.map((baseUrl, i) => {
46
53
  const brain = constructRouteBrain(i === 0 ? "gateway route" : `gateway fallback route[${i - 1}]`, [baseUrl], () => createOpenAIBrain({
47
54
  baseUrl,
48
55
  get apiKey() {
49
- return config.gatewayApiKey;
56
+ return deploymentGatewayKey(config);
50
57
  },
51
58
  ...retryCap,
52
59
  fetchImpl,
@@ -62,7 +69,11 @@ export function createBrain(config, deps = {}) {
62
69
  })
63
70
  : brain;
64
71
  });
65
- const gatewayBrain = openaiBrains.length > 1 ? createFailoverBrain(openaiBrains) : openaiBrains[0];
72
+ const gatewayBrain = openaiBrains.length === 0
73
+ ? constructRouteBrain("openai route (no gateway declared)", [], () => createOpenAIBrain({ ...retryCap, fetchImpl, ...timeouts }))
74
+ : openaiBrains.length > 1
75
+ ? createFailoverBrain(openaiBrains)
76
+ : openaiBrains[0];
66
77
  const anthropicCfg = config.anthropic;
67
78
  const anthropicBrain = anthropicCfg
68
79
  ? constructRouteBrain("anthropic route", [anthropicCfg.baseUrl], () => createAnthropicBrain({
@@ -86,9 +97,8 @@ export function createBrain(config, deps = {}) {
86
97
  if (fallbackModel) {
87
98
  const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
88
99
  resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
89
- const fallbackLane = catalogBrainLane(fallbackModel.provider);
90
- const anthropicLaneHop = fallbackLane === "anthropic" && anthropicBrain !== undefined;
91
- const fallback = fallbackLane === "anthropic" && anthropicBrain
100
+ const anthropicLaneHop = dispatchBrainLane(fallbackModel.provider, anthropicBrain !== undefined) === "anthropic";
101
+ const fallback = anthropicLaneHop && anthropicBrain
92
102
  ?
93
103
  anthropicBrain
94
104
  : constructRouteBrain(`degrade target "${degradeTo}"`, [fallbackModel.baseUrl, config.gatewayBaseUrl], () => createOpenAIBrain({
@@ -96,7 +106,7 @@ export function createBrain(config, deps = {}) {
96
106
  return degradeDeclaredRoot(fallbackModel.baseUrl, config);
97
107
  },
98
108
  get apiKey() {
99
- return config.gatewayApiKey || undefined;
109
+ return deploymentGatewayKey(config) || undefined;
100
110
  },
101
111
  ...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
102
112
  fetchImpl,
@@ -129,8 +139,10 @@ export function brainSummary(config) {
129
139
  const r = config.resilience ?? DEFAULT_RESILIENCE;
130
140
  const hasFailover = config.gatewayFallbackUrls.length > 0;
131
141
  const breakerActive = r.circuitBreaker && hasFailover;
142
+ const degradeTarget = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
143
+ const degradeEndpoint = degradeTarget ? openaiRouteBaseUrl(degradeTarget.baseUrl, config.gatewayBaseUrl) : undefined;
132
144
  return {
133
- gateway: buildDisplaySafeUrl(config.gatewayBaseUrl),
145
+ gateway: buildDisplaySafeUrl(config.gatewayBaseUrl ?? "") || null,
134
146
  gatewayFallbacks: config.gatewayFallbackUrls.length,
135
147
  failover: hasFailover,
136
148
  anthropicRoute: Boolean(config.anthropic),
@@ -147,14 +159,12 @@ export function brainSummary(config) {
147
159
  : "engine-default",
148
160
  circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
149
161
  reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
150
- reactiveDegradeBaseUrl: config.degrade?.reactive && config.models?.[config.degrade.to]
151
- ? buildDisplaySafeUrl(config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl)
152
- : null,
162
+ reactiveDegradeBaseUrl: buildDisplaySafeUrl(degradeEndpoint ?? "") || null,
153
163
  ...((() => {
154
164
  const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
155
165
  if (!target)
156
166
  return { reactiveDegradeRoutePairing: null, reactiveDegradeRouteRefusal: null };
157
- if (catalogBrainLane(target.provider) === "anthropic" && config.anthropic) {
167
+ if (dispatchBrainLane(target.provider, config.anthropic !== undefined) === "anthropic") {
158
168
  return { reactiveDegradeRoutePairing: "anthropic-lane", reactiveDegradeRouteRefusal: null };
159
169
  }
160
170
  const declaresOwnCredential = config.modelApiKeyEnv?.[target.name] !== undefined ||
@@ -165,12 +175,12 @@ export function brainSummary(config) {
165
175
  if (declaresOwnCredential) {
166
176
  return { reactiveDegradeRoutePairing: "per-model-declared", reactiveDegradeRouteRefusal: null };
167
177
  }
168
- const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential({ ...(config.gatewayApiKey ? { configApiKey: config.gatewayApiKey } : {}) }), { baseUrl: degradeDeclaredRoot(target.baseUrl, config) });
178
+ const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential(((key) => (key ? { configApiKey: key } : {}))(deploymentGatewayKey(config))), { baseUrl: degradeDeclaredRoot(target.baseUrl, config) });
169
179
  return verdict.ok
170
180
  ? { reactiveDegradeRoutePairing: `ok:${verdict.posture}`, reactiveDegradeRouteRefusal: null }
171
181
  : {
172
182
  reactiveDegradeRoutePairing: `broken:${verdict.code}`,
173
- reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config)]),
183
+ reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config) ?? ""]),
174
184
  };
175
185
  })()),
176
186
  };
@@ -55,6 +55,9 @@ export declare function applyCatalogToSource(source: {
55
55
  /** codex-2 F2(restart-owed disable):撤下写盘失败后重启,内存 prev 丢了但持久态还 active——
56
56
  * 以 source 自身的 live candidate 为准判「欠 disable」,不是只看内存 prev。 */
57
57
  current?(): unknown;
58
+ /** S-651(codex 修复验证轮 V1):本箱**持久记录**是 active —— 即使那份 artifact 此刻加载不出(活候选为空)。
59
+ * 「欠撤下」的真源是持久记录;活候选只是它能加载时的影子。缺席 = 调用方不掌握持久记录(沿用前两条判据)。 */
60
+ persistedActive?(): boolean;
58
61
  }, next: PromptsDomainFaces | undefined, prev: PromptsDomainFaces | undefined, log: {
59
62
  info: (msg: string, meta?: Record<string, unknown>) => void;
60
63
  warn: (msg: string, meta?: Record<string, unknown>) => void;
@@ -55,7 +55,7 @@ export async function applyCatalogToSource(source, next, prev, log, phase) {
55
55
  log.warn("center_prompts_catalog_apply_failed", { phase, code: r.code, reason: r.reason, note: "validate passed but the source apply failed (disk/state) — declaration axis and built-ins unaffected; candidate unchanged; the transition retries on the next refresh tick" });
56
56
  return false;
57
57
  }
58
- if (prev?.catalog || source.current?.() !== undefined) {
58
+ if (prev?.catalog || source.current?.() !== undefined || source.persistedActive?.() === true) {
59
59
  await source.applyDisabled(`${phase}:catalog-absent`);
60
60
  log.info("center_prompts_catalog_disabled", { phase, note: "catalog axis withdrawn — core candidate cleared (does not revive offline)" });
61
61
  }