@sema-agent/server 7.97.0 → 7.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +11 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +18 -9
- package/deploy/sema-up/cluster-up.sh +8 -4
- package/deploy/sema-up/kube-up.sh +40 -15
- package/deploy/sema-up/lib/envf.sh +241 -0
- package/deploy/sema-up/sema-up.sh +162 -101
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/boot/config-center.js +169 -24
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/resolve-spec.js +9 -4
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/brain.js +28 -18
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.js +7 -6
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -9
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +20 -6
- package/dist/config.d.ts +9 -0
- package/dist/config.js +53 -18
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/memory-scope.d.ts +62 -11
- package/dist/memory-scope.js +32 -3
- package/dist/model-provider.d.ts +13 -0
- package/dist/model-provider.js +3 -0
- package/dist/model-route-endpoint.d.ts +82 -0
- package/dist/model-route-endpoint.js +55 -0
- package/dist/observability/secret-env-scrub.d.ts +2 -26
- package/dist/observability/secret-env-scrub.js +2 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +8 -7
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +41 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/tool-approval.d.ts +48 -1
- package/dist/tool-approval.js +10 -3
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +4 -3
|
@@ -2,15 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/settings-schema";
|
|
3
3
|
import { centerPromptsFromEffective } from "../prompts-domain-validate.js";
|
|
4
4
|
import { foldEffectiveReadWarning } from "./read-warnings.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const res = await fetchImpl(url, {
|
|
11
|
-
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
12
|
-
signal: AbortSignal.timeout(8000),
|
|
13
|
-
});
|
|
5
|
+
import { centerFetch } from "./center-request.js";
|
|
6
|
+
import { answerStillCurrent } from "../center-credential.js";
|
|
7
|
+
const CENTER_PULL_TIMEOUT_MS = 8000;
|
|
8
|
+
export async function fetchEffective(center, etag, fetchImpl = fetch, worker) {
|
|
9
|
+
const { res, identity } = await centerFetch(center, { kind: "effective", worker }, { etag, timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
|
|
14
10
|
if (res.status === 304)
|
|
15
11
|
return null;
|
|
16
12
|
if (!res.ok)
|
|
@@ -29,10 +25,10 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
|
|
|
29
25
|
domainErrors.push(f);
|
|
30
26
|
}
|
|
31
27
|
const effective = wire;
|
|
32
|
-
return { effective, etag: res.headers.get("etag") ?? undefined, readWarnings: warnings, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
|
|
28
|
+
return { effective, etag: res.headers.get("etag") ?? undefined, readWarnings: warnings, credentialIdentity: identity, ...(domainErrors.length > 0 ? { domainErrors } : {}) };
|
|
33
29
|
}
|
|
34
|
-
export async function fetchPrincipalCaps(
|
|
35
|
-
const raw = await requestPrincipalView(
|
|
30
|
+
export async function fetchPrincipalCaps(center, principal, etag, fetchImpl = fetch, worker) {
|
|
31
|
+
const raw = await requestPrincipalView(center, principal, etag, fetchImpl, worker);
|
|
36
32
|
if (raw === null)
|
|
37
33
|
return null;
|
|
38
34
|
const parsedBody = PrincipalCapsWire.safeParse(raw.json);
|
|
@@ -84,31 +80,29 @@ function assertPrincipalEcho(echoed, principal) {
|
|
|
84
80
|
throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(echoed)} but ${JSON.stringify(principal)} was requested — discarding the whole response`);
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
|
-
async function requestPrincipalView(
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
|
|
95
|
-
const res = await fetchImpl(url, {
|
|
96
|
-
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
97
|
-
signal: AbortSignal.timeout(8000),
|
|
98
|
-
});
|
|
83
|
+
async function requestPrincipalView(center, principal, etag, fetchImpl, worker) {
|
|
84
|
+
const { res, identity } = await centerFetch(center, { kind: "effective", principal, worker }, { etag, timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
|
|
85
|
+
const stillCurrent = () => {
|
|
86
|
+
if (!answerStillCurrent(center.credential, identity)) {
|
|
87
|
+
throw new Error("center credential identity changed while the principal view was in flight — response discarded; the next lookup runs under the current credential");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
99
90
|
if (res.status === 304) {
|
|
100
91
|
if (!etag)
|
|
101
92
|
throw new Error("config-center returned 304 to a non-conditional principal-caps request");
|
|
93
|
+
stillCurrent();
|
|
102
94
|
return null;
|
|
103
95
|
}
|
|
104
96
|
if (!res.ok)
|
|
105
97
|
throw new Error(`config-center principal-caps HTTP ${res.status}`);
|
|
106
98
|
const responseEtag = res.headers.get("etag");
|
|
107
|
-
|
|
99
|
+
const json = await res.json();
|
|
100
|
+
stillCurrent();
|
|
101
|
+
return { json, ...(responseEtag !== null ? { etag: responseEtag } : {}) };
|
|
108
102
|
}
|
|
109
103
|
const PrincipalOrgMemoryEnvelope = PrincipalCapsWire.pick({ principal: true, orgMemory: true });
|
|
110
|
-
export async function fetchPrincipalOrgMemory(
|
|
111
|
-
const raw = await requestPrincipalView(
|
|
104
|
+
export async function fetchPrincipalOrgMemory(center, principal, fetchImpl = fetch, worker) {
|
|
105
|
+
const raw = await requestPrincipalView(center, principal, undefined, fetchImpl, worker);
|
|
112
106
|
if (raw === null)
|
|
113
107
|
throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
|
|
114
108
|
const parsed = PrincipalOrgMemoryEnvelope.safeParse(raw.json);
|
|
@@ -126,9 +120,8 @@ export class ConfigCenterHttpError extends Error {
|
|
|
126
120
|
this.name = "ConfigCenterHttpError";
|
|
127
121
|
}
|
|
128
122
|
}
|
|
129
|
-
export async function fetchSkillContent(
|
|
130
|
-
const
|
|
131
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
123
|
+
export async function fetchSkillContent(center, contentHash, fetchImpl = fetch) {
|
|
124
|
+
const { res } = await centerFetch(center, { kind: "skill-content", contentHash }, { timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
|
|
132
125
|
if (!res.ok)
|
|
133
126
|
throw new ConfigCenterHttpError(`skill content HTTP ${res.status} for ${contentHash}`, res.status);
|
|
134
127
|
const content = await res.text();
|
|
@@ -138,16 +131,14 @@ export async function fetchSkillContent(baseUrl, token, contentHash, fetchImpl =
|
|
|
138
131
|
throw new Error(`skill content hash mismatch for ${contentHash}: computed sha256:${got}`);
|
|
139
132
|
return content;
|
|
140
133
|
}
|
|
141
|
-
export async function fetchPromptArtifact(
|
|
142
|
-
const
|
|
143
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
134
|
+
export async function fetchPromptArtifact(center, artifactDigest, fetchImpl = fetch) {
|
|
135
|
+
const { res } = await centerFetch(center, { kind: "prompt-artifact", digest: artifactDigest }, { timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
|
|
144
136
|
if (!res.ok)
|
|
145
137
|
throw new ConfigCenterHttpError(`prompt artifact HTTP ${res.status} for ${artifactDigest}`, res.status);
|
|
146
138
|
return await res.json();
|
|
147
139
|
}
|
|
148
|
-
export async function fetchPromptBlob(
|
|
149
|
-
const
|
|
150
|
-
const res = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(8000) });
|
|
140
|
+
export async function fetchPromptBlob(center, contentDigest, fetchImpl = fetch) {
|
|
141
|
+
const { res } = await centerFetch(center, { kind: "prompt-blob", digest: contentDigest }, { timeoutMs: CENTER_PULL_TIMEOUT_MS, fetchImpl });
|
|
151
142
|
if (!res.ok)
|
|
152
143
|
throw new ConfigCenterHttpError(`prompt blob HTTP ${res.status} for ${contentDigest}`, res.status);
|
|
153
144
|
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
@@ -2,6 +2,7 @@ import type { A2aServerSpec, McpServerSpec } from "@sema-agent/core";
|
|
|
2
2
|
import type { CenterMcp, ScopedA2aPeer } from "../config-types.js";
|
|
3
3
|
import type { Logger } from "../observability/logger.js";
|
|
4
4
|
import type { LoadedSkill } from "../capabilities/skills.js";
|
|
5
|
+
import type { CenterEndpoint } from "./center-request.js";
|
|
5
6
|
import type { CenterA2aPeer, CenterMcpServer, CenterSkillManifest } from "./types.js";
|
|
6
7
|
/**
|
|
7
8
|
* Merge center skills OVER the image baseline ([ref]-sibling B1; `sema-registry docs/MCP-SKILLS.md`). Load order =
|
|
@@ -13,7 +14,9 @@ import type { CenterA2aPeer, CenterMcpServer, CenterSkillManifest } from "./type
|
|
|
13
14
|
*/
|
|
14
15
|
export declare function applyCenterSkills(baseline: LoadedSkill[], manifest: {
|
|
15
16
|
skills: CenterSkillManifest[];
|
|
16
|
-
},
|
|
17
|
+
},
|
|
18
|
+
/** S-651:中心端点(基址 + 凭证来源;每次正文拉取取一次值)。 */
|
|
19
|
+
center: CenterEndpoint, logger?: Logger, fetchImpl?: typeof fetch,
|
|
17
20
|
/** LKG 姊妹面(clay 拍 2026-07-18):skill 正文的 content-addressed 盘缓存目录。读序=内存→盘(验
|
|
18
21
|
* hash,坏文件忽略)→中心(成功后 best-effort 回写)。让 LKG-boot 在中心不可达时不再于正文拉取上
|
|
19
22
|
* 挂数秒(live 实测黑洞 +5.3s/skill 批),顺带省正常 refresh 的重复拉取。undefined=无盘缓存(原行为)。 */
|
|
@@ -5,7 +5,7 @@ import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } f
|
|
|
5
5
|
import { fetchSkillContent } from "./http-client.js";
|
|
6
6
|
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
7
7
|
import { describeBadWireName, isUsableWireName } from "../wire-name-bound.js";
|
|
8
|
-
export async function applyCenterSkills(baseline, manifest,
|
|
8
|
+
export async function applyCenterSkills(baseline, manifest, center, logger, fetchImpl = fetch, diskCacheDir) {
|
|
9
9
|
const { promises: fsp } = await import("node:fs");
|
|
10
10
|
const { join: joinPath } = await import("node:path");
|
|
11
11
|
const diskRead = async (hash) => {
|
|
@@ -50,7 +50,7 @@ export async function applyCenterSkills(baseline, manifest, baseUrl, token, logg
|
|
|
50
50
|
if (content === undefined)
|
|
51
51
|
content = await diskRead(m.contentHash);
|
|
52
52
|
if (content === undefined) {
|
|
53
|
-
content = await fetchSkillContent(
|
|
53
|
+
content = await fetchSkillContent(center, m.contentHash, fetchImpl);
|
|
54
54
|
diskWrite(m.contentHash, content);
|
|
55
55
|
}
|
|
56
56
|
contentCache.set(m.contentHash, content);
|
|
@@ -28,9 +28,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const LIVENESS_HEARTBEAT_MS = 30000;
|
|
30
30
|
/** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
|
|
31
|
-
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "
|
|
31
|
+
export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "model-route-endpoint" | "require-execution-lane";
|
|
32
32
|
/** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
|
|
33
|
-
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "
|
|
33
|
+
export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "models">>;
|
|
34
34
|
export interface CrossInvariantInput {
|
|
35
35
|
/** 现役配置(boot 期 = 候选自身)。 */
|
|
36
36
|
readonly live: CrossInvariantView;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assertGuardPatternsUsable } from "./deployment-governance.js";
|
|
2
|
-
import { hostedPrincipalRefusal } from "./hosted-posture.js";
|
|
3
2
|
import { modelProviderRefusal } from "./model-provider.js";
|
|
4
3
|
import { modelRouteUrlRefusal } from "./model-route-url.js";
|
|
4
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
5
5
|
export const LIVENESS_HEARTBEAT_MS = 30_000;
|
|
6
6
|
const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
|
|
7
7
|
const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
|
|
@@ -81,13 +81,6 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
81
81
|
? "IMAGE_BAKES_ENABLED with an EMPTY OPERATOR_PRINCIPALS is a world-writable bake door (the runner runs build.sh on the privileged build host)"
|
|
82
82
|
: undefined,
|
|
83
83
|
},
|
|
84
|
-
{
|
|
85
|
-
id: "hosted-requires-principal",
|
|
86
|
-
keys: ["requirePrincipal", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud", "authBridgeIssuer"],
|
|
87
|
-
restartReason: "hosted-posture",
|
|
88
|
-
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:hosted-requires-principal" },
|
|
89
|
-
check: ({ next }) => hostedPrincipalRefusal(next),
|
|
90
|
-
},
|
|
91
84
|
{
|
|
92
85
|
id: "sensitive-write-patterns",
|
|
93
86
|
keys: ["sensitiveWritePatterns"],
|
|
@@ -160,12 +153,19 @@ export const CONFIG_CROSS_INVARIANTS = [
|
|
|
160
153
|
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic"],
|
|
161
154
|
restartReason: "model-route-url",
|
|
162
155
|
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:gateway-url-userinfo" },
|
|
163
|
-
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl, "MODEL_API_KEY") ??
|
|
156
|
+
check: ({ next }) => modelRouteUrlRefusal("MODEL_GATEWAY_BASEURL", next.gatewayBaseUrl ?? "", "MODEL_API_KEY") ??
|
|
164
157
|
next.gatewayFallbackUrls
|
|
165
158
|
.map((u, i) => modelRouteUrlRefusal(`MODEL_GATEWAY_FALLBACK_URLS[${i}]`, u, "MODEL_API_KEY"))
|
|
166
159
|
.find((r) => r !== undefined) ??
|
|
167
160
|
modelRouteUrlRefusal("ANTHROPIC_BASE_URL", next.anthropic?.baseUrl ?? "", "ANTHROPIC_API_KEY"),
|
|
168
161
|
},
|
|
162
|
+
{
|
|
163
|
+
id: "model-route-endpoint",
|
|
164
|
+
keys: ["gatewayBaseUrl", "gatewayFallbackUrls", "anthropic", "models"],
|
|
165
|
+
restartReason: "model-route-endpoint",
|
|
166
|
+
bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:model-route-endpoint" },
|
|
167
|
+
check: ({ live, next }) => (next.models === live.models ? undefined : modelRouteEndpointRefusal(next)),
|
|
168
|
+
},
|
|
169
169
|
];
|
|
170
170
|
export function crossInvariant(id) {
|
|
171
171
|
const inv = CONFIG_CROSS_INVARIANTS.find((i) => i.id === id);
|
package/dist/config-lkg.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/** 落盘信封。`effective`/`etag` 逐字=当时的 fetchEffective 返回(JSON verbatim 面)。 */
|
|
2
2
|
export interface PersistedLkg {
|
|
3
|
-
formatVersion:
|
|
3
|
+
formatVersion: 2;
|
|
4
4
|
savedAt: string;
|
|
5
5
|
worker: string;
|
|
6
|
+
/** 拉到这份候选时所用凭证的身份(`center-credential.ts`;摘要形,不含凭证原文)。 */
|
|
7
|
+
identity: string;
|
|
6
8
|
etag?: string;
|
|
7
9
|
effective: Record<string, unknown>;
|
|
8
10
|
}
|
|
@@ -10,21 +12,58 @@ export interface PersistedLkg {
|
|
|
10
12
|
* hash 另拉)——黑洞中心下 LKG-boot 会在 skill 正文拉取上再挂数秒(live 实测 +5.3s)。正文按
|
|
11
13
|
* `<dir>/<sha-hex>` 落盘,读侧**必验 hash**(content-addressed 的全部意义;坏文件=忽略重拉)。 */
|
|
12
14
|
export declare function defaultSkillCacheDir(): string;
|
|
13
|
-
/** 默认路径(scheduler store 同门:SEMA_CONFIG_DIR → ~/.sema)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
export declare function defaultLkgPath(worker: string | undefined): string;
|
|
15
|
+
/** 默认路径(scheduler store 同门:SEMA_CONFIG_DIR → ~/.sema),按「worker + 凭证身份」分箱。`CONFIG_LKG_PATH`
|
|
16
|
+
* 覆写(单一路径 ⇒ 不分箱,信封的 worker + identity 核仍守读回);`CONFIG_LKG_ENABLED=false` 关闭(读写双关)。
|
|
17
|
+
* 🪦 正名前的 `CONFIG_LKG_DISABLED` 自 server 3.0.0 起是**fail-loud 墓碑**——设了(任何值)即拒启并指路新名,不再被读取。 */
|
|
18
|
+
export declare function defaultLkgPath(worker: string | undefined, identity: string): string;
|
|
17
19
|
/** 原子落盘一个干净候选(tmp+rename,0600)。BEST-EFFORT:失败=一条 warn 的素材(调用方发),绝不
|
|
18
20
|
* 影响在飞的应用链——LKG 缓存丢失的代价只是下次冷启动回到 env 兜底。 */
|
|
19
|
-
export declare function saveLkg(path: string, worker: string | undefined, effective: Record<string, unknown>, etag: string | undefined): Promise<void>;
|
|
21
|
+
export declare function saveLkg(path: string, worker: string | undefined, identity: string, effective: Record<string, unknown>, etag: string | undefined): Promise<void>;
|
|
20
22
|
/**
|
|
21
23
|
* 读盘上 LKG。返回 undefined 的所有情形都「安静地回 env 兜底」(调用方 warn 点名 reason):
|
|
22
|
-
* 文件缺席/JSON 坏/信封形不对/worker
|
|
24
|
+
* 文件缺席/JSON 坏/信封形不对/worker 或凭证身份不匹配(防同名文件跨 worker / 跨账号串包;`CONFIG_LKG_PATH`
|
|
25
|
+
* 单一路径时这是唯一的一道)/prompts 面复判失败。
|
|
23
26
|
*/
|
|
24
|
-
export declare function loadLkg(path: string, worker: string | undefined): Promise<{
|
|
27
|
+
export declare function loadLkg(path: string, worker: string | undefined, identity: string): Promise<{
|
|
25
28
|
lkg: PersistedLkg;
|
|
26
29
|
} | {
|
|
27
30
|
lkg?: undefined;
|
|
28
31
|
reason: string;
|
|
29
32
|
}>;
|
|
33
|
+
/** 7.98.0 及以前的默认 LKG 路径(按当时的写法逐字保留 —— 这是盘上已有文件的名字,不是现行分箱规则)。 */
|
|
34
|
+
export declare function legacyV1LkgPath(worker: string | undefined): string;
|
|
35
|
+
export type LegacyLkgMigration = {
|
|
36
|
+
readonly outcome: "none";
|
|
37
|
+
}
|
|
38
|
+
/** `retireError`:新箱已写好,只是旧文件没退役成(改名 / 留副本失败)—— 只响一行,不影响已写好的箱。 */
|
|
39
|
+
| {
|
|
40
|
+
readonly outcome: "migrated";
|
|
41
|
+
readonly from: string;
|
|
42
|
+
readonly retiredTo: string;
|
|
43
|
+
readonly retireError?: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly outcome: "not-migrated";
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly reason: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* formatVersion 1 LKG 的一次性迁移(规则一句:**身份推得出来才迁,推不出来就响亮拒**)。判序:
|
|
51
|
+
* 1. `legacyPath` ≠ `binPath` 时**先看本身份的箱**:箱里已有文件 ⇒ `none`(箱自己说话,旧文件是好是坏都不再相关,
|
|
52
|
+
* 不许一个坏旧文件把有效的箱挡成 env 兜底 —— 7.99.0 修复验证 N4)。箱查不了(非 ENOENT)⇒ 响亮拒。
|
|
53
|
+
* 2. 旧文件没有(ENOENT / ENOTDIR)或不是 formatVersion 1 信封 ⇒ `none`。旧默认路径上却读不了 / 不是 JSON ⇒
|
|
54
|
+
* `not-migrated`(响亮,不当它不存在);`CONFIG_LKG_PATH` 同路径时这两形由 loadLkg 自己报(不重复打一行)。
|
|
55
|
+
* 3. v1 信封只记 worker:唯一推得出身份的形是拉取令牌(身份 = worker,`pullTokenIdentity`)。此刻身份正是它、worker
|
|
56
|
+
* 对得上、载荷过 {@link lkgPayloadProblem} ⇒ **先写后退役**(修复验证 N9):先按新格式把它写进 `binPath`(原子写,
|
|
57
|
+
* 0600),写成了才把旧文件改名 `<legacyPath>.migrated-v1`。写失败 ⇒ `not-migrated`,旧文件原样留在原位,下一次
|
|
58
|
+
* 照常可迁;写成之后改名失败 ⇒ 仍是 `migrated`(`retireError` 响一行),箱已在 ⇒ 下一次按判序 1 不再看旧文件。
|
|
59
|
+
* 同路径(`CONFIG_LKG_PATH`)时新箱的写入会原子替换旧文件本身,所以旧字节在写之前留一份 `.migrated-v1` 副本
|
|
60
|
+
* (副本失败同样只进 `retireError`:载荷正要写进新箱,不是丢数据)。
|
|
61
|
+
* 4. 其余一律 `not-migrated`:拒因点名旧文件路径与出路(删掉它;中心下一份干净候选会写本身份自己的 LKG),不改名、不读回。
|
|
62
|
+
*/
|
|
63
|
+
export declare function migrateV1Lkg(o: {
|
|
64
|
+
legacyPath: string;
|
|
65
|
+
binPath: string;
|
|
66
|
+
worker: string | undefined;
|
|
67
|
+
identity: string;
|
|
68
|
+
}): Promise<LegacyLkgMigration>;
|
|
30
69
|
//# sourceMappingURL=config-lkg.d.ts.map
|
package/dist/config-lkg.js
CHANGED
|
@@ -3,22 +3,22 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { validatePromptsDomain } from "./prompts-domain-validate.js";
|
|
6
|
+
import { centerStateBin, pullTokenIdentity } from "./center-credential.js";
|
|
6
7
|
export function defaultSkillCacheDir() {
|
|
7
8
|
return join(process.env.SEMA_CONFIG_DIR ?? join(homedir(), ".sema"), "skill-cache");
|
|
8
9
|
}
|
|
9
|
-
export function defaultLkgPath(worker) {
|
|
10
|
+
export function defaultLkgPath(worker, identity) {
|
|
10
11
|
const dir = process.env.SEMA_CONFIG_DIR ?? join(homedir(), ".sema");
|
|
11
|
-
|
|
12
|
-
return join(dir, `config-lkg-${key}.json`);
|
|
12
|
+
return join(dir, `config-lkg-${centerStateBin(worker, identity)}.json`);
|
|
13
13
|
}
|
|
14
|
-
export async function saveLkg(path, worker, effective, etag) {
|
|
15
|
-
const body = { formatVersion:
|
|
14
|
+
export async function saveLkg(path, worker, identity, effective, etag) {
|
|
15
|
+
const body = { formatVersion: 2, savedAt: new Date().toISOString(), worker: worker ?? "global", identity, ...(etag !== undefined ? { etag } : {}), effective };
|
|
16
16
|
const tmp = `${path}.${randomBytes(6).toString("hex")}.tmp`;
|
|
17
17
|
await fs.mkdir(dirname(path), { recursive: true });
|
|
18
18
|
await fs.writeFile(tmp, JSON.stringify(body), { mode: 0o600 });
|
|
19
19
|
await fs.rename(tmp, path);
|
|
20
20
|
}
|
|
21
|
-
export async function loadLkg(path, worker) {
|
|
21
|
+
export async function loadLkg(path, worker, identity) {
|
|
22
22
|
let text;
|
|
23
23
|
try {
|
|
24
24
|
text = await fs.readFile(path, "utf8");
|
|
@@ -36,38 +36,125 @@ export async function loadLkg(path, worker) {
|
|
|
36
36
|
if (parsed === null || typeof parsed !== "object")
|
|
37
37
|
return { reason: "bad envelope" };
|
|
38
38
|
const p = parsed;
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const formatVersion = p.formatVersion;
|
|
40
|
+
if (formatVersion === 1)
|
|
41
|
+
return { reason: "pre-identity LKG (formatVersion 1) — not read back: it cannot prove whose credential fetched it (S-651); the next clean candidate rewrites it" };
|
|
42
|
+
if (formatVersion !== 2)
|
|
43
|
+
return { reason: `unknown formatVersion ${String(formatVersion)}` };
|
|
41
44
|
if (typeof p.worker !== "string" || p.worker !== (worker ?? "global"))
|
|
42
45
|
return { reason: `worker mismatch (file=${String(p.worker)})` };
|
|
43
|
-
if (p.
|
|
44
|
-
return { reason: "
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (typeof p.identity !== "string" || p.identity !== identity)
|
|
47
|
+
return { reason: "credential identity mismatch (the LKG was fetched under another credential identity — not read back)" };
|
|
48
|
+
const problem = lkgPayloadProblem(p.effective, p.etag);
|
|
49
|
+
return problem === undefined ? { lkg: p } : { reason: problem };
|
|
50
|
+
}
|
|
51
|
+
function lkgPayloadProblem(effective, etag) {
|
|
52
|
+
if (effective === null || typeof effective !== "object")
|
|
53
|
+
return "bad effective";
|
|
54
|
+
if (etag !== undefined && typeof etag !== "string")
|
|
55
|
+
return "bad etag";
|
|
56
|
+
const eff = effective;
|
|
48
57
|
if (typeof eff.version !== "number")
|
|
49
|
-
return
|
|
58
|
+
return "bad effective.version";
|
|
50
59
|
for (const [domain, arrayKey] of [["models", "models"], ["skills", "skills"], ["mcp", "servers"], ["scenarios", "scenarios"], ["runtime", null]]) {
|
|
51
60
|
const v = eff[domain];
|
|
52
61
|
if (v === undefined)
|
|
53
62
|
continue;
|
|
54
63
|
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
55
|
-
return
|
|
64
|
+
return `bad effective.${domain}`;
|
|
56
65
|
if (arrayKey !== null) {
|
|
57
66
|
const arr = v[arrayKey];
|
|
58
67
|
if (arr !== undefined && !Array.isArray(arr))
|
|
59
|
-
return
|
|
68
|
+
return `bad effective.${domain}.${arrayKey}`;
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
{
|
|
63
72
|
const rt = eff.runtime;
|
|
64
73
|
if (rt && (rt.autonomy !== undefined || rt.commandPolicy !== undefined || rt.approvalRequire !== undefined)) {
|
|
65
|
-
return
|
|
74
|
+
return "legacy runtime governance keys (pre-0.7.1 mirror shape; no contract producer since LKG shipped) — refusing rather than silently dropping governance";
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
|
-
const promptsRaw =
|
|
77
|
+
const promptsRaw = eff.prompts;
|
|
69
78
|
if (promptsRaw !== undefined && !validatePromptsDomain(promptsRaw).ok)
|
|
70
|
-
return
|
|
71
|
-
return
|
|
79
|
+
return "prompts face fails re-validation";
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
export function legacyV1LkgPath(worker) {
|
|
83
|
+
const dir = process.env.SEMA_CONFIG_DIR ?? join(homedir(), ".sema");
|
|
84
|
+
const key = (worker ?? "global").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 64);
|
|
85
|
+
return join(dir, `config-lkg-${key}.json`);
|
|
86
|
+
}
|
|
87
|
+
export async function migrateV1Lkg(o) {
|
|
88
|
+
const refuse = (why) => ({
|
|
89
|
+
outcome: "not-migrated",
|
|
90
|
+
path: o.legacyPath,
|
|
91
|
+
reason: `pre-identity LKG (formatVersion 1) at ${o.legacyPath} not migrated: ${why}. Way out: delete that file — the next clean candidate from the center writes this credential identity's own LKG (${o.binPath})`,
|
|
92
|
+
});
|
|
93
|
+
const sameFile = o.legacyPath === o.binPath;
|
|
94
|
+
if (!sameFile) {
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(o.binPath);
|
|
97
|
+
return { outcome: "none" };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
const code = errnoOf(err);
|
|
101
|
+
if (code !== "ENOENT")
|
|
102
|
+
return refuse(`this credential identity's bin ${o.binPath} could not be checked (${code})`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let text;
|
|
106
|
+
try {
|
|
107
|
+
text = await fs.readFile(o.legacyPath, "utf8");
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const code = errnoOf(err);
|
|
111
|
+
return code === "ENOENT" || code === "ENOTDIR" || sameFile ? { outcome: "none" } : refuse(`it could not be read (${code})`);
|
|
112
|
+
}
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(text);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
return sameFile ? { outcome: "none" } : refuse(`it is not valid JSON (${err instanceof Error ? err.name : "parse error"})`);
|
|
119
|
+
}
|
|
120
|
+
if (parsed === null || typeof parsed !== "object" || !("formatVersion" in parsed) || parsed.formatVersion !== 1)
|
|
121
|
+
return { outcome: "none" };
|
|
122
|
+
const v1 = parsed;
|
|
123
|
+
if (v1.worker !== (o.worker ?? "global"))
|
|
124
|
+
return refuse(`it belongs to worker ${JSON.stringify(v1.worker)}`);
|
|
125
|
+
if (o.identity !== pullTokenIdentity(o.worker)) {
|
|
126
|
+
return refuse("its envelope records only the worker, and only a pull-token credential's identity (= the worker) can be derived from that — the current credential is not this worker's pull token, so the file cannot prove whose credential fetched it");
|
|
127
|
+
}
|
|
128
|
+
const problem = lkgPayloadProblem(v1.effective, v1.etag);
|
|
129
|
+
if (problem !== undefined)
|
|
130
|
+
return refuse(`its payload is unusable (${problem})`);
|
|
131
|
+
const retiredTo = `${o.legacyPath}.migrated-v1`;
|
|
132
|
+
let retireError;
|
|
133
|
+
if (sameFile) {
|
|
134
|
+
try {
|
|
135
|
+
await fs.copyFile(o.legacyPath, retiredTo);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
retireError = `the formatVersion 1 bytes could not be kept as ${retiredTo} (${errnoOf(err)})`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
await saveLkg(o.binPath, o.worker, o.identity, v1.effective, typeof v1.etag === "string" ? v1.etag : undefined);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return refuse(`writing it into this credential identity's bin failed (${String(err)}) — the old file is left in place and the migration retries on the next LKG read`);
|
|
146
|
+
}
|
|
147
|
+
if (!sameFile) {
|
|
148
|
+
try {
|
|
149
|
+
await fs.rename(o.legacyPath, retiredTo);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
retireError = `the old file could not be renamed to ${retiredTo} (${errnoOf(err)}) — harmless: the bin now exists, so the old file is never read again`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { outcome: "migrated", from: o.legacyPath, retiredTo, ...(retireError !== undefined ? { retireError } : {}) };
|
|
156
|
+
}
|
|
157
|
+
function errnoOf(err) {
|
|
158
|
+
return err instanceof Error && "code" in err && typeof err.code === "string" ? err.code : "unknown";
|
|
72
159
|
}
|
|
73
160
|
//# sourceMappingURL=config-lkg.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FileConfigStore } from "@sema-agent/settings-schema/node";
|
|
2
2
|
import { type EffectiveConfig as AgentConfigEffective, type EffectiveReadWarning } from "@sema-agent/settings-schema";
|
|
3
3
|
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, type EffectiveConfig } from "./config-center/facade.js";
|
|
4
|
+
import type { CenterEndpoint } from "./config-center/center-request.js";
|
|
4
5
|
/** Result of a `fetchEffective` — EXACTLY the config-center's return: the effective config + its etag, or
|
|
5
6
|
* `null` for "unchanged" (remote 304; local: the version matched the caller's prior etag).
|
|
6
7
|
* `domainErrors`([ref] registry-core 0.10.12 tolerant seam,local lane only):catalog 域坏文件不再
|
|
@@ -17,6 +18,8 @@ import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetc
|
|
|
17
18
|
* ——缺席/空两形并存就是又一处二义。
|
|
18
19
|
* 🔐 原流的 `error` 未经 {@link redactConfigError}(那道脱敏挂在真正进日志流的 `domainErrors` 产出边界上),
|
|
19
20
|
* 路由进日志/审计流的消费者必须自己先脱敏。 */
|
|
21
|
+
/** `credentialIdentity`(S-651 / [ref] E-15):远程腿恒在场 = 这次拉取所用凭证的身份(分箱键来源);本地腿
|
|
22
|
+
* 缺席(没有凭证 —— config.d 就在本机盘上)。 */
|
|
20
23
|
export type FetchEffectiveResult = {
|
|
21
24
|
effective: EffectiveConfig;
|
|
22
25
|
etag?: string;
|
|
@@ -25,6 +28,7 @@ export type FetchEffectiveResult = {
|
|
|
25
28
|
error: string;
|
|
26
29
|
}>;
|
|
27
30
|
readWarnings: readonly EffectiveReadWarning[];
|
|
31
|
+
credentialIdentity?: string;
|
|
28
32
|
} | null;
|
|
29
33
|
/** Sentinel returned by {@link raceBootFetch} when the boot pull outlived its budget — the caller
|
|
30
34
|
* starts serving on the env fallback and the SAME promise settles in the background (never cancelled). */
|
|
@@ -67,9 +71,7 @@ export interface ConfigProviderInput {
|
|
|
67
71
|
/** Set when SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is configured (config.ts `configCenter`). `dryRun` is carried through so a
|
|
68
72
|
* remote↔local swap via this seam does NOT drop the dry-run posture main.ts branches on (config.ts
|
|
69
73
|
* `configCenter.dryRun`); the provider itself does not consume it (the caller still branches on it). */
|
|
70
|
-
configCenter?: {
|
|
71
|
-
baseUrl: string;
|
|
72
|
-
token: string;
|
|
74
|
+
configCenter?: CenterEndpoint & {
|
|
73
75
|
worker?: string;
|
|
74
76
|
dryRun?: boolean;
|
|
75
77
|
};
|
|
@@ -84,16 +86,15 @@ export interface ConfigProviderInput {
|
|
|
84
86
|
}
|
|
85
87
|
/**
|
|
86
88
|
* RemoteConfigProvider — DELEGATES to the exported config-center HTTP functions. No HTTP is reimplemented;
|
|
87
|
-
* baseUrl
|
|
89
|
+
* the center endpoint (baseUrl + credential SOURCE — S-651: read per request, never a bound token) and worker
|
|
90
|
+
* are passed through. The two delegate fns are injectable (defaulting
|
|
88
91
|
* to the real config-center exports) so a unit test can assert delegation without module-mocking.
|
|
89
92
|
*/
|
|
90
93
|
export declare class RemoteConfigProvider implements ConfigProvider {
|
|
91
94
|
private readonly cc;
|
|
92
95
|
private readonly deps;
|
|
93
96
|
readonly kind: "remote";
|
|
94
|
-
constructor(cc: {
|
|
95
|
-
baseUrl: string;
|
|
96
|
-
token: string;
|
|
97
|
+
constructor(cc: CenterEndpoint & {
|
|
97
98
|
worker?: string;
|
|
98
99
|
}, deps?: {
|
|
99
100
|
fetchEffective?: typeof remoteFetchEffective;
|
package/dist/config-provider.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readMcpSourceLabel } from "./mcp-source-label.js";
|
|
|
2
2
|
import { createHmac, randomBytes } from "node:crypto";
|
|
3
3
|
import { foldEffectiveReadWarning } from "./config-center/read-warnings.js";
|
|
4
4
|
import { FileConfigStore } from "@sema-agent/settings-schema/node";
|
|
5
|
-
import { findSkillContent, skillContentHash, refIntegrityIssues, createSiblingResolver, } from "@sema-agent/settings-schema";
|
|
5
|
+
import { findSkillContent, skillContentHash, refIntegrityIssues, createSiblingResolver, formatConfigPath, REF_CHECKED_DOMAINS, } from "@sema-agent/settings-schema";
|
|
6
6
|
import { redactSecrets } from "./trace/redact.js";
|
|
7
7
|
import { fetchEffective as remoteFetchEffective, fetchSkillContent as remoteFetchSkillContent, ConfigCenterHttpError, } from "./config-center/facade.js";
|
|
8
8
|
import { createLogger } from "./observability/logger.js";
|
|
@@ -32,7 +32,7 @@ export class RemoteConfigProvider {
|
|
|
32
32
|
}
|
|
33
33
|
async fetchEffective(etag) {
|
|
34
34
|
const fn = this.deps.fetchEffective ?? remoteFetchEffective;
|
|
35
|
-
const r = await fn(this.cc
|
|
35
|
+
const r = await fn(this.cc, etag, undefined, this.cc.worker);
|
|
36
36
|
if (r === null || r.domainErrors === undefined)
|
|
37
37
|
return r;
|
|
38
38
|
return { ...r, domainErrors: r.domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })) };
|
|
@@ -40,7 +40,7 @@ export class RemoteConfigProvider {
|
|
|
40
40
|
async fetchSkillContent(contentHash) {
|
|
41
41
|
const fn = this.deps.fetchSkillContent ?? remoteFetchSkillContent;
|
|
42
42
|
try {
|
|
43
|
-
return await fn(this.cc
|
|
43
|
+
return await fn(this.cc, contentHash);
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
46
46
|
if (err instanceof ConfigCenterHttpError && err.status === 404)
|
|
@@ -77,10 +77,10 @@ export class LocalConfigProvider {
|
|
|
77
77
|
return null;
|
|
78
78
|
const refErrors = [];
|
|
79
79
|
const resolve = createSiblingResolver(store, eff);
|
|
80
|
-
for (const d of
|
|
80
|
+
for (const d of REF_CHECKED_DOMAINS) {
|
|
81
81
|
const issues = await refIntegrityIssues(d, eff[d], resolve);
|
|
82
82
|
if (issues.length > 0)
|
|
83
|
-
refErrors.push({ domain: d, error: issues.map((i) => `${i.path
|
|
83
|
+
refErrors.push({ domain: d, error: issues.map((i) => `${formatConfigPath(i.path)}: ${redactConfigError(i.message)}`).join("; ") });
|
|
84
84
|
}
|
|
85
85
|
const parseErrors = parseWarnings.map((w) => ({ domain: w.domain, error: redactConfigError(w.error) }));
|
|
86
86
|
const errors = [...domainErrors.map((de) => ({ domain: de.domain, error: redactConfigError(de.error) })), ...parseErrors, ...refErrors];
|
package/dist/config-types.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { QuestionThrottle } from "./question.js";
|
|
|
15
15
|
import type { UnattendedApprovalPolicy } from "./tool-approval.js";
|
|
16
16
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
17
17
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
18
|
+
import type { CenterCredentialSource } from "./center-credential.js";
|
|
18
19
|
import type { GitApiKind } from "./git-api-kind.js";
|
|
19
20
|
/** [ref]:记忆向量面的 embedder 坐标(env `MEMORY_EMBEDDER_*` 解析结果;单一属主 = 本类型,
|
|
20
21
|
* `plugins/memory-embedder.ts` 的工厂直接消费它,不复制形状)。语义与三问见
|
|
@@ -366,8 +367,18 @@ export interface ImageBakeConfig {
|
|
|
366
367
|
* fixtures that CONSTRUCT a config literal build this type and hand it to `attachConfigGroups`. */
|
|
367
368
|
export interface ServiceConfigFlat {
|
|
368
369
|
port: number;
|
|
369
|
-
/** OpenAI-compatible model gateway base URL (without /chat/completions)
|
|
370
|
-
|
|
370
|
+
/** OpenAI-compatible model gateway base URL (without /chat/completions) — `MODEL_GATEWAY_BASEURL`.
|
|
371
|
+
*
|
|
372
|
+
* 🔴 S-623:**缺席 = 这台部署没有网关**(没有出厂缺省;旧的本机占位值在读入那一步抹掉「来源」,让没配网关的
|
|
373
|
+
* 部署起得来、第一次调用才炸)。形取「可选键」而不是判别联合:消费方问的只有「有没有声明网关根、是哪个」,
|
|
374
|
+
* 一个只带一位载荷的联合不比 `string | undefined` 多说任何事,且与紧邻的 `gatewayApiKey?` / `gatewayMaxRetries?`
|
|
375
|
+
* 的「缺席不铸键」同一条 presence 律。一条 openai 路由请求打到哪儿,只经 `model-route-endpoint.ts` 的
|
|
376
|
+
* `openaiRouteBaseUrl` 求(路由自己的 `baseUrl` 胜、网关次之);「配置能派发到的每条 openai 路由都解析得到」
|
|
377
|
+
* 由 boot(config.ts)与热路径(apply-effective)同一只判据保证。 */
|
|
378
|
+
gatewayBaseUrl?: string;
|
|
379
|
+
/** `MODEL_API_KEY` —— **网关的**凭据,只与网关根配对([ref])。S-623:与 `gatewayBaseUrl` 同进同出 ——
|
|
380
|
+
* 没有网关就不装载(否则 brain 的声明根为空 ⇒ core 判 `unpinned` ⇒ 这把 key 会被送到每条自带 baseUrl 的
|
|
381
|
+
* openai 条目),由 `model_gateway_key_unpaired` 启动通知说出来。 */
|
|
371
382
|
gatewayApiKey?: string;
|
|
372
383
|
/** Secondary openai-compatible gateways for cross-gateway FAILOVER (same protocol + same model id).
|
|
373
384
|
* Tried in order after the primary when a gateway fails before streaming any content. */
|
|
@@ -1230,8 +1241,8 @@ export interface ServiceConfigFlat {
|
|
|
1230
1241
|
principalJwtAud?: string;
|
|
1231
1242
|
/** S-591:registry SSO-JWT 的**显式** issuer(`AUTH_BRIDGE_ISSUER`)。
|
|
1232
1243
|
* 🔴 **只是显式 env,不含 boot 期那条 `?? configCenter.baseUrl` 回退**(回退仍在
|
|
1233
|
-
* `boot/stage-09-leader.ts` 的桥装配处)——
|
|
1234
|
-
*
|
|
1244
|
+
* `boot/stage-09-leader.ts` 的桥装配处)。唯一消费方是桥;S-671([ref] ①)起它**不是**托管证据 ——
|
|
1245
|
+
* 显式与回退 issuer 在租户轴上同判(理由见 `hosted-posture.ts`)。 */
|
|
1235
1246
|
authBridgeIssuer?: string;
|
|
1236
1247
|
/** Max accepted principal-JWT lifetime (exp − now), seconds — caps the replay window past the short-lived mandate.
|
|
1237
1248
|
* 0 = no cap. `PRINCIPAL_JWT_MAX_TTL_SEC` (default 120). */
|
|
@@ -1897,10 +1908,13 @@ export interface ServiceConfigFlat {
|
|
|
1897
1908
|
* `dryRun` (SEMA_REGISTRY_DRY_RUN=true; legacy CONFIG_CENTER_DRY_RUN now boot-rejects, no dual-read): pull + LOG what the registry would change vs the env-derived
|
|
1898
1909
|
* config, but do NOT apply it — a safe-rollout verification step before flipping to live.
|
|
1899
1910
|
* `worker` (SEMA_REGISTRY_WORKER=<name>; legacy CONFIG_CENTER_WORKER now boot-rejects, no dual-read; injected per-worker by the reconciler): scopes the pull to
|
|
1900
|
-
* that worker's roster via `/effective?worker=<name>`. Unset → the global roster (backward compatible).
|
|
1911
|
+
* that worker's roster via `/effective?worker=<name>`. Unset → the global roster (backward compatible).
|
|
1912
|
+
* `credential`(S-651,BREAKING:取代 7.98.0 及以前的 `token: string` 启动快照):凭证来源活对象
|
|
1913
|
+
* (`center-credential.ts`),每条出站请求经它取一次值 —— `SEMA_REGISTRY_TOKEN`(值)或
|
|
1914
|
+
* `SEMA_REGISTRY_TOKEN_FILE`(每次读文件,轮换 / 换账号不重启)二选一。 */
|
|
1901
1915
|
configCenter?: {
|
|
1902
1916
|
baseUrl: string;
|
|
1903
|
-
|
|
1917
|
+
credential: CenterCredentialSource;
|
|
1904
1918
|
dryRun: boolean;
|
|
1905
1919
|
worker?: string;
|
|
1906
1920
|
};
|