@sema-agent/server 7.98.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 (50) hide show
  1. package/MIGRATION.md +6 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +12 -9
  5. package/deploy/sema-up/lib/envf.sh +110 -42
  6. package/deploy/sema-up/sema-up.sh +76 -34
  7. package/dist/boot/config-center.js +167 -22
  8. package/dist/boot/org-memory.d.ts +2 -1
  9. package/dist/boot/org-memory.js +9 -5
  10. package/dist/boot/runtime-caps.js +4 -4
  11. package/dist/capabilities/center-prompts.d.ts +3 -0
  12. package/dist/capabilities/center-prompts.js +1 -1
  13. package/dist/center-credential.d.ts +112 -0
  14. package/dist/center-credential.js +131 -0
  15. package/dist/config-catalog.js +3 -2
  16. package/dist/config-center/center-request.d.ts +60 -0
  17. package/dist/config-center/center-request.js +71 -0
  18. package/dist/config-center/http-client.d.ts +17 -9
  19. package/dist/config-center/http-client.js +27 -36
  20. package/dist/config-center/skills-mcp.d.ts +4 -1
  21. package/dist/config-center/skills-mcp.js +2 -2
  22. package/dist/config-invariants.d.ts +2 -2
  23. package/dist/config-invariants.js +0 -8
  24. package/dist/config-lkg.d.ts +47 -8
  25. package/dist/config-lkg.js +107 -20
  26. package/dist/config-provider.d.ts +8 -7
  27. package/dist/config-provider.js +5 -5
  28. package/dist/config-types.d.ts +8 -4
  29. package/dist/config.js +11 -13
  30. package/dist/fleet-client.d.ts +8 -8
  31. package/dist/fleet-client.js +7 -7
  32. package/dist/fleet-lease.d.ts +11 -11
  33. package/dist/fleet-lease.js +17 -13
  34. package/dist/hosted-posture.d.ts +49 -93
  35. package/dist/hosted-posture.js +1 -25
  36. package/dist/observability/secret-env-scrub.d.ts +2 -26
  37. package/dist/observability/secret-env-scrub.js +2 -2
  38. package/dist/plugins/remote-env-host.d.ts +3 -1
  39. package/dist/plugins/remote-env-host.js +3 -2
  40. package/dist/project-memory.d.ts +28 -1
  41. package/dist/project-memory.js +22 -22
  42. package/dist/run-local.js +0 -2
  43. package/dist/runtime-caps-resolver.d.ts +8 -8
  44. package/dist/runtime-caps-resolver.js +16 -14
  45. package/dist/sealed-key.js +3 -7
  46. package/dist/server-secret-env.d.ts +41 -0
  47. package/dist/server-secret-env.js +18 -0
  48. package/dist/tool-approval.d.ts +48 -1
  49. package/dist/tool-approval.js +10 -3
  50. package/package.json +4 -3
@@ -11,7 +11,8 @@ import { applyCatalogToSource, withPromptArtifactBackfill } from "../capabilitie
11
11
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
12
12
  import { centerScenarios, centerScenarioDetails } from "../capabilities/scenarios.js";
13
13
  import { configLkgEnabled, isRosterGateArmed } from "../config.js";
14
- import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../config-lkg.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,7 +108,7 @@ 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;
@@ -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),
@@ -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
  })
@@ -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
  }
@@ -0,0 +1,112 @@
1
+ /** 凭证种类闭集。新增一个种类 = 本文件与 `config-center/center-request.ts` 的穷举 switch 当场编译红。 */
2
+ export type CenterCredentialKind = "pull-token" | "user-jwt" | "none";
3
+ /** 凭证在哪里声明(env 形带值 —— 只在本模块内流转)。 */
4
+ export type CenterCredentialDeclaration = {
5
+ readonly from: "env";
6
+ readonly token: string;
7
+ } | {
8
+ readonly from: "file";
9
+ readonly path: string;
10
+ } | {
11
+ readonly from: "absent";
12
+ };
13
+ /** 声明形的**非秘密**投影(进日志 / 拒因 / 未来的 caps 读面):env 形不带值。 */
14
+ export type CenterCredentialOrigin = {
15
+ readonly from: "env";
16
+ } | {
17
+ readonly from: "file";
18
+ readonly path: string;
19
+ } | {
20
+ readonly from: "absent";
21
+ };
22
+ /** 一次取值的结果。`ok:false` 只有文件形会出现(env / absent 形的取值恒成功)。 */
23
+ export type CenterCredentialRead = {
24
+ readonly ok: true;
25
+ readonly kind: "pull-token";
26
+ readonly bearer: string;
27
+ readonly identity: string;
28
+ readonly workerScoped: boolean;
29
+ } | {
30
+ readonly ok: true;
31
+ readonly kind: "user-jwt";
32
+ readonly bearer: string;
33
+ readonly identity: string;
34
+ } | {
35
+ readonly ok: true;
36
+ readonly kind: "none";
37
+ readonly identity: string;
38
+ } | {
39
+ readonly ok: false;
40
+ readonly reason: string;
41
+ };
42
+ /** 健康态:最近一次取值的结果(caps `leaseHealth` 之类的读面读它,不自己再判)。 */
43
+ export type CenterCredentialHealth = {
44
+ readonly state: "ok";
45
+ } | {
46
+ readonly state: "unreadable";
47
+ readonly reason: string;
48
+ readonly since: number;
49
+ readonly failures: number;
50
+ };
51
+ export interface CenterCredentialSource {
52
+ /** 声明形(非秘密):`{from:"file", path}` 进日志与拒因用。 */
53
+ readonly origin: CenterCredentialOrigin;
54
+ /** 装配判据:声明了凭证(env 值非空 / 文件路径在场)。**按声明装,不按内容装** —— 文件此刻不可读也算声明了。 */
55
+ readonly declared: boolean;
56
+ /** 每次出站请求调用一次;文件形每次都读文件。 */
57
+ read(): CenterCredentialRead;
58
+ health(): CenterCredentialHealth;
59
+ }
60
+ /** 凭证文件此刻不可读 / 为空。中心请求装配点(`center-request.ts`)取值失败时抛它,各消费方按自己的
61
+ * 失败臂处置(拉配置保 LKG、caps fail-closed、租约走「鉴权失败」响亮臂 …),**不发一个空 bearer 出去**。 */
62
+ export declare class CenterCredentialUnavailableError extends Error {
63
+ readonly reason: string;
64
+ constructor(reason: string);
65
+ }
66
+ export declare const CENTER_TOKEN_ENV = "SEMA_REGISTRY_TOKEN";
67
+ export declare const CENTER_TOKEN_FILE_ENV = "SEMA_REGISTRY_TOKEN_FILE";
68
+ /**
69
+ * 读两个声明键。两个都非空 ⇒ throw(boot 拒启,拒因点名二选一)。空串 = 未设(与 7.98.0 之前
70
+ * `SEMA_REGISTRY_TOKEN || ""` 的读法一致)。文件路径在这里解析成绝对路径(之后 cwd 变了也读同一个文件)。
71
+ */
72
+ export declare function readCenterCredentialDeclaration(env?: NodeJS.ProcessEnv): CenterCredentialDeclaration;
73
+ /** 拉取令牌形的身份 = worker(拉取令牌代表 worker,不是人)。种类判别与 formatVersion 1 LKG 的一次性迁移
74
+ * (`config-lkg.ts` `migrateV1Lkg`:v1 信封只记 worker,只有这一形的身份推得出来)共用这一只。 */
75
+ export declare function pullTokenIdentity(worker: string | undefined): string;
76
+ /**
77
+ * 凭证来源工厂(活对象:文件形每次取值读盘并记健康态)。`worker` = `SEMA_REGISTRY_WORKER`(拉取令牌形的身份)。
78
+ * `readFile` 只为测试注入。
79
+ */
80
+ export declare function createCenterCredentialSource(declaration: CenterCredentialDeclaration, worker: string | undefined, deps?: {
81
+ readFile?: (path: string) => string;
82
+ now?: () => number;
83
+ }): CenterCredentialSource;
84
+ /** 需要作用域 worker 的拉取令牌(`wpt_<self>`)却没配 `SEMA_REGISTRY_WORKER` ⇒ 中心对 per-principal caps
85
+ * 逐次 403(caps fail-closed 拒全部 workflow)。boot 期一次性诊断用;判据只读本模块的种类判别。 */
86
+ export declare function scopedTokenNeedsWorker(read: CenterCredentialRead, worker: string | undefined): boolean;
87
+ /**
88
+ * [ref] E-15 —— 按「worker + 凭证身份」分箱的**唯一键形**(LKG 文件名、prompt 活动状态目录共用)。
89
+ * 两段都取摘要:worker 名 / 身份任意字节都成单段路径,无 dot-segment,无撞形(codex-5 当年给 prompt 状态
90
+ * 目录定的姿势,本批推广到 LKG)。
91
+ */
92
+ export declare function centerStateBin(worker: string | undefined, identity: string): string;
93
+ /** 此刻凭证的身份(每次现读);文件形此刻不可读 ⇒ `undefined`(身份未知)。「我现在是谁」只有这一只读法。 */
94
+ export declare function credentialIdentityNow(source: CenterCredentialSource): string | undefined;
95
+ /**
96
+ * [ref] E-15 ② —— 一次中心往返的答复**仍属于此刻的凭证**:发出它所用的身份 = 此刻的身份。
97
+ * 拉配置(`boot/config-center.ts` `pullEffective`)与两条 `?principal=` 视图(`config-center/http-client.ts`)
98
+ * 都只问这一只;不成立 ⇒ 答复丢弃,当这次拉取失败(各走各的失败臂)。
99
+ */
100
+ export declare function answerStillCurrent(source: CenterCredentialSource, usedIdentity: string | undefined): usedIdentity is string;
101
+ /**
102
+ * [ref] E-15 · 内存面 —— 身份绑定的进程内状态(per-principal caps 缓存与在飞表、org 记忆目录)的**唯一分箱规则**,
103
+ * 与盘上的 {@link centerStateBin} 同一条规则的两面(7.99.0 合并复审 F2:此前两处缓存只按 principal 键)。
104
+ * · 箱 = 取箱那一刻的凭证身份(惰建)。凭证此刻不可读 ⇒ 「身份未知」箱。
105
+ * · 每个箱只经**钉在它身份上**的凭证来源对中心说话(`make` 拿到的那只):此刻的凭证已换了人 / 不可读 ⇒ 这一箱的
106
+ * 请求不发(取值失败 ⇒ `CenterCredentialUnavailableError`,消费方走自己的失败臂)。身份未知箱因此从不收任何
107
+ * 中心答复;缓存 / 在飞 / 条件请求的校验子全在箱里,换了人结构上够不着上一个人的。
108
+ * · 请求发出之后才换人:答复到达时由 {@link answerStillCurrent} 判丢弃(请求装配侧,见 http-client.ts)。
109
+ * 旧身份的箱不淘汰(量级 = 进程生命期内出现过的身份数):换回来的身份在 TTL 内照常命中它自己的缓存。
110
+ */
111
+ export declare function createIdentityBins<T>(source: CenterCredentialSource, make: (pinned: CenterCredentialSource) => T): () => T;
112
+ //# sourceMappingURL=center-credential.d.ts.map