@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApprovalHmacKey } from "./auth-keys.js";
|
|
2
|
+
export declare function approvalHmacMessage(env: {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
boundCallId?: string | null;
|
|
5
|
+
boundInputHash?: string | null;
|
|
6
|
+
decision: string;
|
|
7
|
+
reason?: string | null;
|
|
8
|
+
}): string;
|
|
9
|
+
export declare const MAX_APPROVAL_REASON_CHARS = 4096;
|
|
10
|
+
export declare function verifyApprovalHmac(env: {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
boundCallId?: string | null;
|
|
13
|
+
boundInputHash?: string | null;
|
|
14
|
+
decision: string;
|
|
15
|
+
reason?: string | null;
|
|
16
|
+
}, mac: string, kid: string | undefined, keys: ApprovalHmacKey[]): boolean;
|
|
17
|
+
//# sourceMappingURL=approval-hmac.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export function approvalHmacMessage(env) {
|
|
3
|
+
return JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null, env.decision, env.reason ?? null]);
|
|
4
|
+
}
|
|
5
|
+
export const MAX_APPROVAL_REASON_CHARS = 4_096;
|
|
6
|
+
export function verifyApprovalHmac(env, mac, kid, keys) {
|
|
7
|
+
if (keys.length === 0)
|
|
8
|
+
return false;
|
|
9
|
+
let supplied;
|
|
10
|
+
try {
|
|
11
|
+
supplied = Buffer.from(mac, "hex");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (supplied.length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
const ordered = kid ? [...keys.filter((k) => k.kid === kid), ...keys.filter((k) => k.kid !== kid)] : keys;
|
|
19
|
+
const msg = approvalHmacMessage(env);
|
|
20
|
+
for (const k of ordered) {
|
|
21
|
+
const expected = createHmac("sha256", k.key).update(msg).digest();
|
|
22
|
+
if (expected.length === supplied.length && timingSafeEqual(expected, supplied))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=approval-hmac.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
2
|
+
import { type SealedKeyOpener } from "../sealed-key.js";
|
|
3
|
+
import type { Logger } from "../observability/logger.js";
|
|
4
|
+
import type { EffectiveConfig } from "./types.js";
|
|
5
|
+
export declare function mutateInPlace<V>(target: Record<string, V>, source: Record<string, V>): void;
|
|
6
|
+
export declare function applyEffective(config: ServiceConfig, eff: EffectiveConfig, logger?: Logger, opts?: {
|
|
7
|
+
teamsOnly?: boolean;
|
|
8
|
+
sealedKeys?: SealedKeyOpener;
|
|
9
|
+
deferModelPlane?: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
export declare const RUNTIME_GATE_KEYS: readonly ["rateLimitPerMin", "approvalRequire", "maxTaskCostUsd", "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec"];
|
|
12
|
+
export type RuntimeGateKey = (typeof RUNTIME_GATE_KEYS)[number];
|
|
13
|
+
export declare function runtimeGatePresent(rt: NonNullable<EffectiveConfig["runtime"]>, key: RuntimeGateKey): boolean;
|
|
14
|
+
export declare function applyRuntimeGates(config: ServiceConfig, rt: EffectiveConfig["runtime"], logger?: Logger): void;
|
|
15
|
+
export declare function applyRuntimeHot(config: ServiceConfig, rt: EffectiveConfig["runtime"], logger?: Logger): void;
|
|
16
|
+
export declare function resolveDefaultModelName(eff: EffectiveConfig, has: (name: string) => boolean, fallback: string, onDangling?: (source: string, name: string) => void): {
|
|
17
|
+
name: string;
|
|
18
|
+
source: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function logEffectiveDiff(config: ServiceConfig, eff: EffectiveConfig, logger?: Logger): void;
|
|
21
|
+
export declare function runtimeHasActiveGate(rt: EffectiveConfig["runtime"]): boolean;
|
|
22
|
+
//# sourceMappingURL=apply-effective.d.ts.map
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { isThinkingLevel } from "@sema-agent/core";
|
|
2
|
+
import { resolveActiveTiers, SEALED_BOX_ALG } from "@sema-agent/registry-core";
|
|
3
|
+
import { sealedKeyPoison } from "../sealed-key.js";
|
|
4
|
+
import { applyAutoCompactWindow, parseAutonomy } from "../config.js";
|
|
5
|
+
import { validateCommandRules } from "../runtime-governance.js";
|
|
6
|
+
import { projectCollabToWorkflows, registerCollabWorkflows } from "../capabilities/collab-workflows.js";
|
|
7
|
+
import { registerTeams } from "../capabilities/team.js";
|
|
8
|
+
const asModelThinking = (v) => (v && isThinkingLevel(v) && v !== "off" ? v : undefined);
|
|
9
|
+
const charsPerTokenOf = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
|
|
10
|
+
function toModel(m, envDefaults = {}) {
|
|
11
|
+
const thinkingDefault = asModelThinking(m.defaultThinking) ?? envDefaults.defaultThinking;
|
|
12
|
+
const effortLevels = (m.reasoningEffortLevels ? m.reasoningEffortLevels.map(asModelThinking).filter((x) => x !== undefined) : undefined) ?? envDefaults.reasoningEffortLevels ?? [];
|
|
13
|
+
const model = {
|
|
14
|
+
id: m.id,
|
|
15
|
+
name: m.name,
|
|
16
|
+
provider: m.provider,
|
|
17
|
+
api: m.api,
|
|
18
|
+
baseUrl: m.baseUrl ?? "",
|
|
19
|
+
reasoning: m.reasoning !== undefined ? Boolean(m.reasoning) : (envDefaults.reasoning ?? true),
|
|
20
|
+
input: m.vision ? ["text", "image"] : ["text"],
|
|
21
|
+
contextWindow: m.contextWindow ?? envDefaults.contextWindow ?? 262144,
|
|
22
|
+
maxTokens: m.maxTokens ?? envDefaults.maxTokens ?? 4096,
|
|
23
|
+
...(charsPerTokenOf(m.charsPerToken) !== undefined || envDefaults.charsPerToken !== undefined
|
|
24
|
+
? { charsPerToken: charsPerTokenOf(m.charsPerToken) ?? envDefaults.charsPerToken }
|
|
25
|
+
: {}),
|
|
26
|
+
cost: m.cost
|
|
27
|
+
? { input: m.cost.input, output: m.cost.output, cacheRead: m.cost.cacheRead ?? 0, cacheWrite: m.cost.cacheWrite ?? 0 }
|
|
28
|
+
: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
29
|
+
params: { ...(m.tier ? { tier: m.tier } : {}), promptCacheFamily: m.provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
|
|
30
|
+
...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
|
|
31
|
+
...(effortLevels.length > 0 && m.provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
|
|
32
|
+
...(m.extraBody ? { extraBody: m.extraBody } : {}),
|
|
33
|
+
...(Array.isArray(m.promptGuidance) && m.promptGuidance.length > 0 ? { promptGuidance: m.promptGuidance.map(String) } : {}),
|
|
34
|
+
};
|
|
35
|
+
const explicitAct = m.autoCompactTokens;
|
|
36
|
+
applyAutoCompactWindow(model, typeof explicitAct === "number" ? explicitAct : undefined);
|
|
37
|
+
return model;
|
|
38
|
+
}
|
|
39
|
+
export function mutateInPlace(target, source) {
|
|
40
|
+
for (const k of Object.keys(target))
|
|
41
|
+
delete target[k];
|
|
42
|
+
Object.assign(target, source);
|
|
43
|
+
}
|
|
44
|
+
export function applyEffective(config, eff, logger, opts = {}) {
|
|
45
|
+
if (!opts.teamsOnly && !eff.version) {
|
|
46
|
+
logger?.warn("sema_registry_unpublished", {
|
|
47
|
+
version: eff.version,
|
|
48
|
+
note: "empty/unpublished effective config — using env + built-in fallback; publish a config or unset CONFIG_PUBLISH_MODE",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
|
|
52
|
+
if (enabled.length > 0 && opts.deferModelPlane !== true) {
|
|
53
|
+
const models = {};
|
|
54
|
+
const modelApiKeyEnv = {};
|
|
55
|
+
const modelApiKeys = {};
|
|
56
|
+
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 };
|
|
57
|
+
for (const m of enabled) {
|
|
58
|
+
const rawBaseUrl = m.baseUrl;
|
|
59
|
+
let safeUrl = rawBaseUrl;
|
|
60
|
+
if (rawBaseUrl) {
|
|
61
|
+
let ok = false;
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(rawBaseUrl);
|
|
64
|
+
ok = u.protocol === "http:" || u.protocol === "https:";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
ok = false;
|
|
68
|
+
}
|
|
69
|
+
if (ok)
|
|
70
|
+
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" });
|
|
71
|
+
else {
|
|
72
|
+
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" });
|
|
73
|
+
safeUrl = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
models[m.name] = toModel(safeUrl === rawBaseUrl ? m : { ...m, baseUrl: safeUrl }, envDefaults);
|
|
77
|
+
if (m.sealedApiKey) {
|
|
78
|
+
const s = m.sealedApiKey;
|
|
79
|
+
if (s.alg !== SEALED_BOX_ALG) {
|
|
80
|
+
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)" });
|
|
81
|
+
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "unsupported_alg");
|
|
82
|
+
}
|
|
83
|
+
else if (!opts.sealedKeys) {
|
|
84
|
+
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)" });
|
|
85
|
+
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "no_key_store");
|
|
86
|
+
}
|
|
87
|
+
else if (!opts.sealedKeys.has(s.publicKeyId)) {
|
|
88
|
+
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)" });
|
|
89
|
+
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "stale_ciphertext");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
try {
|
|
93
|
+
modelApiKeys[m.name] = opts.sealedKeys.open(s.ciphertext, s.publicKeyId);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
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)" });
|
|
97
|
+
modelApiKeys[m.name] = sealedKeyPoison(s.publicKeyId, "sealed_decrypt_failed", err instanceof Error ? err.name : "unknown");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (m.apiKeyEnv) {
|
|
102
|
+
if (process.env[m.apiKeyEnv])
|
|
103
|
+
modelApiKeyEnv[m.name] = m.apiKeyEnv;
|
|
104
|
+
else
|
|
105
|
+
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" });
|
|
106
|
+
}
|
|
107
|
+
if (m.provider === "anthropic" && !config.anthropic) {
|
|
108
|
+
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" });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
config.modelApiKeyEnv = modelApiKeyEnv;
|
|
112
|
+
config.modelApiKeys = modelApiKeys;
|
|
113
|
+
const quotaWeights = {};
|
|
114
|
+
for (const m of enabled) {
|
|
115
|
+
const qw = typeof m.quotaWeight === "number" && Number.isFinite(m.quotaWeight) && m.quotaWeight > 0 ? m.quotaWeight : undefined;
|
|
116
|
+
if (qw !== undefined) {
|
|
117
|
+
quotaWeights[m.name] = qw;
|
|
118
|
+
if (m.id && m.id !== m.name)
|
|
119
|
+
quotaWeights[m.id] = qw;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
mutateInPlace(config.modelQuotaWeights, quotaWeights);
|
|
123
|
+
const roles = {};
|
|
124
|
+
for (const [role, tgt] of Object.entries(eff.models.roles ?? {})) {
|
|
125
|
+
if ("model" in tgt)
|
|
126
|
+
roles[role] = tgt.model;
|
|
127
|
+
else if ("select" in tgt)
|
|
128
|
+
roles[role] = { select: tgt.select };
|
|
129
|
+
}
|
|
130
|
+
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
131
|
+
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" }));
|
|
132
|
+
const defaultName = picked.name;
|
|
133
|
+
const defaultSource = picked.source;
|
|
134
|
+
models.default = models[defaultName];
|
|
135
|
+
mutateInPlace(config.models, models);
|
|
136
|
+
config.model = config.models.default;
|
|
137
|
+
if (Object.keys(roles).length > 0)
|
|
138
|
+
mutateInPlace(config.roles, roles);
|
|
139
|
+
mutateInPlace(config.tiers, activeTiers);
|
|
140
|
+
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 });
|
|
141
|
+
}
|
|
142
|
+
const gatesView = eff.governance ? { ...eff.runtime, ...eff.governance } : eff.runtime;
|
|
143
|
+
if (!opts.teamsOnly)
|
|
144
|
+
applyRuntimeGates(config, gatesView, logger);
|
|
145
|
+
applyRuntimeHot(config, gatesView, logger);
|
|
146
|
+
if (opts.deferModelPlane !== true) {
|
|
147
|
+
const teams = {};
|
|
148
|
+
for (const t of (eff.teams?.teams ?? []).filter((t) => t.enabled !== false)) {
|
|
149
|
+
teams[t.name] = {
|
|
150
|
+
name: t.name,
|
|
151
|
+
members: t.members.map((mm) => ({ role: mm.role, ...(mm.model ? { model: mm.model } : {}), modelRole: mm.modelRole, systemPrompt: mm.systemPrompt })),
|
|
152
|
+
rounds: t.rounds,
|
|
153
|
+
synthesizer: t.synthesizer ? { role: t.synthesizer.role, modelRole: t.synthesizer.modelRole, systemPrompt: t.synthesizer.systemPrompt } : undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
registerTeams(teams);
|
|
157
|
+
if (Object.keys(teams).length > 0)
|
|
158
|
+
logger?.info("sema_registry_teams", { teams: Object.keys(teams) });
|
|
159
|
+
const projected = projectCollabToWorkflows(eff.collab?.templates);
|
|
160
|
+
registerCollabWorkflows(projected.workflows);
|
|
161
|
+
if (Object.keys(projected.workflows).length > 0)
|
|
162
|
+
logger?.info("sema_registry_collab_workflows", { workflows: Object.keys(projected.workflows) });
|
|
163
|
+
if (projected.skipped.length > 0)
|
|
164
|
+
logger?.warn("sema_registry_collab_skipped", { skipped: projected.skipped });
|
|
165
|
+
if (projected.notes.length > 0)
|
|
166
|
+
logger?.info("sema_registry_collab_notes", { notes: projected.notes });
|
|
167
|
+
}
|
|
168
|
+
const projDomain = eff.projects;
|
|
169
|
+
const rawProjects = (projDomain && typeof projDomain === "object" && !Array.isArray(projDomain)
|
|
170
|
+
? "projects" in projDomain && typeof projDomain.projects === "object"
|
|
171
|
+
? (projDomain.projects ?? {})
|
|
172
|
+
: projDomain
|
|
173
|
+
: {}) ?? {};
|
|
174
|
+
const projects = {};
|
|
175
|
+
for (const [pid, reg] of Object.entries(rawProjects)) {
|
|
176
|
+
if (!reg || typeof reg !== "object" || Array.isArray(reg))
|
|
177
|
+
continue;
|
|
178
|
+
const r = reg;
|
|
179
|
+
projects[pid] = {
|
|
180
|
+
...(typeof r.displayName === "string" ? { displayName: r.displayName } : {}),
|
|
181
|
+
...(Array.isArray(r.gitRemotes) ? { gitRemotes: r.gitRemotes.filter((x) => typeof x === "string") } : {}),
|
|
182
|
+
...(Array.isArray(r.defaultScopes) ? { defaultScopes: r.defaultScopes.filter((x) => typeof x === "string") } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
mutateInPlace(config.projects, projects);
|
|
186
|
+
if (Object.keys(projects).length > 0)
|
|
187
|
+
logger?.info("sema_registry_projects", { projects: Object.keys(projects) });
|
|
188
|
+
}
|
|
189
|
+
export const RUNTIME_GATE_KEYS = ["rateLimitPerMin", "approvalRequire", "maxTaskCostUsd", "maxTaskTokens", "maxPrincipalCostUsd", "costQuotaWindowSec"];
|
|
190
|
+
export function runtimeGatePresent(rt, key) {
|
|
191
|
+
const v = rt[key];
|
|
192
|
+
if (key === "approvalRequire")
|
|
193
|
+
return Array.isArray(v);
|
|
194
|
+
if (key === "costQuotaWindowSec")
|
|
195
|
+
return typeof v === "number" && v > 0;
|
|
196
|
+
return typeof v === "number";
|
|
197
|
+
}
|
|
198
|
+
export function applyRuntimeGates(config, rt, logger) {
|
|
199
|
+
if (!rt)
|
|
200
|
+
return;
|
|
201
|
+
const applied = {};
|
|
202
|
+
for (const key of RUNTIME_GATE_KEYS) {
|
|
203
|
+
if (!runtimeGatePresent(rt, key))
|
|
204
|
+
continue;
|
|
205
|
+
config[key] = rt[key];
|
|
206
|
+
applied[key] = rt[key];
|
|
207
|
+
}
|
|
208
|
+
if (Object.keys(applied).length > 0)
|
|
209
|
+
logger?.info("sema_registry_runtime", applied);
|
|
210
|
+
}
|
|
211
|
+
export function applyRuntimeHot(config, rt, logger) {
|
|
212
|
+
const applied = {};
|
|
213
|
+
const envAutonomy = parseAutonomy(process.env.AUTONOMY);
|
|
214
|
+
const nextAutonomy = rt?.autonomy !== undefined ? rt.autonomy : envAutonomy;
|
|
215
|
+
if (config.autonomy !== nextAutonomy) {
|
|
216
|
+
config.autonomy = nextAutonomy;
|
|
217
|
+
applied.autonomy = nextAutonomy ?? "(env-baseline)";
|
|
218
|
+
}
|
|
219
|
+
if (rt?.commandPolicy !== undefined) {
|
|
220
|
+
const errors = validateCommandRules(rt.commandPolicy);
|
|
221
|
+
if (errors.length > 0) {
|
|
222
|
+
logger?.error("sema_registry_commandpolicy_invalid", { errors, kept: config.commandPolicy?.length ?? 0 });
|
|
223
|
+
}
|
|
224
|
+
else if (JSON.stringify(config.commandPolicy) !== JSON.stringify(rt.commandPolicy)) {
|
|
225
|
+
config.commandPolicy = rt.commandPolicy;
|
|
226
|
+
applied.commandPolicy = rt.commandPolicy.length;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (config.commandPolicy !== undefined) {
|
|
230
|
+
config.commandPolicy = undefined;
|
|
231
|
+
applied.commandPolicy = "(env-baseline)";
|
|
232
|
+
}
|
|
233
|
+
if (Object.keys(applied).length > 0)
|
|
234
|
+
logger?.info("sema_registry_runtime_hot", applied);
|
|
235
|
+
}
|
|
236
|
+
const DEFAULT_ROLE_TIER_LADDER = ["pro", "flash", "lite"];
|
|
237
|
+
export function resolveDefaultModelName(eff, has, fallback, onDangling) {
|
|
238
|
+
const activeTiers = resolveActiveTiers(eff.models) ?? {};
|
|
239
|
+
const structural = eff.models?.default;
|
|
240
|
+
const rolesDefault = eff.models?.roles?.default?.model;
|
|
241
|
+
const candidates = [
|
|
242
|
+
{ source: "models.default", name: typeof structural === "string" && structural.length > 0 ? structural : undefined },
|
|
243
|
+
{ source: "roles.default", name: typeof rolesDefault === "string" && rolesDefault.length > 0 ? rolesDefault : undefined },
|
|
244
|
+
...DEFAULT_ROLE_TIER_LADDER.map((t) => ({ source: `tierGroup.${t}`, name: activeTiers[t] })),
|
|
245
|
+
];
|
|
246
|
+
for (const c of candidates) {
|
|
247
|
+
if (!c.name)
|
|
248
|
+
continue;
|
|
249
|
+
if (has(c.name))
|
|
250
|
+
return { name: c.name, source: c.source };
|
|
251
|
+
onDangling?.(c.source, c.name);
|
|
252
|
+
}
|
|
253
|
+
return { name: fallback, source: "enabled[0]" };
|
|
254
|
+
}
|
|
255
|
+
export function logEffectiveDiff(config, eff, logger) {
|
|
256
|
+
const enabled = (eff.models?.models ?? []).filter((m) => m.enabled !== false);
|
|
257
|
+
const centerModels = enabled.map((m) => ({ name: m.name, id: m.id, provider: m.provider, apiKeyEnv: m.apiKeyEnv ?? null, tier: m.tier ?? null }));
|
|
258
|
+
const envModels = Object.entries(config.models).map(([name, m]) => ({ name, id: m.id, provider: m.provider }));
|
|
259
|
+
const centerRoles = Object.fromEntries(Object.entries(eff.models?.roles ?? {}).map(([r, t]) => [r, "model" in t ? t.model : { select: t.select }]));
|
|
260
|
+
const centerTeams = (eff.teams?.teams ?? []).filter((t) => t.enabled !== false).map((t) => t.name);
|
|
261
|
+
logger?.warn("sema_registry_dry_run", {
|
|
262
|
+
note: "DRY RUN — registry config NOT applied; unset SEMA_REGISTRY_DRY_RUN to go live",
|
|
263
|
+
version: eff.version,
|
|
264
|
+
wouldOverrideModels: enabled.length > 0,
|
|
265
|
+
wouldDefaultModel: enabled.length > 0 ? resolveDefaultModelName(eff, (n) => enabled.some((m) => m.name === n), enabled[0].name).name : null,
|
|
266
|
+
currentDefaultModel: config.model.id,
|
|
267
|
+
models: { center: centerModels, envDerived: envModels },
|
|
268
|
+
roles: { center: centerRoles, current: config.roles },
|
|
269
|
+
teams: { centerWouldRegister: centerTeams },
|
|
270
|
+
skills: { centerWouldLoad: (eff.skills?.skills ?? []).filter((s) => s.enabled !== false).map((s) => s.name) },
|
|
271
|
+
mcp: { centerWouldRegister: (eff.mcp?.servers ?? []).filter((s) => s.enabled !== false).map((s) => s.name) },
|
|
272
|
+
runtime: {
|
|
273
|
+
center: eff.runtime ?? null,
|
|
274
|
+
current: Object.fromEntries(RUNTIME_GATE_KEYS.map((k) => [k, config[k]])),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
export function runtimeHasActiveGate(rt) {
|
|
279
|
+
if (!rt)
|
|
280
|
+
return false;
|
|
281
|
+
return RUNTIME_GATE_KEYS.some((k) => runtimeGatePresent(rt, k));
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=apply-effective.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntitlementRuntimeCaps } from "@sema-agent/registry-core";
|
|
2
|
+
import type { ScenarioRuling } from "../capabilities/scenarios.js";
|
|
3
|
+
import type { EffectiveConfig, ExecutionRuling } from "./types.js";
|
|
4
|
+
export declare function fetchEffective(baseUrl: string, token: string, etag: string | undefined, fetchImpl?: typeof fetch, worker?: string): Promise<{
|
|
5
|
+
effective: EffectiveConfig;
|
|
6
|
+
etag?: string;
|
|
7
|
+
} | null>;
|
|
8
|
+
export declare function fetchPrincipalCaps(baseUrl: string, token: string, principal: string, etag: string | undefined, fetchImpl?: typeof fetch, worker?: string): Promise<{
|
|
9
|
+
runtimeCaps: EntitlementRuntimeCaps | null;
|
|
10
|
+
configured: boolean;
|
|
11
|
+
scenario?: ScenarioRuling;
|
|
12
|
+
execution?: ExecutionRuling;
|
|
13
|
+
executionDrift?: string;
|
|
14
|
+
etag?: string;
|
|
15
|
+
} | null>;
|
|
16
|
+
export declare class ConfigCenterHttpError extends Error {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
constructor(message: string, status: number);
|
|
19
|
+
}
|
|
20
|
+
export declare function fetchSkillContent(baseUrl: string, token: string, contentHash: string, fetchImpl?: typeof fetch): Promise<string>;
|
|
21
|
+
export declare function fetchPromptArtifact(baseUrl: string, token: string, artifactDigest: string, fetchImpl?: typeof fetch): Promise<unknown>;
|
|
22
|
+
export declare function fetchPromptBlob(baseUrl: string, token: string, contentDigest: string, fetchImpl?: typeof fetch): Promise<string>;
|
|
23
|
+
//# sourceMappingURL=http-client.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, worker) {
|
|
3
|
+
const scheme = new URL(baseUrl).protocol;
|
|
4
|
+
if (scheme !== "http:" && scheme !== "https:")
|
|
5
|
+
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
6
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective${worker ? `?worker=${encodeURIComponent(worker)}` : ""}`;
|
|
7
|
+
const res = await fetchImpl(url, {
|
|
8
|
+
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
9
|
+
signal: AbortSignal.timeout(8000),
|
|
10
|
+
});
|
|
11
|
+
if (res.status === 304)
|
|
12
|
+
return null;
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
throw new Error(`sema-registry HTTP ${res.status}`);
|
|
15
|
+
return { effective: (await res.json()), etag: res.headers.get("etag") ?? undefined };
|
|
16
|
+
}
|
|
17
|
+
export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchImpl = fetch, worker) {
|
|
18
|
+
const scheme = new URL(baseUrl).protocol;
|
|
19
|
+
if (scheme !== "http:" && scheme !== "https:")
|
|
20
|
+
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
21
|
+
const params = new URLSearchParams({ principal });
|
|
22
|
+
if (worker)
|
|
23
|
+
params.set("worker", worker);
|
|
24
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
|
|
25
|
+
const res = await fetchImpl(url, {
|
|
26
|
+
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
27
|
+
signal: AbortSignal.timeout(8000),
|
|
28
|
+
});
|
|
29
|
+
if (res.status === 304) {
|
|
30
|
+
if (!etag)
|
|
31
|
+
throw new Error("sema-registry returned 304 to a non-conditional principal-caps request");
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
throw new Error(`sema-registry principal-caps HTTP ${res.status}`);
|
|
36
|
+
const body = (await res.json());
|
|
37
|
+
return {
|
|
38
|
+
runtimeCaps: body.runtimeCaps ?? null,
|
|
39
|
+
configured: Boolean(body.configured),
|
|
40
|
+
...("scenario" in body || "allowlist" in body
|
|
41
|
+
? {
|
|
42
|
+
scenario: {
|
|
43
|
+
scenario: typeof body.scenario === "string" ? body.scenario : null,
|
|
44
|
+
allowlist: Array.isArray(body.allowlist) ? body.allowlist.filter((s) => typeof s === "string") : [],
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
: {}),
|
|
48
|
+
...((() => {
|
|
49
|
+
const ex = body.execution;
|
|
50
|
+
if (ex === undefined || ex === null)
|
|
51
|
+
return {};
|
|
52
|
+
if (typeof ex.required !== "boolean")
|
|
53
|
+
return { executionDrift: `execution.required is ${typeof ex.required}, expected boolean — ruling dropped (fail-open)` };
|
|
54
|
+
if (!Array.isArray(ex.allowedLanes))
|
|
55
|
+
return { executionDrift: `execution.allowedLanes is ${typeof ex.allowedLanes}, expected string[] — ruling dropped (fail-open)` };
|
|
56
|
+
const execution = { required: ex.required, allowedLanes: ex.allowedLanes.filter((s) => typeof s === "string") };
|
|
57
|
+
if (ex.sessionMirror !== undefined && ex.sessionMirror !== null) {
|
|
58
|
+
const sm = ex.sessionMirror;
|
|
59
|
+
const smOk = typeof sm === "object" && typeof sm.engineUrl === "string" && sm.engineUrl.length > 0 && (sm.required === undefined || typeof sm.required === "boolean");
|
|
60
|
+
if (smOk)
|
|
61
|
+
execution.sessionMirror = { engineUrl: sm.engineUrl, required: sm.required === true };
|
|
62
|
+
else
|
|
63
|
+
return { execution, executionDrift: "execution.sessionMirror malformed — mirror observation dropped (fail-open; lane ruling kept)" };
|
|
64
|
+
}
|
|
65
|
+
return { execution };
|
|
66
|
+
})()),
|
|
67
|
+
etag: res.headers.get("etag") ?? undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export class ConfigCenterHttpError extends Error {
|
|
71
|
+
status;
|
|
72
|
+
constructor(message, status) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.status = status;
|
|
75
|
+
this.name = "ConfigCenterHttpError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function fetchSkillContent(baseUrl, token, contentHash, fetchImpl = fetch) {
|
|
79
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/skills/content/${encodeURIComponent(contentHash)}`;
|
|
80
|
+
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw new ConfigCenterHttpError(`skill content HTTP ${res.status} for ${contentHash}`, res.status);
|
|
83
|
+
const content = await res.text();
|
|
84
|
+
const want = contentHash.replace(/^sha256:/, "").toLowerCase();
|
|
85
|
+
const got = createHash("sha256").update(content, "utf8").digest("hex");
|
|
86
|
+
if (got !== want)
|
|
87
|
+
throw new Error(`skill content hash mismatch for ${contentHash}: computed sha256:${got}`);
|
|
88
|
+
return content;
|
|
89
|
+
}
|
|
90
|
+
export async function fetchPromptArtifact(baseUrl, token, artifactDigest, fetchImpl = fetch) {
|
|
91
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/prompts/epoch-artifacts/${encodeURIComponent(artifactDigest)}`;
|
|
92
|
+
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
throw new ConfigCenterHttpError(`prompt artifact HTTP ${res.status} for ${artifactDigest}`, res.status);
|
|
95
|
+
return await res.json();
|
|
96
|
+
}
|
|
97
|
+
export async function fetchPromptBlob(baseUrl, token, contentDigest, fetchImpl = fetch) {
|
|
98
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/prompts/blobs/${encodeURIComponent(contentDigest)}`;
|
|
99
|
+
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw new ConfigCenterHttpError(`prompt blob HTTP ${res.status} for ${contentDigest}`, res.status);
|
|
102
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
103
|
+
const want = contentDigest.replace(/^sha256:/, "").toLowerCase();
|
|
104
|
+
const got = createHash("sha256").update(bytes).digest("hex");
|
|
105
|
+
if (got !== want)
|
|
106
|
+
throw new Error(`prompt blob hash mismatch for ${contentDigest}: computed sha256:${got}`);
|
|
107
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=http-client.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EffectiveConfig } from "./types.js";
|
|
2
|
+
export type RestartSlice = "skills" | "mcp" | "scenarios" | "runtime-gates" | "models-tiers";
|
|
3
|
+
export interface RestartSignal {
|
|
4
|
+
restartRequired: true;
|
|
5
|
+
reasons: RestartSlice[];
|
|
6
|
+
version: number;
|
|
7
|
+
since: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function restartReasons(boot: EffectiveConfig | undefined, current: EffectiveConfig): RestartSlice[];
|
|
10
|
+
export declare function planeHasActiveTiers(eff: EffectiveConfig): boolean;
|
|
11
|
+
export declare function modelPlaneChanged(prev: EffectiveConfig | undefined, next: EffectiveConfig): boolean;
|
|
12
|
+
//# sourceMappingURL=restart-signal.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { resolveActiveTiers } from "@sema-agent/registry-core";
|
|
2
|
+
import { RUNTIME_GATE_KEYS, runtimeGatePresent, resolveDefaultModelName } from "./apply-effective.js";
|
|
3
|
+
const RESTART_SLICES = ["skills", "mcp", "scenarios", "runtime-gates", "models-tiers"];
|
|
4
|
+
function stableStringify(v) {
|
|
5
|
+
if (v === null || typeof v !== "object")
|
|
6
|
+
return JSON.stringify(v) ?? "null";
|
|
7
|
+
if (Array.isArray(v))
|
|
8
|
+
return `[${v.map(stableStringify).join(",")}]`;
|
|
9
|
+
const obj = v;
|
|
10
|
+
return `{${Object.keys(obj)
|
|
11
|
+
.sort()
|
|
12
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
13
|
+
.join(",")}}`;
|
|
14
|
+
}
|
|
15
|
+
function enabledOnly(rows) {
|
|
16
|
+
if (!rows)
|
|
17
|
+
return null;
|
|
18
|
+
return rows.filter((r) => r.enabled !== false);
|
|
19
|
+
}
|
|
20
|
+
function restartSliceValue(eff, slice) {
|
|
21
|
+
if (!eff)
|
|
22
|
+
return null;
|
|
23
|
+
switch (slice) {
|
|
24
|
+
case "skills":
|
|
25
|
+
return enabledOnly(eff.skills?.skills);
|
|
26
|
+
case "mcp":
|
|
27
|
+
return enabledOnly(eff.mcp?.servers);
|
|
28
|
+
case "scenarios":
|
|
29
|
+
return enabledOnly(eff.scenarios?.scenarios);
|
|
30
|
+
case "runtime-gates": {
|
|
31
|
+
const rt = eff.runtime;
|
|
32
|
+
if (!rt)
|
|
33
|
+
return null;
|
|
34
|
+
const present = {};
|
|
35
|
+
for (const k of RUNTIME_GATE_KEYS)
|
|
36
|
+
if (runtimeGatePresent(rt, k))
|
|
37
|
+
present[k] = rt[k];
|
|
38
|
+
return Object.keys(present).length ? present : null;
|
|
39
|
+
}
|
|
40
|
+
case "models-tiers": {
|
|
41
|
+
const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
|
|
42
|
+
if (!active || Object.keys(active).length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const enabled = enabledOnly(eff.models?.models);
|
|
45
|
+
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
46
|
+
const def = resolveDefaultModelName(eff, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
47
|
+
return { models: enabled, tiers: active, default: def.name };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function restartReasons(boot, current) {
|
|
52
|
+
return RESTART_SLICES.filter((s) => stableStringify(restartSliceValue(boot, s)) !== stableStringify(restartSliceValue(current, s)));
|
|
53
|
+
}
|
|
54
|
+
export function planeHasActiveTiers(eff) {
|
|
55
|
+
const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
|
|
56
|
+
return active !== undefined && Object.keys(active).length > 0;
|
|
57
|
+
}
|
|
58
|
+
export function modelPlaneChanged(prev, next) {
|
|
59
|
+
if (!prev)
|
|
60
|
+
return true;
|
|
61
|
+
const fp = (e) => {
|
|
62
|
+
const enabled = enabledOnly(e.models?.models);
|
|
63
|
+
const active = e.models ? (resolveActiveTiers(e.models) ?? null) : null;
|
|
64
|
+
const names = new Set((enabled ?? []).map((m) => m.name));
|
|
65
|
+
const def = resolveDefaultModelName(e, (n) => names.has(n), enabled?.[0]?.name ?? "");
|
|
66
|
+
return stableStringify({ models: enabled, tiers: active, default: def.name });
|
|
67
|
+
};
|
|
68
|
+
return fp(prev) !== fp(next);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=restart-signal.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { McpServerSpec } from "@sema-agent/core";
|
|
2
|
+
import type { ScopedMcpServer } from "../config-types.js";
|
|
3
|
+
import type { Logger } from "../observability/logger.js";
|
|
4
|
+
import type { LoadedSkill } from "../capabilities/skills.js";
|
|
5
|
+
import type { CenterMcpServer, CenterSkillManifest } from "./types.js";
|
|
6
|
+
export declare function applyCenterSkills(baseline: LoadedSkill[], manifest: {
|
|
7
|
+
skills: CenterSkillManifest[];
|
|
8
|
+
}, baseUrl: string, token: string, logger?: Logger, fetchImpl?: typeof fetch, diskCacheDir?: string): Promise<LoadedSkill[]>;
|
|
9
|
+
export declare function resolveMcpServers(mcp: {
|
|
10
|
+
servers: CenterMcpServer[];
|
|
11
|
+
}, logger?: Logger): ScopedMcpServer[];
|
|
12
|
+
export declare function mcpForScenario(servers: ScopedMcpServer[] | undefined, scenario: string): McpServerSpec[] | undefined;
|
|
13
|
+
//# sourceMappingURL=skills-mcp.d.ts.map
|