@sema-agent/server 1.314.0 → 1.315.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/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/hooks/hook-runner.js +18 -9
- package/dist/http/server.d.ts +95 -69
- package/dist/http/server.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.js +169 -165
- package/dist/main.js +462 -451
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/security.d.ts +2 -58
- package/dist/security.js +3 -118
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/package.json +1 -1
package/dist/sema-registry.js
CHANGED
|
@@ -1,568 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { applyAutoCompactWindow, parseAutonomy } from "./config.js";
|
|
6
|
-
import { validateCommandRules } from "./runtime-governance.js";
|
|
7
|
-
import { projectCollabToWorkflows, registerCollabWorkflows } from "./capabilities/collab-workflows.js";
|
|
8
|
-
import { registerTeams } from "./capabilities/team.js";
|
|
9
|
-
export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, worker) {
|
|
10
|
-
const scheme = new URL(baseUrl).protocol;
|
|
11
|
-
if (scheme !== "http:" && scheme !== "https:")
|
|
12
|
-
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
13
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective${worker ? `?worker=${encodeURIComponent(worker)}` : ""}`;
|
|
14
|
-
const res = await fetchImpl(url, {
|
|
15
|
-
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
16
|
-
signal: AbortSignal.timeout(8000),
|
|
17
|
-
});
|
|
18
|
-
if (res.status === 304)
|
|
19
|
-
return null;
|
|
20
|
-
if (!res.ok)
|
|
21
|
-
throw new Error(`sema-registry HTTP ${res.status}`);
|
|
22
|
-
return { effective: (await res.json()), etag: res.headers.get("etag") ?? undefined };
|
|
23
|
-
}
|
|
24
|
-
export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchImpl = fetch, worker) {
|
|
25
|
-
const scheme = new URL(baseUrl).protocol;
|
|
26
|
-
if (scheme !== "http:" && scheme !== "https:")
|
|
27
|
-
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
28
|
-
const params = new URLSearchParams({ principal });
|
|
29
|
-
if (worker)
|
|
30
|
-
params.set("worker", worker);
|
|
31
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
|
|
32
|
-
const res = await fetchImpl(url, {
|
|
33
|
-
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
34
|
-
signal: AbortSignal.timeout(8000),
|
|
35
|
-
});
|
|
36
|
-
if (res.status === 304) {
|
|
37
|
-
if (!etag)
|
|
38
|
-
throw new Error("sema-registry returned 304 to a non-conditional principal-caps request");
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
if (!res.ok)
|
|
42
|
-
throw new Error(`sema-registry principal-caps HTTP ${res.status}`);
|
|
43
|
-
const body = (await res.json());
|
|
44
|
-
return {
|
|
45
|
-
runtimeCaps: body.runtimeCaps ?? null,
|
|
46
|
-
configured: Boolean(body.configured),
|
|
47
|
-
...("scenario" in body || "allowlist" in body
|
|
48
|
-
? {
|
|
49
|
-
scenario: {
|
|
50
|
-
scenario: typeof body.scenario === "string" ? body.scenario : null,
|
|
51
|
-
allowlist: Array.isArray(body.allowlist) ? body.allowlist.filter((s) => typeof s === "string") : [],
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
: {}),
|
|
55
|
-
...((() => {
|
|
56
|
-
const ex = body.execution;
|
|
57
|
-
if (ex === undefined || ex === null)
|
|
58
|
-
return {};
|
|
59
|
-
if (typeof ex.required !== "boolean")
|
|
60
|
-
return { executionDrift: `execution.required is ${typeof ex.required}, expected boolean — ruling dropped (fail-open)` };
|
|
61
|
-
if (!Array.isArray(ex.allowedLanes))
|
|
62
|
-
return { executionDrift: `execution.allowedLanes is ${typeof ex.allowedLanes}, expected string[] — ruling dropped (fail-open)` };
|
|
63
|
-
const execution = { required: ex.required, allowedLanes: ex.allowedLanes.filter((s) => typeof s === "string") };
|
|
64
|
-
if (ex.sessionMirror !== undefined && ex.sessionMirror !== null) {
|
|
65
|
-
const sm = ex.sessionMirror;
|
|
66
|
-
const smOk = typeof sm === "object" && typeof sm.engineUrl === "string" && sm.engineUrl.length > 0 && (sm.required === undefined || typeof sm.required === "boolean");
|
|
67
|
-
if (smOk)
|
|
68
|
-
execution.sessionMirror = { engineUrl: sm.engineUrl, required: sm.required === true };
|
|
69
|
-
else
|
|
70
|
-
return { execution, executionDrift: "execution.sessionMirror malformed — mirror observation dropped (fail-open; lane ruling kept)" };
|
|
71
|
-
}
|
|
72
|
-
return { execution };
|
|
73
|
-
})()),
|
|
74
|
-
etag: res.headers.get("etag") ?? undefined,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
const asModelThinking = (v) => (v && isThinkingLevel(v) && v !== "off" ? v : undefined);
|
|
78
|
-
const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
79
|
-
function toModel(m, envDefaults = {}) {
|
|
80
|
-
const thinkingDefault = asModelThinking(m.defaultThinking) ?? envDefaults.defaultThinking;
|
|
81
|
-
const effortLevels = (m.reasoningEffortLevels ? m.reasoningEffortLevels.map(asModelThinking).filter((x) => x !== undefined) : undefined) ?? envDefaults.reasoningEffortLevels ?? [];
|
|
82
|
-
const model = {
|
|
83
|
-
id: m.id,
|
|
84
|
-
name: m.name,
|
|
85
|
-
provider: m.provider,
|
|
86
|
-
api: m.api,
|
|
87
|
-
baseUrl: m.baseUrl ?? "",
|
|
88
|
-
reasoning: m.reasoning !== undefined ? Boolean(m.reasoning) : (envDefaults.reasoning ?? true),
|
|
89
|
-
input: m.vision ? ["text", "image"] : ["text"],
|
|
90
|
-
contextWindow: m.contextWindow ?? envDefaults.contextWindow ?? 262144,
|
|
91
|
-
maxTokens: m.maxTokens ?? envDefaults.maxTokens ?? 4096,
|
|
92
|
-
...(charsPerTokenOf(m.charsPerToken) !== undefined || envDefaults.charsPerToken !== undefined
|
|
93
|
-
? { charsPerToken: charsPerTokenOf(m.charsPerToken) ?? envDefaults.charsPerToken }
|
|
94
|
-
: {}),
|
|
95
|
-
cost: m.cost
|
|
96
|
-
? { input: m.cost.input, output: m.cost.output, cacheRead: m.cost.cacheRead ?? 0, cacheWrite: m.cost.cacheWrite ?? 0 }
|
|
97
|
-
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
98
|
-
params: { ...(m.tier ? { tier: m.tier } : {}), promptCacheFamily: m.provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
|
|
99
|
-
...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
|
|
100
|
-
...(effortLevels.length > 0 && m.provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
|
|
101
|
-
...(m.extraBody ? { extraBody: m.extraBody } : {}),
|
|
102
|
-
...(Array.isArray(m.promptGuidance) && m.promptGuidance.length > 0 ? { promptGuidance: m.promptGuidance.map(String) } : {}),
|
|
103
|
-
};
|
|
104
|
-
const explicitAct = m.autoCompactTokens;
|
|
105
|
-
applyAutoCompactWindow(model, typeof explicitAct === "number" ? explicitAct : undefined);
|
|
106
|
-
return model;
|
|
107
|
-
}
|
|
108
|
-
export function mutateInPlace(target, source) {
|
|
109
|
-
for (const k of Object.keys(target))
|
|
110
|
-
delete target[k];
|
|
111
|
-
Object.assign(target, source);
|
|
112
|
-
}
|
|
113
|
-
export function applyEffective(config, eff, logger, opts = {}) {
|
|
114
|
-
if (!opts.teamsOnly && !eff.version) {
|
|
115
|
-
logger?.warn("sema_registry_unpublished", {
|
|
116
|
-
version: eff.version,
|
|
117
|
-
note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
|
|
121
|
-
if (enabled.length > 0 && opts.deferModelPlane !== true) {
|
|
122
|
-
const models = {};
|
|
123
|
-
const modelApiKeyEnv = {};
|
|
124
|
-
const modelApiKeys = {};
|
|
125
|
-
const envDefaults = { maxTokens: config.model.maxTokens, contextWindow: config.model.contextWindow, reasoning: config.model.reasoning, charsPerToken: config.model.charsPerToken, defaultThinking: config.model.defaultThinking, reasoningEffortLevels: config.model.compat?.reasoningEffortLevels };
|
|
126
|
-
for (const m of enabled) {
|
|
127
|
-
const rawBaseUrl = m.baseUrl;
|
|
128
|
-
let safeUrl = rawBaseUrl;
|
|
129
|
-
if (rawBaseUrl) {
|
|
130
|
-
let ok = false;
|
|
131
|
-
try {
|
|
132
|
-
const u = new URL(rawBaseUrl);
|
|
133
|
-
ok = u.protocol === "http:" || u.protocol === "https:";
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
ok = false;
|
|
137
|
-
}
|
|
138
|
-
if (ok)
|
|
139
|
-
logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: rawBaseUrl, hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
|
|
140
|
-
else {
|
|
141
|
-
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: String(rawBaseUrl).slice(0, 120), hint: "only http(s) URLs are honored — falling back to the boot-env gateway" });
|
|
142
|
-
safeUrl = undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
models[m.name] = toModel(safeUrl === rawBaseUrl ? m : { ...m, baseUrl: safeUrl }, envDefaults);
|
|
146
|
-
if (m.sealedApiKey) {
|
|
147
|
-
const s = m.sealedApiKey;
|
|
148
|
-
if (s.alg !== SEALED_BOX_ALG) {
|
|
149
|
-
logger?.warn("sema_registry_model_sealed_key_bad_alg", { model: m.name, alg: String(s.alg), expected: SEALED_BOX_ALG, hint: "unsupported sealed-box alg — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback; contract v1 pins the alg)" });
|
|
150
|
-
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "unsupported_alg");
|
|
151
|
-
}
|
|
152
|
-
else if (!opts.sealedKeys) {
|
|
153
|
-
logger?.warn("sema_registry_model_sealed_key_no_store", { model: m.name, publicKeyId: s.publicKeyId, hint: "no sealed-key store on this host (boot custody init failed or lane unwired) — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
|
|
154
|
-
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "no_key_store");
|
|
155
|
-
}
|
|
156
|
-
else if (!opts.sealedKeys.has(s.publicKeyId)) {
|
|
157
|
-
logger?.warn("sema_registry_model_sealed_key_no_private_key", { model: m.name, publicKeyId: s.publicKeyId, held: opts.sealedKeys.ids(), hint: "ciphertext targets a private key this host doesn't hold (rotated away / other host) — key POISONED: tasks on this model fail loud until the operator re-pastes the key in the web UI (it seals against the newest registered public key)" });
|
|
158
|
-
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "stale_ciphertext");
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
try {
|
|
162
|
-
modelApiKeys[m.name] = opts.sealedKeys.open(s.ciphertext, s.publicKeyId);
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
logger?.warn("sema_registry_model_sealed_key_open_failed", { model: m.name, publicKeyId: s.publicKeyId, err: String(err), hint: "corrupt/foreign ciphertext — key POISONED: tasks on this model fail loud (no apiKeyEnv fallback, no gateway fallback)" });
|
|
166
|
-
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "sealed_decrypt_failed", err instanceof Error ? err.name : "unknown");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
else if (m.apiKeyEnv) {
|
|
171
|
-
if (process.env[m.apiKeyEnv])
|
|
172
|
-
modelApiKeyEnv[m.name] = m.apiKeyEnv;
|
|
173
|
-
else
|
|
174
|
-
logger?.warn("sema_registry_model_key_env_missing", { model: m.name, apiKeyEnv: m.apiKeyEnv, hint: "env var not set in this service — model falls back to the gateway key" });
|
|
175
|
-
}
|
|
176
|
-
if (m.provider === "anthropic" && !config.anthropic) {
|
|
177
|
-
logger?.warn("sema_registry_model_no_brain", { model: m.name, provider: m.provider, hint: "set ANTHROPIC_API_KEY in this service's env, else it mis-routes to the gateway brain" });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
config.modelApiKeyEnv = modelApiKeyEnv;
|
|
181
|
-
config.modelApiKeys = modelApiKeys;
|
|
182
|
-
const quotaWeights = {};
|
|
183
|
-
for (const m of enabled) {
|
|
184
|
-
const qw = typeof m.quotaWeight === "number" && Number.isFinite(m.quotaWeight) && m.quotaWeight > 0 ? m.quotaWeight : undefined;
|
|
185
|
-
if (qw !== undefined) {
|
|
186
|
-
quotaWeights[m.name] = qw;
|
|
187
|
-
if (m.id && m.id !== m.name)
|
|
188
|
-
quotaWeights[m.id] = qw;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
mutateInPlace(config.modelQuotaWeights, quotaWeights);
|
|
192
|
-
const roles = {};
|
|
193
|
-
for (const [role, tgt] of Object.entries(eff.models.roles ?? {})) {
|
|
194
|
-
if ("model" in tgt)
|
|
195
|
-
roles[role] = tgt.model;
|
|
196
|
-
else if ("select" in tgt)
|
|
197
|
-
roles[role] = { select: tgt.select };
|
|
198
|
-
}
|
|
199
|
-
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
200
|
-
const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));
|
|
201
|
-
const defaultName = picked.name;
|
|
202
|
-
const defaultSource = picked.source;
|
|
203
|
-
models.default = models[defaultName];
|
|
204
|
-
mutateInPlace(config.models, models);
|
|
205
|
-
config.model = config.models.default;
|
|
206
|
-
if (Object.keys(roles).length > 0)
|
|
207
|
-
mutateInPlace(config.roles, roles);
|
|
208
|
-
mutateInPlace(config.tiers, activeTiers);
|
|
209
|
-
logger?.info("sema_registry_models", { count: enabled.length, default: config.model.id, defaultSource, roles: Object.keys(roles), tiers: Object.keys(config.tiers), sealedKeys: Object.values(modelApiKeys).filter((v) => typeof v === "string").length, sealedPoisoned: Object.values(modelApiKeys).filter((v) => typeof v !== "string").length });
|
|
210
|
-
}
|
|
211
|
-
const gatesView = eff.governance ? { ...eff.runtime, ...eff.governance } : eff.runtime;
|
|
212
|
-
if (!opts.teamsOnly)
|
|
213
|
-
applyRuntimeGates(config, gatesView, logger);
|
|
214
|
-
applyRuntimeHot(config, gatesView, logger);
|
|
215
|
-
if (opts.deferModelPlane !== true) {
|
|
216
|
-
const teams = {};
|
|
217
|
-
for (const t of (eff.teams?.teams ?? []).filter((t) => t.enabled !== false)) {
|
|
218
|
-
teams[t.name] = {
|
|
219
|
-
name: t.name,
|
|
220
|
-
members: t.members.map((mm) => ({ role: mm.role, ...(mm.model ? { model: mm.model } : {}), modelRole: mm.modelRole, systemPrompt: mm.systemPrompt })),
|
|
221
|
-
rounds: t.rounds,
|
|
222
|
-
synthesizer: t.synthesizer ? { role: t.synthesizer.role, modelRole: t.synthesizer.modelRole, systemPrompt: t.synthesizer.systemPrompt } : undefined,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
registerTeams(teams);
|
|
226
|
-
if (Object.keys(teams).length > 0)
|
|
227
|
-
logger?.info("sema_registry_teams", { teams: Object.keys(teams) });
|
|
228
|
-
const projected = projectCollabToWorkflows(eff.collab?.templates);
|
|
229
|
-
registerCollabWorkflows(projected.workflows);
|
|
230
|
-
if (Object.keys(projected.workflows).length > 0)
|
|
231
|
-
logger?.info("sema_registry_collab_workflows", { workflows: Object.keys(projected.workflows) });
|
|
232
|
-
if (projected.skipped.length > 0)
|
|
233
|
-
logger?.warn("sema_registry_collab_skipped", { skipped: projected.skipped });
|
|
234
|
-
if (projected.notes.length > 0)
|
|
235
|
-
logger?.info("sema_registry_collab_notes", { notes: projected.notes });
|
|
236
|
-
}
|
|
237
|
-
const projDomain = eff.projects;
|
|
238
|
-
const rawProjects = (projDomain && typeof projDomain === "object" && !Array.isArray(projDomain)
|
|
239
|
-
? "projects" in projDomain && typeof projDomain.projects === "object"
|
|
240
|
-
? (projDomain.projects ?? {})
|
|
241
|
-
: projDomain
|
|
242
|
-
: {}) ?? {};
|
|
243
|
-
const projects = {};
|
|
244
|
-
for (const [pid, reg] of Object.entries(rawProjects)) {
|
|
245
|
-
if (!reg || typeof reg !== "object" || Array.isArray(reg))
|
|
246
|
-
continue;
|
|
247
|
-
const r = reg;
|
|
248
|
-
projects[pid] = {
|
|
249
|
-
...(typeof r.displayName === "string" ? { displayName: r.displayName } : {}),
|
|
250
|
-
...(Array.isArray(r.gitRemotes) ? { gitRemotes: r.gitRemotes.filter((x) => typeof x === "string") } : {}),
|
|
251
|
-
...(Array.isArray(r.defaultScopes) ? { defaultScopes: r.defaultScopes.filter((x) => typeof x === "string") } : {}),
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
mutateInPlace(config.projects, projects);
|
|
255
|
-
if (Object.keys(projects).length > 0)
|
|
256
|
-
logger?.info("sema_registry_projects", { projects: Object.keys(projects) });
|
|
257
|
-
}
|
|
258
|
-
const RUNTIME_GATE_KEYS = ["rateLimitPerMin", "approvalRequire", "maxTaskCostUsd", "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec"];
|
|
259
|
-
function runtimeGatePresent(rt, key) {
|
|
260
|
-
const v = rt[key];
|
|
261
|
-
if (key === "approvalRequire")
|
|
262
|
-
return Array.isArray(v);
|
|
263
|
-
if (key === "costQuotaWindowSec")
|
|
264
|
-
return typeof v === "number" && v > 0;
|
|
265
|
-
return typeof v === "number";
|
|
266
|
-
}
|
|
267
|
-
export function applyRuntimeGates(config, rt, logger) {
|
|
268
|
-
if (!rt)
|
|
269
|
-
return;
|
|
270
|
-
const applied = {};
|
|
271
|
-
for (const key of RUNTIME_GATE_KEYS) {
|
|
272
|
-
if (!runtimeGatePresent(rt, key))
|
|
273
|
-
continue;
|
|
274
|
-
config[key] = rt[key];
|
|
275
|
-
applied[key] = rt[key];
|
|
276
|
-
}
|
|
277
|
-
if (Object.keys(applied).length > 0)
|
|
278
|
-
logger?.info("sema_registry_runtime", applied);
|
|
279
|
-
}
|
|
280
|
-
export function applyRuntimeHot(config, rt, logger) {
|
|
281
|
-
const applied = {};
|
|
282
|
-
const envAutonomy = parseAutonomy(process.env.AUTONOMY);
|
|
283
|
-
const nextAutonomy = rt?.autonomy !== undefined ? rt.autonomy : envAutonomy;
|
|
284
|
-
if (config.autonomy !== nextAutonomy) {
|
|
285
|
-
config.autonomy = nextAutonomy;
|
|
286
|
-
applied.autonomy = nextAutonomy ?? "(env-baseline)";
|
|
287
|
-
}
|
|
288
|
-
if (rt?.commandPolicy !== undefined) {
|
|
289
|
-
const errors = validateCommandRules(rt.commandPolicy);
|
|
290
|
-
if (errors.length > 0) {
|
|
291
|
-
logger?.error("sema_registry_commandpolicy_invalid", { errors, kept: config.commandPolicy?.length ?? 0 });
|
|
292
|
-
}
|
|
293
|
-
else if (JSON.stringify(config.commandPolicy) !== JSON.stringify(rt.commandPolicy)) {
|
|
294
|
-
config.commandPolicy = rt.commandPolicy;
|
|
295
|
-
applied.commandPolicy = rt.commandPolicy.length;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else if (config.commandPolicy !== undefined) {
|
|
299
|
-
config.commandPolicy = undefined;
|
|
300
|
-
applied.commandPolicy = "(env-baseline)";
|
|
301
|
-
}
|
|
302
|
-
if (Object.keys(applied).length > 0)
|
|
303
|
-
logger?.info("sema_registry_runtime_hot", applied);
|
|
304
|
-
}
|
|
305
|
-
const DEFAULT_ROLE_TIER_LADDER = ["pro", "flash", "lite"];
|
|
306
|
-
export function resolveDefaultModelName(eff, has, fallback, onDangling) {
|
|
307
|
-
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
308
|
-
const structural = eff.models?.default;
|
|
309
|
-
const rolesDefault = eff.models?.roles?.default?.model;
|
|
310
|
-
const candidates = [
|
|
311
|
-
{ source: "models.default", name: typeof structural === "string" && structural.length > 0 ? structural : undefined },
|
|
312
|
-
{ source: "roles.default", name: typeof rolesDefault === "string" && rolesDefault.length > 0 ? rolesDefault : undefined },
|
|
313
|
-
...DEFAULT_ROLE_TIER_LADDER.map((t) => ({ source: `tierGroup.${t}`, name: activeTiers[t] })),
|
|
314
|
-
];
|
|
315
|
-
for (const c of candidates) {
|
|
316
|
-
if (!c.name)
|
|
317
|
-
continue;
|
|
318
|
-
if (has(c.name))
|
|
319
|
-
return { name: c.name, source: c.source };
|
|
320
|
-
onDangling?.(c.source, c.name);
|
|
321
|
-
}
|
|
322
|
-
return { name: fallback, source: "enabled[0]" };
|
|
323
|
-
}
|
|
324
|
-
export function logEffectiveDiff(config, eff, logger) {
|
|
325
|
-
const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
|
|
326
|
-
const centerModels = enabled.map((m) => ({ name: m.name, id: m.id, provider: m.provider, apiKeyEnv: m.apiKeyEnv ?? null, tier: m.tier ?? null }));
|
|
327
|
-
const envModels = Object.entries(config.models).map(([name, m]) => ({ name, id: m.id, provider: m.provider }));
|
|
328
|
-
const centerRoles = Object.fromEntries(Object.entries(eff.models?.roles ?? {}).map(([r, t]) => [r, "model" in t ? t.model : { select: t.select }]));
|
|
329
|
-
const centerTeams = (eff.teams?.teams ?? []).filter((t) => t.enabled !== false).map((t) => t.name);
|
|
330
|
-
logger?.warn("sema_registry_dry_run", {
|
|
331
|
-
note: "DRY RUN — registry config NOT applied; unset SEMA_REGISTRY_DRY_RUN to go live",
|
|
332
|
-
version: eff.version,
|
|
333
|
-
wouldOverrideModels: enabled.length > 0,
|
|
334
|
-
wouldDefaultModel: enabled.length > 0 ? resolveDefaultModelName(eff, (n) => enabled.some((m) => m.name === n), enabled[0].name).name : null,
|
|
335
|
-
currentDefaultModel: config.model.id,
|
|
336
|
-
models: { center: centerModels, envDerived: envModels },
|
|
337
|
-
roles: { center: centerRoles, current: config.roles },
|
|
338
|
-
teams: { centerWouldRegister: centerTeams },
|
|
339
|
-
skills: { centerWouldLoad: (eff.skills?.skills ?? []).filter((s) => s.enabled !== false).map((s) => s.name) },
|
|
340
|
-
mcp: { centerWouldRegister: (eff.mcp?.servers ?? []).filter((s) => s.enabled !== false).map((s) => s.name) },
|
|
341
|
-
runtime: {
|
|
342
|
-
center: eff.runtime ?? null,
|
|
343
|
-
current: Object.fromEntries(RUNTIME_GATE_KEYS.map((k) => [k, config[k]])),
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
export function runtimeHasActiveGate(rt) {
|
|
348
|
-
if (!rt)
|
|
349
|
-
return false;
|
|
350
|
-
return RUNTIME_GATE_KEYS.some((k) => runtimeGatePresent(rt, k));
|
|
351
|
-
}
|
|
352
|
-
const RESTART_SLICES = ["skills", "mcp", "scenarios", "runtime-gates", "models-tiers"];
|
|
353
|
-
function stableStringify(v) {
|
|
354
|
-
if (v === null || typeof v !== "object")
|
|
355
|
-
return JSON.stringify(v) ?? "null";
|
|
356
|
-
if (Array.isArray(v))
|
|
357
|
-
return `[${v.map(stableStringify).join(",")}]`;
|
|
358
|
-
const obj = v;
|
|
359
|
-
return `{${Object.keys(obj)
|
|
360
|
-
.sort()
|
|
361
|
-
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
362
|
-
.join(",")}}`;
|
|
363
|
-
}
|
|
364
|
-
function enabledOnly(rows) {
|
|
365
|
-
if (!rows)
|
|
366
|
-
return null;
|
|
367
|
-
return rows.filter((r) => r.enabled !== false);
|
|
368
|
-
}
|
|
369
|
-
function restartSliceValue(eff, slice) {
|
|
370
|
-
if (!eff)
|
|
371
|
-
return null;
|
|
372
|
-
switch (slice) {
|
|
373
|
-
case "skills":
|
|
374
|
-
return enabledOnly(eff.skills?.skills);
|
|
375
|
-
case "mcp":
|
|
376
|
-
return enabledOnly(eff.mcp?.servers);
|
|
377
|
-
case "scenarios":
|
|
378
|
-
return enabledOnly(eff.scenarios?.scenarios);
|
|
379
|
-
case "runtime-gates": {
|
|
380
|
-
const rt = eff.runtime;
|
|
381
|
-
if (!rt)
|
|
382
|
-
return null;
|
|
383
|
-
const present = {};
|
|
384
|
-
for (const k of RUNTIME_GATE_KEYS)
|
|
385
|
-
if (runtimeGatePresent(rt, k))
|
|
386
|
-
present[k] = rt[k];
|
|
387
|
-
return Object.keys(present).length ? present : null;
|
|
388
|
-
}
|
|
389
|
-
case "models-tiers": {
|
|
390
|
-
const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
|
|
391
|
-
if (!active || Object.keys(active).length === 0)
|
|
392
|
-
return null;
|
|
393
|
-
const enabled = enabledOnly(eff.models?.models);
|
|
394
|
-
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
395
|
-
const def = resolveDefaultModelName(eff, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
396
|
-
return { models: enabled, tiers: active, default: def.name };
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
export function restartReasons(boot, current) {
|
|
401
|
-
return RESTART_SLICES.filter((s) => stableStringify(restartSliceValue(boot, s)) !== stableStringify(restartSliceValue(current, s)));
|
|
402
|
-
}
|
|
403
|
-
export function planeHasActiveTiers(eff) {
|
|
404
|
-
const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
|
|
405
|
-
return active !== undefined && Object.keys(active).length > 0;
|
|
406
|
-
}
|
|
407
|
-
export function modelPlaneChanged(prev, next) {
|
|
408
|
-
if (!prev)
|
|
409
|
-
return true;
|
|
410
|
-
const fp = (e) => {
|
|
411
|
-
const enabled = enabledOnly(e.models?.models);
|
|
412
|
-
const active = e.models ? (resolveActiveTiers(e.models) ?? null) : null;
|
|
413
|
-
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
414
|
-
const def = resolveDefaultModelName(e, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
415
|
-
return stableStringify({ models: enabled, tiers: active, default: def.name });
|
|
416
|
-
};
|
|
417
|
-
return fp(prev) !== fp(next);
|
|
418
|
-
}
|
|
419
|
-
export class ConfigCenterHttpError extends Error {
|
|
420
|
-
status;
|
|
421
|
-
constructor(message, status) {
|
|
422
|
-
super(message);
|
|
423
|
-
this.status = status;
|
|
424
|
-
this.name = "ConfigCenterHttpError";
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
export async function fetchSkillContent(baseUrl, token, contentHash, fetchImpl = fetch) {
|
|
428
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/skills/content/${encodeURIComponent(contentHash)}`;
|
|
429
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
430
|
-
if (!res.ok)
|
|
431
|
-
throw new ConfigCenterHttpError(`skill content HTTP ${res.status} for ${contentHash}`, res.status);
|
|
432
|
-
const content = await res.text();
|
|
433
|
-
const want = contentHash.replace(/^sha256:/, "").toLowerCase();
|
|
434
|
-
const got = createHash("sha256").update(content, "utf8").digest("hex");
|
|
435
|
-
if (got !== want)
|
|
436
|
-
throw new Error(`skill content hash mismatch for ${contentHash}: computed sha256:${got}`);
|
|
437
|
-
return content;
|
|
438
|
-
}
|
|
439
|
-
export async function fetchPromptArtifact(baseUrl, token, artifactDigest, fetchImpl = fetch) {
|
|
440
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/prompts/epoch-artifacts/${encodeURIComponent(artifactDigest)}`;
|
|
441
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
442
|
-
if (!res.ok)
|
|
443
|
-
throw new ConfigCenterHttpError(`prompt artifact HTTP ${res.status} for ${artifactDigest}`, res.status);
|
|
444
|
-
return await res.json();
|
|
445
|
-
}
|
|
446
|
-
export async function fetchPromptBlob(baseUrl, token, contentDigest, fetchImpl = fetch) {
|
|
447
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/prompts/blobs/${encodeURIComponent(contentDigest)}`;
|
|
448
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
449
|
-
if (!res.ok)
|
|
450
|
-
throw new ConfigCenterHttpError(`prompt blob HTTP ${res.status} for ${contentDigest}`, res.status);
|
|
451
|
-
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
452
|
-
const want = contentDigest.replace(/^sha256:/, "").toLowerCase();
|
|
453
|
-
const got = createHash("sha256").update(bytes).digest("hex");
|
|
454
|
-
if (got !== want)
|
|
455
|
-
throw new Error(`prompt blob hash mismatch for ${contentDigest}: computed sha256:${got}`);
|
|
456
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
457
|
-
}
|
|
458
|
-
export async function applyCenterSkills(baseline, manifest, baseUrl, token, logger, fetchImpl = fetch, diskCacheDir) {
|
|
459
|
-
const { promises: fsp } = await import("node:fs");
|
|
460
|
-
const { join: joinPath } = await import("node:path");
|
|
461
|
-
const diskRead = async (hash) => {
|
|
462
|
-
if (!diskCacheDir)
|
|
463
|
-
return undefined;
|
|
464
|
-
const hex = hash.replace(/^sha256:/, "");
|
|
465
|
-
if (!/^[0-9a-f]{64}$/.test(hex))
|
|
466
|
-
return undefined;
|
|
467
|
-
try {
|
|
468
|
-
const text = await fsp.readFile(joinPath(diskCacheDir, hex), "utf8");
|
|
469
|
-
if (skillContentHash(text) === hash)
|
|
470
|
-
return text;
|
|
471
|
-
logger?.warn("sema_registry_skill_cache_corrupt", { hash, note: "on-disk body fails its own hash — ignored, refetching from center" });
|
|
472
|
-
}
|
|
473
|
-
catch {
|
|
474
|
-
}
|
|
475
|
-
return undefined;
|
|
476
|
-
};
|
|
477
|
-
const pendingWrites = [];
|
|
478
|
-
const diskWrite = (hash, content) => {
|
|
479
|
-
if (!diskCacheDir)
|
|
480
|
-
return;
|
|
481
|
-
const hex = hash.replace(/^sha256:/, "");
|
|
482
|
-
pendingWrites.push((async () => {
|
|
483
|
-
await fsp.mkdir(diskCacheDir, { recursive: true });
|
|
484
|
-
const tmp = joinPath(diskCacheDir, `${hex}.${Math.random().toString(36).slice(2, 8)}.tmp`);
|
|
485
|
-
await fsp.writeFile(tmp, content, { mode: 0o600 });
|
|
486
|
-
await fsp.rename(tmp, joinPath(diskCacheDir, hex));
|
|
487
|
-
})().catch((err) => logger?.warn("sema_registry_skill_cache_write_failed", { hash, err: String(err) })));
|
|
488
|
-
};
|
|
489
|
-
const byName = new Map(baseline.map((s) => [s.spec.name, s]));
|
|
490
|
-
const contentCache = new Map();
|
|
491
|
-
const seenNames = new Set();
|
|
492
|
-
for (const m of manifest.skills ?? []) {
|
|
493
|
-
if (m.enabled === false)
|
|
494
|
-
continue;
|
|
495
|
-
if (seenNames.has(m.name))
|
|
496
|
-
logger?.warn("sema_registry_skill_duplicate_name", { name: m.name, note: "duplicate name in center manifest — last entry wins" });
|
|
497
|
-
seenNames.add(m.name);
|
|
498
|
-
try {
|
|
499
|
-
let content = contentCache.get(m.contentHash);
|
|
500
|
-
if (content === undefined)
|
|
501
|
-
content = await diskRead(m.contentHash);
|
|
502
|
-
if (content === undefined) {
|
|
503
|
-
content = await fetchSkillContent(baseUrl, token, m.contentHash, fetchImpl);
|
|
504
|
-
diskWrite(m.contentHash, content);
|
|
505
|
-
}
|
|
506
|
-
contentCache.set(m.contentHash, content);
|
|
507
|
-
const overrodeBuiltin = byName.has(m.name);
|
|
508
|
-
byName.set(m.name, { spec: { name: m.name, description: m.description, content }, scenarios: m.scenarios ?? [] });
|
|
509
|
-
logger?.info("sema_registry_skill", { name: m.name, hash: m.contentHash, scenarios: m.scenarios, overrodeBuiltin });
|
|
510
|
-
}
|
|
511
|
-
catch (err) {
|
|
512
|
-
logger?.warn("sema_registry_skill_failed", { name: m.name, hash: m.contentHash, err: String(err), note: "keeping baseline if any; skipping center version" });
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
await Promise.all(pendingWrites);
|
|
516
|
-
return [...byName.values()];
|
|
517
|
-
}
|
|
518
|
-
function resolveRefs(refs, server, kind, logger) {
|
|
519
|
-
if (!refs)
|
|
520
|
-
return {};
|
|
521
|
-
const out = {};
|
|
522
|
-
for (const [key, envName] of Object.entries(refs)) {
|
|
523
|
-
const v = process.env[envName];
|
|
524
|
-
if (v === undefined) {
|
|
525
|
-
logger?.warn("sema_registry_mcp_env_missing", { server, kind, key, envName, note: "skipping this MCP server — referenced env var is unset in this service" });
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
out[key] = v;
|
|
529
|
-
}
|
|
530
|
-
return out;
|
|
531
|
-
}
|
|
532
|
-
export function resolveMcpServers(mcp, logger) {
|
|
533
|
-
const out = [];
|
|
534
|
-
for (const s of mcp.servers ?? []) {
|
|
535
|
-
if (s.enabled === false)
|
|
536
|
-
continue;
|
|
537
|
-
const allow = s.allowTools && s.allowTools.length > 0 ? { allowTools: s.allowTools } : {};
|
|
538
|
-
const elicit = s.elicitation === true ? { elicitation: true } : {};
|
|
539
|
-
if (s.transport.kind === "stdio") {
|
|
540
|
-
const env = resolveRefs(s.transport.envRefs, s.name, "env", logger);
|
|
541
|
-
if (env === null)
|
|
542
|
-
continue;
|
|
543
|
-
out.push({
|
|
544
|
-
scenarios: s.scenarios ?? [],
|
|
545
|
-
spec: { name: s.name, transport: { kind: "stdio", command: s.transport.command, args: s.transport.args, ...(Object.keys(env).length ? { env } : {}) }, ...allow, ...elicit },
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
const headers = resolveRefs(s.transport.headerRefs, s.name, "header", logger);
|
|
550
|
-
if (headers === null)
|
|
551
|
-
continue;
|
|
552
|
-
out.push({
|
|
553
|
-
scenarios: s.scenarios ?? [],
|
|
554
|
-
spec: { name: s.name, transport: { kind: "http", url: s.transport.url, ...(Object.keys(headers).length ? { headers } : {}), ...(s.transport.principalHeader ? { principalHeader: s.transport.principalHeader } : {}) }, ...allow, ...elicit },
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
if (out.length > 0)
|
|
559
|
-
logger?.info("sema_registry_mcp", { servers: out.map((s) => s.spec.name) });
|
|
560
|
-
return out;
|
|
561
|
-
}
|
|
562
|
-
export function mcpForScenario(servers, scenario) {
|
|
563
|
-
if (!servers || servers.length === 0)
|
|
564
|
-
return undefined;
|
|
565
|
-
const hit = servers.filter((s) => s.scenarios.length === 0 || s.scenarios.includes(scenario)).map((s) => s.spec);
|
|
566
|
-
return hit.length > 0 ? hit : undefined;
|
|
567
|
-
}
|
|
1
|
+
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./config-center/http-client.js";
|
|
2
|
+
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./config-center/apply-effective.js";
|
|
3
|
+
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./config-center/restart-signal.js";
|
|
4
|
+
export { applyCenterSkills, resolveMcpServers, mcpForScenario } from "./config-center/skills-mcp.js";
|
|
568
5
|
//# sourceMappingURL=sema-registry.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.315.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|