@sema-agent/server 1.220.0 → 1.222.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.
package/dist/main.js CHANGED
@@ -18,7 +18,7 @@ import { withLedgerRecording } from "./plugins/send-file-ledger.js";
18
18
  import { basename, resolve } from "node:path";
19
19
  import { stat as fsStat, readFile as fsReadFile } from "node:fs/promises";
20
20
  import { buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, purgeScratchpadDir, sweepStaleScratchpads } from "./env-facts.js";
21
- import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody } from "./spec-fields.js";
21
+ import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody, toolNameListFromBody } from "./spec-fields.js";
22
22
  import { createBrain, brainSummary } from "./brain.js";
23
23
  import { loadConfig, logConfigDiagnostics } from "./config.js";
24
24
  import { resourceSuspendOptIn } from "./resource-suspend.js";
@@ -69,7 +69,8 @@ import { QuestionCoordinator } from "./question.js";
69
69
  import { ToolApprovalCoordinator } from "./tool-approval.js";
70
70
  import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, mcpForScenario, restartReasons } from "./sema-registry.js";
71
71
  import { ensureSealedKeyStore, reportExecutionPublicKey } from "./sealed-key.js";
72
- import { createConfigProvider } from "./config-provider.js";
72
+ import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "./config-provider.js";
73
+ import { validateCenterPrompts, centerPromptProvider } from "./capabilities/center-prompts.js";
73
74
  import { createPrincipalCapsClient, gateExecutionLane, scopedTokenNeedsWorker, applyObserverEnvOptIn } from "./runtime-caps-resolver.js";
74
75
  import { applyRuntimeGovernance, stripDelegationTools } from "./runtime-governance.js";
75
76
  import { parseTaskSettings, applyTaskSettings, coercePermissionMode, withPermissionMode, effectiveThinking } from "./task-settings.js";
@@ -154,12 +155,45 @@ async function main() {
154
155
  logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
155
156
  }
156
157
  };
158
+ let centerPrompts;
159
+ const adoptCenterPrompts = (eff, phase) => {
160
+ const raw = eff?.prompts;
161
+ if (raw === undefined) {
162
+ if (centerPrompts)
163
+ logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
164
+ centerPrompts = undefined;
165
+ return;
166
+ }
167
+ const v = validateCenterPrompts(raw);
168
+ if (!v.ok) {
169
+ logger.warn("center_prompts_invalid", { phase, error: v.error, kept: centerPrompts?.packId ?? "(builtin)", note: "malformed prompts face rejected (core assemble would THROW per task) — keeping the previous pack/built-ins; fix the center payload" });
170
+ return;
171
+ }
172
+ if (centerPrompts?.contentDigest !== v.value.contentDigest) {
173
+ logger.info("center_prompts_adopted", { phase, packId: v.value.packId, contentDigest: v.value.contentDigest, sections: v.value.sections.length, scenarioOverrides: Object.keys(v.value.scenarioOverrides ?? {}).length, note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned" });
174
+ }
175
+ centerPrompts = v.value;
176
+ };
177
+ let bootConfigPending;
157
178
  if (configProvider) {
158
179
  const dryRun = cc?.dryRun ?? false;
159
180
  try {
160
- const r = await configProvider.fetchEffective(undefined);
181
+ const bootFetch = configProvider.fetchEffective(undefined);
182
+ const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
183
+ if (raced === BOOT_FETCH_DEFERRED) {
184
+ bootConfigPending = bootFetch;
185
+ bootFetch.catch(() => { });
186
+ logger.warn("config_boot_fetch_deferred", {
187
+ source: configProvider.kind,
188
+ budgetMs: config.configBootFetchBudgetMs,
189
+ note: "center slow/unreachable — serving on the env fallback now; the pull continues in the background and hot-applies on arrival (roster gate opens then). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
190
+ });
191
+ }
192
+ const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
161
193
  if (r) {
162
- const bootClean = (r.domainErrors ?? []).length === 0;
194
+ const bootPromptsRaw = r.effective.prompts;
195
+ const bootPromptsOk = bootPromptsRaw === undefined || validateCenterPrompts(bootPromptsRaw).ok;
196
+ const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
163
197
  if (bootClean)
164
198
  ccEtag = r.etag;
165
199
  for (const de of r.domainErrors ?? [])
@@ -169,6 +203,7 @@ async function main() {
169
203
  }
170
204
  else {
171
205
  applyEffective(config, r.effective, logger, { sealedKeys });
206
+ adoptCenterPrompts(r.effective, "boot");
172
207
  markRosterLanded(r.effective);
173
208
  effective = r.effective;
174
209
  if (bootClean)
@@ -1042,67 +1077,108 @@ async function main() {
1042
1077
  if (configProvider) {
1043
1078
  const ccRef = config.configCenter;
1044
1079
  let refreshInFlight = false;
1045
- const ccTimer = setInterval(() => {
1046
- void (async () => {
1047
- if (refreshInFlight)
1048
- return;
1049
- refreshInFlight = true;
1050
- try {
1051
- const r = await configProvider.fetchEffective(ccEtag);
1052
- if (r) {
1053
- const badDomains = [...new Set((r.domainErrors ?? []).map((de) => de.domain))].sort().join(",");
1054
- if (badDomains) {
1055
- if (lastRejectedCandidate?.version !== r.effective.version || lastRejectedCandidate.domains !== badDomains) {
1056
- lastRejectedCandidate = { version: r.effective.version, domains: badDomains, at: Date.now() };
1057
- logger.warn("config_candidate_rejected", {
1058
- version: r.effective.version,
1059
- domains: badDomains,
1060
- errors: (r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
1061
- lkgVersion: latestEffective?.version ?? "(env)",
1062
- note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
1063
- });
1064
- }
1065
- return;
1080
+ const refreshTick = async () => {
1081
+ if (refreshInFlight)
1082
+ return;
1083
+ refreshInFlight = true;
1084
+ try {
1085
+ const r = await configProvider.fetchEffective(ccEtag);
1086
+ if (r) {
1087
+ const promptsRaw = r.effective.prompts;
1088
+ const promptsGate = promptsRaw !== undefined ? validateCenterPrompts(promptsRaw) : { ok: true };
1089
+ const badDomains = [
1090
+ ...new Set([...(r.domainErrors ?? []).map((de) => de.domain), ...(promptsGate.ok ? [] : ["prompts"])]),
1091
+ ]
1092
+ .sort()
1093
+ .join(",");
1094
+ if (badDomains) {
1095
+ if (lastRejectedCandidate?.version !== r.effective.version || lastRejectedCandidate.domains !== badDomains) {
1096
+ lastRejectedCandidate = { version: r.effective.version, domains: badDomains, at: Date.now() };
1097
+ logger.warn("config_candidate_rejected", {
1098
+ version: r.effective.version,
1099
+ domains: badDomains,
1100
+ errors: [
1101
+ ...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
1102
+ ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }]),
1103
+ ],
1104
+ lkgVersion: latestEffective?.version ?? "(env)",
1105
+ note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
1106
+ });
1066
1107
  }
1067
- lastRejectedCandidate = undefined;
1068
- if (ccRef?.dryRun) {
1069
- logEffectiveDiff(config, r.effective, logger);
1070
- ccEtag = r.etag;
1108
+ return;
1109
+ }
1110
+ lastRejectedCandidate = undefined;
1111
+ if (ccRef?.dryRun) {
1112
+ logEffectiveDiff(config, r.effective, logger);
1113
+ ccEtag = r.etag;
1114
+ }
1115
+ else {
1116
+ applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys });
1117
+ markRosterLanded(r.effective);
1118
+ mutateInPlace(pricing, buildPricing(config.models));
1119
+ keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1120
+ const reasons = restartReasons(effective, r.effective);
1121
+ if (reasons.length === 0) {
1122
+ pendingRestart = undefined;
1123
+ }
1124
+ else if (!pendingRestart || pendingRestart.reasons.join(",") !== reasons.join(",")) {
1125
+ pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1071
1126
  }
1072
1127
  else {
1073
- applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys });
1074
- markRosterLanded(r.effective);
1075
- mutateInPlace(pricing, buildPricing(config.models));
1076
- keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1077
- const reasons = restartReasons(effective, r.effective);
1078
- if (reasons.length === 0) {
1079
- pendingRestart = undefined;
1080
- }
1081
- else if (!pendingRestart || pendingRestart.reasons.join(",") !== reasons.join(",")) {
1082
- pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1083
- }
1084
- else {
1085
- pendingRestart = { ...pendingRestart, version: r.effective.version };
1086
- }
1087
- logger.info("sema_registry_refreshed", {
1088
- version: r.effective.version,
1089
- note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
1090
- ...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
1091
- });
1092
- latestEffective = r.effective;
1093
- ccEtag = r.etag;
1128
+ pendingRestart = { ...pendingRestart, version: r.effective.version };
1094
1129
  }
1130
+ logger.info("sema_registry_refreshed", {
1131
+ version: r.effective.version,
1132
+ note: "teams + runtime governance + models/roles/roster hot-applied; skills/mcp/runtime-gates/scenarios are restart-to-apply",
1133
+ ...(pendingRestart ? { restartRequired: true, restartReasons: pendingRestart.reasons } : {}),
1134
+ });
1135
+ latestEffective = r.effective;
1136
+ ccEtag = r.etag;
1095
1137
  }
1096
1138
  }
1097
- catch (err) {
1098
- logger.warn("sema_registry_refresh_failed", { err: String(err), note: "etag NOT advanced — the same candidate is re-fetched and re-applied next poll" });
1099
- }
1100
- finally {
1101
- refreshInFlight = false;
1102
- }
1103
- })();
1104
- }, 60_000);
1139
+ }
1140
+ catch (err) {
1141
+ logger.warn("sema_registry_refresh_failed", { err: String(err), note: "etag NOT advanced — the same candidate is re-fetched and re-applied next poll" });
1142
+ }
1143
+ finally {
1144
+ refreshInFlight = false;
1145
+ }
1146
+ };
1147
+ const ccTimer = setInterval(() => void refreshTick(), 60_000);
1105
1148
  ccTimer.unref?.();
1149
+ const deferredBootApply = (r) => {
1150
+ if (!r)
1151
+ return;
1152
+ const promptsRaw = r.effective.prompts;
1153
+ const promptsGate = promptsRaw !== undefined ? validateCenterPrompts(promptsRaw) : { ok: true };
1154
+ if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
1155
+ logger.warn("config_boot_deferred_candidate_rejected", {
1156
+ version: r.effective.version,
1157
+ errors: [...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })), ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }])],
1158
+ note: "late boot pull carried an invalid candidate — env fallback keeps serving; the refresh cadence re-judges the same tree (etag not advanced)",
1159
+ });
1160
+ return;
1161
+ }
1162
+ if (ccRef?.dryRun) {
1163
+ logEffectiveDiff(config, r.effective, logger);
1164
+ ccEtag = r.etag;
1165
+ return;
1166
+ }
1167
+ applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys });
1168
+ adoptCenterPrompts(r.effective, "boot-deferred");
1169
+ markRosterLanded(r.effective);
1170
+ mutateInPlace(pricing, buildPricing(config.models));
1171
+ keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1172
+ effective = r.effective;
1173
+ latestEffective = r.effective;
1174
+ ccEtag = r.etag;
1175
+ const reasons = restartReasons(undefined, r.effective).filter((sl) => sl !== "prompts");
1176
+ if (reasons.length > 0)
1177
+ pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1178
+ logger.info("config_loaded_deferred", { source: configProvider.kind, version: r.effective.version, models: (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length, ...(reasons.length > 0 ? { restartRequired: true, restartReasons: reasons } : {}) });
1179
+ };
1180
+ if (bootConfigPending)
1181
+ void bootConfigPending.then((r) => deferredBootApply(r), () => { });
1106
1182
  }
1107
1183
  let keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1108
1184
  const mysqlPool = backend?.mysqlPool();
@@ -1397,6 +1473,8 @@ async function main() {
1397
1473
  ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
1398
1474
  ? { retainBackgroundProcesses: true }
1399
1475
  : {}),
1476
+ ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
1477
+ ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
1400
1478
  ...(typeof body.interactiveTools === "boolean"
1401
1479
  ? { interactiveTools: body.interactiveTools }
1402
1480
  : {}),
@@ -1432,7 +1510,7 @@ async function main() {
1432
1510
  })(cap.tools),
1433
1511
  skills: mergeUserSkills(cap.skills, body.skills, logger),
1434
1512
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
1435
- promptProvider: cap.promptProvider,
1513
+ promptProvider: centerPrompts ? centerPromptProvider(centerPrompts, scenarioName) : cap.promptProvider,
1436
1514
  toolPolicy: durableEnabled
1437
1515
  ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
1438
1516
  requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,