@sema-agent/server 7.97.0 → 7.98.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 +5 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/USAGE.md +6 -0
- 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 +173 -0
- package/deploy/sema-up/sema-up.sh +113 -94
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/boot/config-center.js +2 -2
- package/dist/boot/resolve-spec.js +9 -4
- package/dist/brain.js +28 -18
- package/dist/config-catalog.js +4 -4
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -1
- package/dist/config-types.d.ts +12 -2
- package/dist/config.d.ts +9 -0
- package/dist/config.js +42 -5
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- 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/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +8 -5
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TaskSpec } from "@sema-agent/core";
|
|
2
|
+
/** core 的项目身份声明席(`TaskSpec.memory.projectKey`)—— 从 core 类型派生,不另写一份。 */
|
|
3
|
+
export type MemoryProjectKey = NonNullable<NonNullable<TaskSpec["memory"]>["projectKey"]>;
|
|
4
|
+
/** 本仓会说的缺席词。`not_git` 刻意排除:裁 D 起非 git 目录按目录键控,不是缺席(core 闭集仍收它,本仓不铸)。 */
|
|
5
|
+
export type ProjectIdentityAbsentReason = Exclude<Extract<MemoryProjectKey, {
|
|
6
|
+
absent: unknown;
|
|
7
|
+
}>["absent"], "not_git">;
|
|
8
|
+
/** 一个任务根的项目身份:canonical 项目根,或说得出理由的缺席。 */
|
|
9
|
+
export type ProjectIdentity = {
|
|
10
|
+
readonly root: string;
|
|
11
|
+
} | {
|
|
12
|
+
readonly absent: ProjectIdentityAbsentReason;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 任务根 → 项目身份([ref] 四例;见文件头)。纯文件系统读,零子进程。
|
|
16
|
+
* `taskRoot` 必须是**本进程读得到**的路径(调用方按车道判:根在沙箱/设备盘上就别调,直接给 `unreadable`)。
|
|
17
|
+
*
|
|
18
|
+
* 一次向上走,规则三条按优先序:最近的 `.git`(① ② ③)> 链上最近的裸仓布局(④ bare)> 非 git 按目录键控(④)。
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveProjectIdentity(taskRoot: string): ProjectIdentity;
|
|
21
|
+
/** 本仓**唯一**的 `repo:` 键铸点([ref] ①:`repo:` + sha256 hex 前 24;`root` 为 realpath 字符串,不加尾斜杠、不折大小写)。 */
|
|
22
|
+
export declare function mintRepoScopeKey(projectRoot: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* 任务根 → core 声明席 `TaskSpec.memory.projectKey`。
|
|
25
|
+
* `taskRoot === undefined` = 调用方**此刻不知道**任务根在本机哪里(根在沙箱/设备盘上;或 host 车道会话没登记 cwd、
|
|
26
|
+
* 手落在 factory 现铸的临时目录)⇒ `unreadable` —— 「我看不到」,不是「这里没有项目」。
|
|
27
|
+
*/
|
|
28
|
+
export declare function projectKeyForTaskRoot(taskRoot: string | undefined): MemoryProjectKey;
|
|
29
|
+
//# sourceMappingURL=project-identity.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
const UNREADABLE = { absent: "unreadable" };
|
|
5
|
+
const BARE = { absent: "bare" };
|
|
6
|
+
function isMissing(err) {
|
|
7
|
+
const code = err?.code;
|
|
8
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
9
|
+
}
|
|
10
|
+
function statFact(p) {
|
|
11
|
+
try {
|
|
12
|
+
const st = statSync(p);
|
|
13
|
+
return { ok: st.isDirectory() ? "dir" : st.isFile() ? "file" : "other" };
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return isMissing(err) ? "missing" : "unreadable";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function readTextFact(p) {
|
|
20
|
+
try {
|
|
21
|
+
return { ok: readFileSync(p, "utf8").trim() };
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return isMissing(err) ? "missing" : "unreadable";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function realpathFact(p) {
|
|
28
|
+
try {
|
|
29
|
+
return { ok: realpathSync.native(p) };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
return isMissing(err) ? "missing" : "unreadable";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isBareRepoLayout(dir) {
|
|
36
|
+
if (basename(dir) === ".git")
|
|
37
|
+
return false;
|
|
38
|
+
const head = statFact(join(dir, "HEAD"));
|
|
39
|
+
const objects = statFact(join(dir, "objects"));
|
|
40
|
+
const refs = statFact(join(dir, "refs"));
|
|
41
|
+
return typeof head === "object" && head.ok === "file" && typeof objects === "object" && objects.ok === "dir" && typeof refs === "object" && refs.ok === "dir";
|
|
42
|
+
}
|
|
43
|
+
function identityOfGitFile(dir) {
|
|
44
|
+
const pointer = readTextFact(join(dir, ".git"));
|
|
45
|
+
if (typeof pointer !== "object" || !pointer.ok.startsWith("gitdir:"))
|
|
46
|
+
return UNREADABLE;
|
|
47
|
+
const raw = pointer.ok.slice("gitdir:".length).trim();
|
|
48
|
+
if (raw === "")
|
|
49
|
+
return UNREADABLE;
|
|
50
|
+
const gitDirFact = realpathFact(resolve(dir, raw));
|
|
51
|
+
if (typeof gitDirFact !== "object")
|
|
52
|
+
return UNREADABLE;
|
|
53
|
+
const gitDir = gitDirFact.ok;
|
|
54
|
+
const commonText = readTextFact(join(gitDir, "commondir"));
|
|
55
|
+
if (commonText === "missing")
|
|
56
|
+
return { root: dir };
|
|
57
|
+
if (commonText === "unreadable")
|
|
58
|
+
return UNREADABLE;
|
|
59
|
+
const commonFact = realpathFact(resolve(gitDir, commonText.ok));
|
|
60
|
+
if (typeof commonFact !== "object")
|
|
61
|
+
return UNREADABLE;
|
|
62
|
+
const common = commonFact.ok;
|
|
63
|
+
if (dirname(gitDir) !== join(common, "worktrees"))
|
|
64
|
+
return UNREADABLE;
|
|
65
|
+
const back = readTextFact(join(gitDir, "gitdir"));
|
|
66
|
+
if (typeof back !== "object")
|
|
67
|
+
return UNREADABLE;
|
|
68
|
+
const backFact = realpathFact(resolve(gitDir, back.ok));
|
|
69
|
+
if (typeof backFact !== "object" || backFact.ok !== join(dir, ".git"))
|
|
70
|
+
return UNREADABLE;
|
|
71
|
+
return { root: basename(common) !== ".git" ? common : dirname(common) };
|
|
72
|
+
}
|
|
73
|
+
function canonicalTaskRoot(taskRoot) {
|
|
74
|
+
const rest = [];
|
|
75
|
+
for (let p = resolve(taskRoot);;) {
|
|
76
|
+
const fact = realpathFact(p);
|
|
77
|
+
if (typeof fact === "object")
|
|
78
|
+
return { canonical: rest.length > 0 ? join(fact.ok, ...rest) : fact.ok, existing: fact.ok };
|
|
79
|
+
if (fact === "unreadable")
|
|
80
|
+
return "unreadable";
|
|
81
|
+
const parent = dirname(p);
|
|
82
|
+
if (parent === p)
|
|
83
|
+
return "unreadable";
|
|
84
|
+
rest.unshift(basename(p));
|
|
85
|
+
p = parent;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function resolveProjectIdentity(taskRoot) {
|
|
89
|
+
const root = canonicalTaskRoot(taskRoot);
|
|
90
|
+
if (root === "unreadable")
|
|
91
|
+
return UNREADABLE;
|
|
92
|
+
let sawBareLayout = false;
|
|
93
|
+
for (let dir = root.existing;;) {
|
|
94
|
+
const dotGit = statFact(join(dir, ".git"));
|
|
95
|
+
if (dotGit === "unreadable")
|
|
96
|
+
return UNREADABLE;
|
|
97
|
+
if (typeof dotGit === "object") {
|
|
98
|
+
if (dotGit.ok === "dir")
|
|
99
|
+
return { root: dir };
|
|
100
|
+
if (dotGit.ok === "file")
|
|
101
|
+
return identityOfGitFile(dir);
|
|
102
|
+
}
|
|
103
|
+
if (!sawBareLayout && isBareRepoLayout(dir))
|
|
104
|
+
sawBareLayout = true;
|
|
105
|
+
const parent = dirname(dir);
|
|
106
|
+
if (parent === dir)
|
|
107
|
+
return sawBareLayout ? BARE : { root: root.canonical };
|
|
108
|
+
dir = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function mintRepoScopeKey(projectRoot) {
|
|
112
|
+
return `repo:${createHash("sha256").update(projectRoot, "utf8").digest("hex").slice(0, 24)}`;
|
|
113
|
+
}
|
|
114
|
+
export function projectKeyForTaskRoot(taskRoot) {
|
|
115
|
+
if (taskRoot === undefined)
|
|
116
|
+
return { absent: "unreadable" };
|
|
117
|
+
const id = resolveProjectIdentity(taskRoot);
|
|
118
|
+
return "root" in id ? { scope: mintRepoScopeKey(id.root) } : { absent: id.absent };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=project-identity.js.map
|
package/dist/run-local.d.ts
CHANGED
|
@@ -47,10 +47,20 @@ export declare function parseArgs(argv: string[]): RunLocalArgs;
|
|
|
47
47
|
export declare class UsageError extends Error {
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* The model GATEWAY pre-flight (mirrors the doctor message): the engine cannot run a real
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* The model GATEWAY pre-flight (mirrors the doctor message): the engine cannot run a real task through a
|
|
51
|
+
* declared gateway without the gateway/model API key resolvable from env. We surface a "you still need to
|
|
52
|
+
* set: …" list rather than letting the first brain call crash with a cryptic auth error. Returns a list of
|
|
53
|
+
* missing items (empty = OK).
|
|
54
|
+
*
|
|
55
|
+
* 🔴 S-623:这里**不再**判网关 base URL。「配置能派发到的每一条 openai 路由都解析到显式端点」是 `loadConfig()`
|
|
56
|
+
* 那一层的拒启判据(`model-route-endpoint.ts`,本入口在预检之前就调 `loadConfig()` —— 没设网关的配置根本走不到
|
|
57
|
+
* 这里),原来那条臂(a)是它的一份更弱的手抄:注释要求「本机用户必须指向自己的网关」,判据却只查 `/v1` 后缀,
|
|
58
|
+
* 旧的出厂缺省值恰好以 `/v1` 结尾 ⇒ 缺省值照样过(本仓自己也被骗)。`/v1` 后缀本身也一并删,理由三条:
|
|
59
|
+
* ① 同一个问题两个判官 —— `serve` 从不要求 `/v1`,同一份 .env 在 `serve` 起得来、在 `run-local` 被拒;
|
|
60
|
+
* ② 它会误拒真实存在的 OpenAI 兼容端点(DeepSeek 根路径直接服务 `/chat/completions`,本仓
|
|
61
|
+
* `src/bench/s1/live-deps.ts` 用的正是 `https://api.deepseek.com`;Gemini 的兼容面 `…/v1beta/openai`、Azure 的
|
|
62
|
+
* `…/openai/deployments/<name>` 也都不以 `/v1` 结尾);
|
|
63
|
+
* ③ 规则集只许变小:端点判据只有 loadConfig 那一只。
|
|
54
64
|
*
|
|
55
65
|
* NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips
|
|
56
66
|
* this check in that case — the check is about a REAL run, not the assembly.
|
package/dist/run-local.js
CHANGED
|
@@ -34,7 +34,8 @@ import { createTaskListLane } from "./boot/task-list-lane.js";
|
|
|
34
34
|
import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
|
|
35
35
|
import { HttpError } from "./security.js";
|
|
36
36
|
import { createMemoryEngineIncidentSink } from "./memory-incident-sink.js";
|
|
37
|
-
import { memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js";
|
|
37
|
+
import { memoryEngineBackendFor, memoryProjectKeyFor, memorySpecForRequest } from "./memory-scope.js";
|
|
38
|
+
import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
|
|
38
39
|
import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
|
|
39
40
|
import { buildPricing, cappedCeiling, createTracer } from "./budget.js";
|
|
40
41
|
import { createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
@@ -99,9 +100,8 @@ export class UsageError extends Error {
|
|
|
99
100
|
}
|
|
100
101
|
export function missingGatewayRequirements(config) {
|
|
101
102
|
const missing = [];
|
|
102
|
-
if (
|
|
103
|
-
missing
|
|
104
|
-
}
|
|
103
|
+
if (config.gatewayBaseUrl === undefined)
|
|
104
|
+
return missing;
|
|
105
105
|
const perModelEnvs = Object.values(config.modelApiKeyEnv ?? {});
|
|
106
106
|
const haveGatewayKey = !!config.gatewayApiKey;
|
|
107
107
|
const catalogModelNames = new Set(Object.values(config.models ?? {}).map((m) => m.name));
|
|
@@ -209,6 +209,9 @@ export async function runLocal(argv, deps = {}) {
|
|
|
209
209
|
catch (err) {
|
|
210
210
|
logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
|
|
211
211
|
}
|
|
212
|
+
const endpointRefusal = modelRouteEndpointRefusal(config);
|
|
213
|
+
if (endpointRefusal !== undefined)
|
|
214
|
+
throw new Error(endpointRefusal);
|
|
212
215
|
const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
|
|
213
216
|
if (unpricedCeiling)
|
|
214
217
|
logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
|
|
@@ -477,7 +480,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
477
480
|
backgroundScope: "session",
|
|
478
481
|
oneShot: true,
|
|
479
482
|
principal: scope,
|
|
480
|
-
...(memoryEngine ? { memory: memorySpecForRequest(scope, undefined) } : {}),
|
|
483
|
+
...(memoryEngine ? { memory: memorySpecForRequest(scope, memoryProjectKeyFor(config, scope, workspaceDir), undefined) } : {}),
|
|
481
484
|
model: mention.model ?? "default",
|
|
482
485
|
getApiKeyAndHeaders: keyResolver,
|
|
483
486
|
tools: cap.tools,
|
package/dist/trace/redact.d.ts
CHANGED
|
@@ -190,7 +190,7 @@ export declare function isHierarchicalUrl(raw: string): boolean;
|
|
|
190
190
|
* ① **有效性闸**:配置里的那一位应当是一个**绝对 URL**。`new URL(raw)` 解析不动 ⇒ 这一位根本不是 URL
|
|
191
191
|
* (裸 `host:port`、相对路径、手滑的散文),自由文本的 URL 律对它没有定界依据 ⇒ 返回**占位**
|
|
192
192
|
* {@link DISPLAY_SAFE_URL_TOKEN},**不回落原值**([ref]:安全轴上的兜底必须 fail-closed 或响亮)。
|
|
193
|
-
* 成文代价:`MODEL_GATEWAY_BASEURL=
|
|
193
|
+
* 成文代价:`MODEL_GATEWAY_BASEURL=gw.internal:8000` 这种缺 scheme 的配置在日志上变成 `«redacted:url»`
|
|
194
194
|
* —— 而它在 `fetch` 那一侧本来就是必然失败的形(WHATWG 要求绝对 URL),日志上响亮一点方向一致。
|
|
195
195
|
* ② **空串 = 未设**:里面没有凭据,原样回空串(不铸占位 —— 占位会让「没配」读成「配了但看不见」)。
|
|
196
196
|
* ⚠️ **保形只到 host / port / path 这一层**(S-483 起如实改口;`test/config-url-display-safe.test.ts`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.98.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",
|