@sema-agent/server 7.31.0 → 7.32.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/boot/lexical-path-env.d.ts +10 -0
- package/dist/boot/lexical-path-env.js +88 -0
- package/dist/capabilities/oa-tools.d.ts +15 -0
- package/dist/capabilities/oa-tools.js +54 -0
- package/dist/capabilities/tool-defer.d.ts +10 -0
- package/dist/finance/cost-taxonomy.d.ts +34 -0
- package/dist/finance/cost-taxonomy.js +26 -0
- package/dist/http/routes/capabilities.js +11 -0
- package/dist/http/routes/memory-bundle.d.ts +109 -0
- package/dist/http/routes/memory-bundle.js +178 -0
- package/dist/http/server.d.ts +29 -3
- package/dist/http/server.js +39 -4
- package/dist/main.js +16 -0
- package/dist/memory-bundle-engine.d.ts +64 -0
- package/dist/memory-bundle-engine.js +67 -0
- package/dist/plugins/approval-store-sql.d.ts +116 -0
- package/dist/plugins/approval-store-sql.js +151 -0
- package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
- package/dist/plugins/file-workflow-journal-store.js +12 -0
- package/dist/plugins/pg-approval-store.d.ts +9 -0
- package/dist/plugins/pg-approval-store.js +9 -0
- package/dist/plugins/pg-breaker-state.d.ts +8 -0
- package/dist/plugins/pg-breaker-state.js +8 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
- package/dist/plugins/pg-checkpoint-store.js +10 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/pg-file-snapshot-store.js +8 -0
- package/dist/plugins/pg-image-bake.d.ts +12 -0
- package/dist/plugins/pg-image-bake.js +11 -0
- package/dist/plugins/pg-image-index.d.ts +12 -0
- package/dist/plugins/pg-image-index.js +11 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
- package/dist/plugins/pg-outcome-ledger.js +11 -0
- package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/pg-resume-anchor-store.js +7 -0
- package/dist/plugins/pg-run-store.d.ts +9 -0
- package/dist/plugins/pg-run-store.js +9 -0
- package/dist/plugins/pg-session-policy-store.d.ts +7 -0
- package/dist/plugins/pg-session-policy-store.js +7 -0
- package/dist/plugins/pg-session-store.d.ts +12 -0
- package/dist/plugins/pg-session-store.js +12 -0
- package/dist/plugins/pg-tool-result-store.d.ts +9 -0
- package/dist/plugins/pg-tool-result-store.js +9 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-journal-store.js +9 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-run-store.js +9 -0
- package/dist/plugins/tidb-approval-store.d.ts +8 -0
- package/dist/plugins/tidb-approval-store.js +8 -0
- package/dist/plugins/tidb-breaker-state.d.ts +7 -0
- package/dist/plugins/tidb-breaker-state.js +7 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
- package/dist/plugins/tidb-checkpoint-store.js +9 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/tidb-file-snapshot-store.js +8 -0
- package/dist/plugins/tidb-image-bake.d.ts +12 -0
- package/dist/plugins/tidb-image-bake.js +11 -0
- package/dist/plugins/tidb-image-index.d.ts +12 -0
- package/dist/plugins/tidb-image-index.js +11 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
- package/dist/plugins/tidb-outcome-ledger.js +12 -0
- package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/tidb-resume-anchor-store.js +7 -0
- package/dist/plugins/tidb-run-store.d.ts +10 -0
- package/dist/plugins/tidb-run-store.js +9 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
- package/dist/plugins/tidb-session-policy-store.js +7 -0
- package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
- package/dist/plugins/tidb-tool-result-store.js +10 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/tidb-workflow-journal-store.js +9 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
- package/dist/plugins/tidb-workflow-run-store.js +10 -0
- package/dist/plugins/workflow-journal-limits.d.ts +12 -0
- package/dist/plugins/workflow-journal-limits.js +12 -0
- package/dist/sema-registry.d.ts +41 -0
- package/dist/sema-registry.js +40 -0
- package/dist/spec-fields.d.ts +7 -0
- package/dist/spec-fields.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FileError, StubExecutionEnv, type Result } from "@sema-agent/core";
|
|
2
|
+
/**
|
|
3
|
+
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
4
|
+
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
5
|
+
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
6
|
+
*/
|
|
7
|
+
export declare class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
8
|
+
absolutePath(path: string): Promise<Result<string, FileError>>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=lexical-path-env.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #156 —— 非 host lane(e2b/k8s/ssh/adb/local-docker)写门的**纯路径规则**过渡形所用的裁决 env。
|
|
3
|
+
*
|
|
4
|
+
* ## 它解决的缺口
|
|
5
|
+
*
|
|
6
|
+
* core 的 `createFsWriteGatePolicy` / `createSensitivePathPolicy` 都把写目标交给
|
|
7
|
+
* `canonicalizeTarget(env, …)` 去「问 fs 要真身」。host lane 上那个 env 就是 hand 工具真正写的那块盘,
|
|
8
|
+
* 答案可信。沙箱 lane 不是:per-task 的沙箱 env 由 core 的 `executionEnvFactory` 在 spec **之后**才铸,
|
|
9
|
+
* spec 期手边只有 worker 本机的 fs——拿它去裁沙箱里的路径会答错(exists/symlink 全是别人机器上的事实),
|
|
10
|
+
* 而**错误的 allow 比没有门更糟**。所以 [816]/[820] 当年在沙箱 lane 诚实不挂门(wiring=undefined ⇒
|
|
11
|
+
* 三个模式臂全回落 base 规则,写门整条缺席)。#153 的 ③ 号注把这笔记成了余款,#156 是它的过渡还款。
|
|
12
|
+
*
|
|
13
|
+
* ## 这个 env 是什么
|
|
14
|
+
*
|
|
15
|
+
* 一个**没有文件系统**的 env:除 `absolutePath` 外的每一个 fs 面(exists / canonicalPath / fileInfo /
|
|
16
|
+
* read* / write* / listDir / createDir / …)与 shell 面都继承 core 的 `StubExecutionEnv`,
|
|
17
|
+
* 恒返回 `not_supported` 错误(`readLink` 是可选面,Stub 干脆没有——core 把缺席当「symlink 不可解」,
|
|
18
|
+
* 且该分支在本 env 恒不可达:exists 先错就进了 canonicalizeNewPath)——这不是伪装,这就是本 env 的诚实回答:**spec 期我们确实不知道沙箱里
|
|
19
|
+
* 那块盘上有什么**。`absolutePath` 是唯一被赋予真实语义的一面,而它本来就是纯路径运算(不碰盘):
|
|
20
|
+
* POSIX 绝对路径做词法归一后原样奉还,其余一律「不知道」。
|
|
21
|
+
*
|
|
22
|
+
* ## 由此得到的裁决语义(core dist 亲读推出,`tools/fs/safety.js` canonicalizeTarget)
|
|
23
|
+
*
|
|
24
|
+
* · 绝对路径(`/…`):`absolutePath` ok ⇒ core 进 exists 探测 ⇒ 我们返回错误 ⇒ core 按「该路径尚不存在」
|
|
25
|
+
* 走 `canonicalizeNewPath`,那里逐级 exists 也全错 ⇒ 原样返回我们给的词法绝对路径。于是 canonical key
|
|
26
|
+
* = **词法归一后的路径**,`canonicalPath` / `fileInfo` / `readLink` 三面在这条路上根本不可达。
|
|
27
|
+
* · 相对路径 / `~/…` / Windows 盘符形:`absolutePath` 返回错误 ⇒ `canon.ok=false` ⇒ 写门直接
|
|
28
|
+
* `ask`(fail-closed)。沙箱的 cwd 在 spec 期不可知,这正是我们要的偏置——**不猜沙箱工作目录**。
|
|
29
|
+
* · 反斜杠 UNC 形(`\\host\share\…`)是唯一**不经过**本 env 的形:core 的 `canonicalizeTarget` 在入口
|
|
30
|
+
* 就把它短路成 `ok:true, key=原样`(复审 2026-08-05 dist 亲读+实测)。结果不变——写门无放行域可越,
|
|
31
|
+
* 落 defaultWrite 的 `ask`;sensitive 段匹配按 `[\\/]` 双分隔符切段,`\\host\share\.env` 照样 deny。
|
|
32
|
+
* · sensitivePatterns 的 deny 腿照常施加:它按 canonical key 的**路径段**做 glob 匹配,词法 key 足够,
|
|
33
|
+
* 且在 `combinePolicies` 折叠里 deny 恒胜(session 豁免 / accept 域越不过)。
|
|
34
|
+
* · `isExempt`(会话「本会话不再询问」探针)是 name-keyed 的,不做 fs 裁决,任何 lane 都安全,照接。
|
|
35
|
+
*
|
|
36
|
+
* ## 已知残余面(过渡形的边界,不是疏漏)
|
|
37
|
+
*
|
|
38
|
+
* · **symlink 形**:词法裁决看不见符号链接。沙箱里一个名字普通的软链可以指向守卫段(`.ssh` 等),
|
|
39
|
+
* 我们只会给 `ask` 而不是 `deny`;host lane 上 core 会 canonicalize 出真身并 deny。相应地,core 的
|
|
40
|
+
* `unresolvedSymlink ⇒ deny` 那条腿在本 env 下永不触发。注意豁免会话下的口径(codex 复审 E):
|
|
41
|
+
* name-keyed 豁免不看路径,这类词法无害的软链目标在豁免会话里是 **allow** 而非 ask——这是本残余面
|
|
42
|
+
* 在「操作员已授 don't-ask-again」情形下的完整代价,host lane 同情形仍会 deny。
|
|
43
|
+
* · **exempt/accept 域**:沙箱 lane 既无可信 cwd 也无 scratchpad 对应物,故本过渡形**一个自动放行域都
|
|
44
|
+
* 不铸**(见 resolve-spec 的 wiring)。代价是 acceptEdits 在沙箱 lane 退化成与 default 同形(全 ask);
|
|
45
|
+
* 这是 fail-safe 方向,与 host lane「accept 域解析不出 ⇒ 该域不生效」的哨兵先例同口径。
|
|
46
|
+
* · **`cwd` 字段**:继承 `StubExecutionEnv` 的 `"/"` 占位。本 env 只活在 policy 折叠里,core 的两个
|
|
47
|
+
* policy 都不读 `env.cwd`(相对路径基准走 `rootPath` 形参,而我们不给)——它不代表沙箱的工作目录。
|
|
48
|
+
*
|
|
49
|
+
* ## 终局 seam
|
|
50
|
+
*
|
|
51
|
+
* 正解是让写门在**工具执行时刻**拿到真沙箱 env:core [2751] 已排期给 `HookToolContext` 加一个只读的
|
|
52
|
+
* env 能力窄面(174/#22 窗)。那一面到货后,本过渡形整体退役换正解——届时沙箱 lane 与 host lane 走同
|
|
53
|
+
* 一条真 fs 裁决,上面三条残余面一并消失。
|
|
54
|
+
*/
|
|
55
|
+
import { posix } from "node:path";
|
|
56
|
+
import { FileError, StubExecutionEnv, err, ok } from "@sema-agent/core";
|
|
57
|
+
/** 词法归一:纯字符串运算,折 `.` / `..` / 重复分隔符,不碰 fs。
|
|
58
|
+
* `posix.normalize` 把 `..` 在根部截断(`/../x` → `/x`),与「沙箱根之上没有东西」的语义一致。
|
|
59
|
+
* 尾部分隔符统一剥掉(根 `/` 除外),让同一目标只有一个 key ——前缀判域靠的就是 key 的唯一性。
|
|
60
|
+
* (core 内部有同形的 `normalizeAbsPathLexically`,但未从包根导出;此处是 Node 标准库的等价运算,
|
|
61
|
+
* 不是它的抄本——若日后 core 导出,这里应改为直接复用。) */
|
|
62
|
+
function normalizeAbsolutePathLexically(path) {
|
|
63
|
+
const collapsed = posix.normalize(path.replace(/^\/+/, "/"));
|
|
64
|
+
return collapsed.length > 1 ? collapsed.replace(/\/+$/, "") : collapsed;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 无文件系统的路径裁决 env(见文件头)。只覆写 `absolutePath` 一面,其余全部继承 `StubExecutionEnv`
|
|
68
|
+
* 的 `not_supported` ——**继承而非逐一手写**是刻意的:core 日后给 `ExecutionEnv` 加必填面时,新面会
|
|
69
|
+
* 随 `StubExecutionEnv` 一起到位并保持同一个诚实答案,不会在这里留下一个悄悄编出来的假答案。
|
|
70
|
+
*/
|
|
71
|
+
export class LexicalPathExecutionEnv extends StubExecutionEnv {
|
|
72
|
+
absolutePath(path) {
|
|
73
|
+
// POSIX 绝对形是沙箱 lane(全 Linux)上 hand 工具的书面契约形。其余一切——相对路径、`~`、
|
|
74
|
+
// `C:\…`——在 spec 期都无法诚实解析成一个沙箱内的绝对路径,返回错误让 core 落 ask。
|
|
75
|
+
// (反斜杠 UNC 形根本到不了这里:core 在 canonicalizeTarget 入口短路,见文件头「由此得到的裁决语义」。)
|
|
76
|
+
// codex 复审 A(2026-08-05):含 NUL 的字符串不是合法 POSIX 路径(任何 fs 面都写不进去),但词法归一
|
|
77
|
+
// 会照样给它铸出 canonical key——`\0` 尾巴让守卫段 glob 失配,豁免会话下还能拿 allow。拒收让 canon
|
|
78
|
+
// 在豁免咨询**之前**就失败 ⇒ 恒 ask。
|
|
79
|
+
if (path.includes("\u0000")) {
|
|
80
|
+
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and rejects a path containing a NUL byte (not a representable POSIX path)", path)));
|
|
81
|
+
}
|
|
82
|
+
if (!path.startsWith("/")) {
|
|
83
|
+
return Promise.resolve(err(new FileError("not_supported", "this environment adjudicates paths lexically and cannot resolve a non-absolute path (the sandbox working directory is unknown at spec time)", path)));
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve(ok(normalizeAbsolutePathLexically(path)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=lexical-path-env.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolSpec } from "@sema-agent/core";
|
|
2
|
+
/**
|
|
3
|
+
* OA scenario — tools are thin HTTP clients to the OA backend's existing APIs (read = /api/v1/*,
|
|
4
|
+
* write = /api/assets/*). The shared service token + the request's principal are forwarded so OA
|
|
5
|
+
* enforces THAT end-user's RBAC + audit (the service never holds per-user creds). Decoupled: the
|
|
6
|
+
* service knows no OA schema/DB — OA stays the source of truth (its /api/v1/meta is self-describing).
|
|
7
|
+
*/
|
|
8
|
+
export interface OaCtx {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
token: string;
|
|
11
|
+
principal?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function oaTools(ctx: OaCtx): ToolSpec[];
|
|
14
|
+
export declare const oaPrompt: import("@sema-agent/core").PromptProvider;
|
|
15
|
+
//# sourceMappingURL=oa-tools.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { stablePrompt } from "./prompt.js";
|
|
3
|
+
async function oaFetch(ctx, path, init) {
|
|
4
|
+
if (!ctx.baseUrl || !ctx.token)
|
|
5
|
+
return "OA 后端未配置(OA_API_BASEURL / OA_SERVICE_TOKEN)";
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(ctx.baseUrl + path, {
|
|
8
|
+
method: init?.method ?? "GET",
|
|
9
|
+
headers: {
|
|
10
|
+
"content-type": "application/json",
|
|
11
|
+
authorization: `Bearer ${ctx.token}`,
|
|
12
|
+
...(ctx.principal ? { "x-agent-principal": ctx.principal } : {}),
|
|
13
|
+
},
|
|
14
|
+
body: init?.body !== undefined ? JSON.stringify(init.body) : undefined,
|
|
15
|
+
});
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
if (!res.ok)
|
|
18
|
+
return `OA ${res.status}: ${text.slice(0, 600)}`;
|
|
19
|
+
return text.slice(0, 60_000);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return `调用 OA 失败:${e.message}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const needWrite = (ctx) => (!ctx.principal ? "缺少身份(principal),无法代表用户写库;请让调用方带上 x-agent-principal。" : null);
|
|
26
|
+
export function oaTools(ctx) {
|
|
27
|
+
const obj = Type.Object({}, { additionalProperties: true });
|
|
28
|
+
return [
|
|
29
|
+
{ name: "oa_schema", effect: "read", description: "取 OA 数据结构(每类资产的字段/必填/条件必填 requiredWhen/联动 showWhen/枚举、生命周期、表名)。建/改前先调它,别凭记忆。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/meta") },
|
|
30
|
+
{ name: "oa_alerts", effect: "read", description: "派生告警:GP 闲置(已注册未变现) / APP 未上架(已接广告未 LIVE)。", parameters: Type.Object({}), execute: () => oaFetch(ctx, "/api/v1/alerts") },
|
|
31
|
+
{ name: "oa_list", effect: "read", description: "列出某类资产。type∈gp/package/ad-platform/ad-unit/email/environment/proxy。可选 q 关键字。", parameters: Type.Object({ type: Type.String(), q: Type.Optional(Type.String()) }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}${x.q ? `?q=${encodeURIComponent(x.q)}` : ""}`); } },
|
|
32
|
+
{ name: "oa_get", effect: "read", description: "取某资产详情(敏感字段为掩码)。", parameters: Type.Object({ type: Type.String(), id: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/entities/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`); } },
|
|
33
|
+
{ name: "oa_lookup", effect: "read", description: "约定字段值反查(同手机/证件=同一实物)。field: phone|idCardNo|bizLicenseNo。", parameters: Type.Object({ field: Type.String(), value: Type.String() }), execute: (a) => { const x = a; return oaFetch(ctx, `/api/v1/lookup?field=${encodeURIComponent(x.field)}&value=${encodeURIComponent(x.value)}`); } },
|
|
34
|
+
{ name: "oa_create", effect: "write", description: "创建实体(走 OA 校验+当前用户 RBAC+审计)。type + data(字段对象,参照 oa_schema)。失败会返回缺失/非法字段。", parameters: Type.Object({ type: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
35
|
+
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}`, { method: "POST", body: x.data ?? {} }); } },
|
|
36
|
+
{ name: "oa_update", effect: "write", description: "改字段(敏感留空=保持原值;状态字段改不动,用 oa_transition)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), data: obj }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
37
|
+
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}`, { method: "PATCH", body: x.data ?? {} }); } },
|
|
38
|
+
{ name: "oa_transition", effect: "write", description: "受控状态流转(先 oa_schema 看可用迁移;风险/弃用等需 note)。", parameters: Type.Object({ type: Type.String(), id: Type.String(), to: Type.String(), note: Type.Optional(Type.String()) }), execute: (a) => { const e = needWrite(ctx); if (e)
|
|
39
|
+
return e; const x = a; return oaFetch(ctx, `/api/assets/${encodeURIComponent(x.type)}/${encodeURIComponent(x.id)}/transition`, { method: "POST", body: { to: x.to, note: x.note ?? null } }); } },
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
// OA business prompt MOVED OUT — migration complete (design/29, 2026-06-05). Persona / 工作准则 / UI
|
|
43
|
+
// markers (::FORM:: / ::GOTO:: / ::SUGGEST:: / ::ISSUE_FORM::) / 分级确认 / 报bug流程 are OA's business,
|
|
44
|
+
// and OA now injects the WHOLE thing per request via `request.systemPrompt` (it holds the service token,
|
|
45
|
+
// content is stable per OA version → cacheable prefix; page context still rides `objective`). Keeping
|
|
46
|
+
// any of it here would just DUPLICATE OA's prompt, so the base is EMPTY — single source of truth = OA.
|
|
47
|
+
// The service stays business-agnostic: it provides the oa_* tools (referenced by NAME from OA's prompt;
|
|
48
|
+
// they carry their own descriptions) and nothing else. `stablePrompt` keeps STABLE→VARIABLE order —
|
|
49
|
+
// OA's systemPrompt is appended after this (empty) base, before core's <user_memory>.
|
|
50
|
+
// (::ISSUE_FORM:: is parsed/rendered + submitted entirely by OA's frontend via OA /api/ai/issue — the
|
|
51
|
+
// service never touches it; `open_gitea_issue` remains only as the unattended/automated fallback.)
|
|
52
|
+
const OA_SYSTEM = "";
|
|
53
|
+
export const oaPrompt = stablePrompt(OA_SYSTEM);
|
|
54
|
+
//# sourceMappingURL=oa-tools.js.map
|
|
@@ -5,10 +5,20 @@
|
|
|
5
5
|
* - candidates for deferral are ONLY `spec.tools` (scenario/user tools) + MCP tools — core's first-party
|
|
6
6
|
* roster (Bash/Read/Edit/Grep/Glob/Write/…) is structurally NEVER deferred, so the CC "12 core tools
|
|
7
7
|
* inline" posture holds without any server action;
|
|
8
|
+
* ⚠️ **这句在中间那些世代上一度不成立**(#277 件⑤ 对表):`deferMode:"auto"` 的压力阀曾把内建 fs 族
|
|
9
|
+
* 也卷进候选,小窗模型上首用即激活错;core 5.40.0(#308 a 案,判据帖 [4315] 翻面清单 #3)把阀候选
|
|
10
|
+
* 收回 caller specs,**内建恒 inline**,于是这句话重新为真。**对本仓零行为影响**——理由是下一条:
|
|
11
|
+
* 我们从不开 auto。这一段留着是因为它是那次漂移的出处(数字对了而理由没对,是最难查的一类)。
|
|
8
12
|
* - MCP tool names are ALWAYS deferred (unconditional in `classifyDeferred`) — no server knob needed;
|
|
9
13
|
* - `RunnerDeps.deferMode:"auto"` is ALL-OR-NOTHING over the candidates (defers every spec tool once their
|
|
10
14
|
* combined inline chars exceed 10% of the window) — on our default scenario that would defer Agent and the
|
|
11
15
|
* Task quartet too, the OPPOSITE of the CC posture, so we deliberately do NOT enable it;
|
|
16
|
+
* 🔴 该判断在 core 5.40.0 上**逐字仍成立**(#277 件⑤ 亲验):5.40 收窄的是「内建卷不卷进来」,而
|
|
17
|
+
* Agent / Task 四件是 **caller spec 工具**,收窄后照样在候选池里 —— 所以「开了 auto 就会 defer 掉
|
|
18
|
+
* CC 姿势工具」这条不开 auto 的理由没有被这次收窄消解。
|
|
19
|
+
* 我方**从不接线**这个座:全树零 `RunnerDeps.deferMode` 赋值(`deferModelPlane` 是 config-center 的
|
|
20
|
+
* 同形近名,与本轴无关)。机器钉见 test/tool-defer.test.ts 的 #277 件⑤ 格 —— 这条「零接线」正是本仓
|
|
21
|
+
* 在两个 core 世代之间行为逐字不变的**全部**依据,所以它值一根钉而不只是一句注。
|
|
12
22
|
* - per-tool `ToolSpec.defer:true` is the precise instrument — that is what this helper applies.
|
|
13
23
|
*
|
|
14
24
|
* Measured inline surface (chars = description + JSON schema; ÷4 ≈ tokens), default scenario @ core 1.289:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface InfraCostRates {
|
|
2
|
+
toolCallMicroUsd: number;
|
|
3
|
+
sandboxSecMicroUsd: number;
|
|
4
|
+
egressGbMicroUsd: number;
|
|
5
|
+
}
|
|
6
|
+
export interface InfraUsage {
|
|
7
|
+
toolCalls: number;
|
|
8
|
+
sandboxWalltimeMs: number;
|
|
9
|
+
egressBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export interface InfraCostBreakdown {
|
|
12
|
+
toolCallMicroUsd: number;
|
|
13
|
+
sandboxWalltimeMicroUsd: number;
|
|
14
|
+
egressMicroUsd: number;
|
|
15
|
+
totalMicroUsd: number;
|
|
16
|
+
}
|
|
17
|
+
export interface LlmCostBreakdown {
|
|
18
|
+
llmRootMicroUsd: number;
|
|
19
|
+
nestedSubagentMicroUsd: number;
|
|
20
|
+
memoryConsolidationMicroUsd: number;
|
|
21
|
+
compactionMicroUsd: number;
|
|
22
|
+
}
|
|
23
|
+
export interface SupervisorCostBreakdown {
|
|
24
|
+
llm: LlmCostBreakdown | null;
|
|
25
|
+
infra: InfraCostBreakdown;
|
|
26
|
+
totalMicroUsd: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function infraCost(usage: InfraUsage, rates: InfraCostRates): InfraCostBreakdown;
|
|
29
|
+
export declare function composeSupervisorCost(llm: LlmCostBreakdown | null, llmTotalMicroUsd: number, infra: InfraCostBreakdown): SupervisorCostBreakdown;
|
|
30
|
+
export declare function hasInfraPricing(r: InfraCostRates): boolean;
|
|
31
|
+
export declare function infraUsageFromEvents(events: ReadonlyArray<{
|
|
32
|
+
type?: string;
|
|
33
|
+
}>, runDurationMs: number): InfraUsage;
|
|
34
|
+
//# sourceMappingURL=cost-taxonomy.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const nonNeg = (n) => (Number.isFinite(n) && n > 0 ? n : 0);
|
|
2
|
+
export function infraCost(usage, rates) {
|
|
3
|
+
const toolCallMicroUsd = nonNeg(usage.toolCalls) * nonNeg(rates.toolCallMicroUsd);
|
|
4
|
+
const sandboxWalltimeMicroUsd = (nonNeg(usage.sandboxWalltimeMs) / 1000) * nonNeg(rates.sandboxSecMicroUsd);
|
|
5
|
+
const egressMicroUsd = (nonNeg(usage.egressBytes) / 1_000_000_000) * nonNeg(rates.egressGbMicroUsd);
|
|
6
|
+
return {
|
|
7
|
+
toolCallMicroUsd,
|
|
8
|
+
sandboxWalltimeMicroUsd,
|
|
9
|
+
egressMicroUsd,
|
|
10
|
+
totalMicroUsd: toolCallMicroUsd + sandboxWalltimeMicroUsd + egressMicroUsd,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function composeSupervisorCost(llm, llmTotalMicroUsd, infra) {
|
|
14
|
+
return { llm, infra, totalMicroUsd: nonNeg(llmTotalMicroUsd) + infra.totalMicroUsd };
|
|
15
|
+
}
|
|
16
|
+
export function hasInfraPricing(r) {
|
|
17
|
+
return r.toolCallMicroUsd > 0 || r.sandboxSecMicroUsd > 0 || r.egressGbMicroUsd > 0;
|
|
18
|
+
}
|
|
19
|
+
export function infraUsageFromEvents(events, runDurationMs) {
|
|
20
|
+
return {
|
|
21
|
+
toolCalls: events.reduce((n, e) => (e.type === "tool_start" ? n + 1 : n), 0),
|
|
22
|
+
sandboxWalltimeMs: nonNeg(runDurationMs),
|
|
23
|
+
egressBytes: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=cost-taxonomy.js.map
|
|
@@ -149,6 +149,17 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
149
149
|
// 空名单在这条轴上**刻意**不走 `isOperator` 的「人人都是 operator」旧义(那是一道世界可写的门,
|
|
150
150
|
// 见 principal-gate.ts 的 `explicitOperatorOk` 头注),所以补的是能力位、不是放宽授权。
|
|
151
151
|
adoption: Boolean(deps.backend?.adoptionLog) && deps.config.operatorPrincipals.length > 0,
|
|
152
|
+
// #264 v2-c:治理携出 bundle 两口(`POST /v1/memory/{export,import}`,operator lane)。谓词**逐项**
|
|
153
|
+
// 对着 routes/memory-bundle.ts 的拒绝臂写,与 adoption 同族两项合取:
|
|
154
|
+
// ①引擎两面都接线(`memoryBundle{Export,Import}`;记忆引擎没接线的部署 ⇒ 501);
|
|
155
|
+
// ②operator 名单**非空** —— 否则 `explicitOperatorOk` 对任何身份恒假,两口**恒 403**
|
|
156
|
+
// ([3397]-4 逮到的那条真缺口,adoption 同病,这里从第一天就带上)。
|
|
157
|
+
// ⚠️ 位为真**只**保证「门能过 + 引擎在场」,不保证这台部署的记忆后端真有 bundle 复合面:本仓两只
|
|
158
|
+
// SQL 记忆孪生没实现 `exportSnapshotOf`/`importBundleCommit`,那时 core 自己响亮拒
|
|
159
|
+
// (`memory.export_incomplete`)。刻意不把「后端有没有复合面」编进本位:那是一次**运行期**能力探测
|
|
160
|
+
// (要真打一次店面才知道),把它做成部署级布尔就会变成一个会撒谎的位 —— 诚实的边界写在这里,
|
|
161
|
+
// 而不是让位去猜。消费方按码分支(附录 A 已收录该码),不是 trial-by-501。
|
|
162
|
+
memoryBundle: Boolean(deps.memoryBundleExport && deps.memoryBundleImport) && deps.config.operatorPrincipals.length > 0,
|
|
152
163
|
// [3397]-5:`GET /v1/outcomes`(design/73 §7.2 机械信号只读聚合)。此前**有 501 拒而无能力位** ——
|
|
153
164
|
// 消费端只能 trial-by-501,与本仓「gate-don't-trial-by-501」的姿势相反。谓词逐项对着该路由的
|
|
154
165
|
// 拒绝臂:①可查询的账面在场(`outcomeSink.summary`;File sink 只写 JSONL,没有查询面 ⇒ 501);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #264 v2-c —— **治理携出 bundle 的 operator 面**(core 5.40.0 `MemoryEngine.exportMemoryScopes` /
|
|
3
|
+
* `importMemoryBundle`,design/178 v2-c;判据帖 [4315] G1/G2/G3)。
|
|
4
|
+
*
|
|
5
|
+
* · `POST /v1/memory/export` —— 请求 `{scopes: string[]}`,回**治理完备包**(bundle 原样);
|
|
6
|
+
* · `POST /v1/memory/import` —— 请求 `{bundle}`,回 `MemoryImportReport` 的白名单投影。
|
|
7
|
+
*
|
|
8
|
+
* ── 为什么是 operator-only(而不是属主自助)────────────────────────────────────────────────────
|
|
9
|
+
* 同路径上的 `GET /v1/memory/export?scope=`(routes/memory-policy.ts,142-S5)是**属主自助的条目读**:
|
|
10
|
+
* 一个 principal 导出自己那张盘。本面不是它的 POST 版本,是**另一件东西**:
|
|
11
|
+
* · import 是**治理写面** —— 它往本部署的记忆平面里落别处铸的条目、挑战、污染标记与托管链。写错=
|
|
12
|
+
* 把一份外来治理史当成本店的账。fail-closed 是唯一可接受的缺省,门=`explicitOperatorOk`(与
|
|
13
|
+
* approvals RBAC / adoption / retention-ops 同一条门,不是第二套判据);
|
|
14
|
+
* · export 出的包**跨 scope 携带治理行**(custody 链、跨会话污染标记、lineage),按定义超出任何单个
|
|
15
|
+
* principal 的自助边界 —— 与 import 同级授权,不给「读半场松一格」的斜坡。
|
|
16
|
+
* 缺 principal 的形照 sibling mutating 端点(retention-ops / adoption)401 `auth.principal_required`。
|
|
17
|
+
*
|
|
18
|
+
* ── 门序(逐字同 routes/adoption.ts / retention-ops.ts)────────────────────────────────────────
|
|
19
|
+
* 身份(401)→ 授权(403)→ 能力(501)→ 验型(400)。**授权在能力之前**:一个够不着任何东西的调用方
|
|
20
|
+
* 不该从「这个部署有没有记忆引擎」上读出部署形态。
|
|
21
|
+
*
|
|
22
|
+
* ── 能力面的诚实形(与 GET 面**方向相反**,这不是笔误)──────────────────────────────────────
|
|
23
|
+
* `GET /v1/memory/export` 要的是 **DB 记忆平面**(pg|tidb),file 形态 501。本面要的是 core 的
|
|
24
|
+
* **bundle 复合面**(`exportSnapshotOf` / `importBundleCommit`),而它今天只在 core 自带的 **File**
|
|
25
|
+
* 后端上实装:本仓的两只 SQL 记忆孪生(plugins/memory-engine-{pg,tidb}.ts)没有实现这两个可选面。
|
|
26
|
+
* 因此**两个成因同落 501** `capability.memory_engine_required`(都是「换部署形态才行」,消费端分支相同):
|
|
27
|
+
* · 记忆引擎整个没接线(`MEMORY_ENGINE=off` / 多租 file 底座等暗态);
|
|
28
|
+
* · 引擎接了但后端没有 bundle 复合面(pg/tidb 孪生)—— 这一支在**挂载期**就判掉了
|
|
29
|
+
* (`createMemoryBundleFaces` 返回 undefined ⇒ 两口整个不挂),能力位 `memoryBundle` 因此与「路由真能用」
|
|
30
|
+
* 严格同真。🔴 首版把它当运行期事实(照常挂载、让 core 每次抛 409/422),被 codex 对抗复审驳倒并改此形:
|
|
31
|
+
* 面在不在是一次**属性检查**、boot 期判得出、且是**永久**缺席 ⇒ 照常挂着就是一个恒假的 yes,
|
|
32
|
+
* 壳会据此给用户开出一条走不通的迁移流程。
|
|
33
|
+
* ⚠️ 与**运行期**拒别混:后端**有**复合面但托管链 chain-degraded 时,core 照样抛
|
|
34
|
+
* `memory.export_incomplete`(409)—— 那是诚实的运行期拒,附录 A 单列。
|
|
35
|
+
* ⚠️ 绝不在本层为 SQL 孪生现造一个「够用的」导出:那正是 core 明写要拒绝的
|
|
36
|
+
* 「a governance-less export is the laundering shape this API refuses by design」。
|
|
37
|
+
* SQL 孪生补齐两面是上游/本仓 store 层的独立工单(消费台账 debt 行)。
|
|
38
|
+
*
|
|
39
|
+
* ── 验型的分权:server 验**形**,core 验**义** ─────────────────────────────────────────────────
|
|
40
|
+
* 本层只用 zod 校 JSON **结构**(宪法 [2704]:边界必 schema、禁裸 as-cast)。scopes 的「非空/去重/
|
|
41
|
+
* 元素非空」与 bundle 的整包判决(section hash 键集、逐条目 rev 重算、批内唯一性、scope 授权信封)
|
|
42
|
+
* **全部**归 core —— 在本层抄一遍等于第二真源:core 收紧一格我方不会跟着动,而两边都"绿"。
|
|
43
|
+
* 于是 `{scopes:["a","a"]}` 这种**形对义错**的请求会走到引擎并带回 `config.memory_export_request`。
|
|
44
|
+
*
|
|
45
|
+
* ── 披露的传导([4352] 权威句;打分帖 [4351] 扣点① 的 server 半场)────────────────────────────
|
|
46
|
+
* bundle 体里的 `doc: string[]` 是**铸包方的治理披露**(自由串字面的租户隔离界限等),core 铸包时恒
|
|
47
|
+
* 写入。本面把它**原样透传**:不剥离、不复述、不另铸 `disclosure` 键。理由是单一属主 —— 复述会随
|
|
48
|
+
* core 版本漂移,而转发即真话。端点文档(openapi 段 + docs/ASSISTANT-WIRE-CONTRACT.md)负责告诉消费方
|
|
49
|
+
* 「`bundle.doc` 要呈现给最终用户」。
|
|
50
|
+
*
|
|
51
|
+
* ── 幂等 ────────────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
* 同 bundle 重放的收敛是 **core 的**契约(崩溃后重导同一包收敛;已完成的导入幂等答同一份记录报告)。
|
|
53
|
+
* 本端点**不加**第二层去重:两个写者压同一条语义面,其重叠通常比原缺陷更坏且静默。
|
|
54
|
+
*
|
|
55
|
+
* 计费/lane:两口都是**部署级治理动作**,零模型工作 ⇒ `billable=false`(与 adoption / retention-ops
|
|
56
|
+
* 同族,申明在 test/billable-route-declaration.test.ts)。
|
|
57
|
+
*
|
|
58
|
+
* 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
|
|
59
|
+
*/
|
|
60
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
61
|
+
import type { MemoryImportReport } from "@sema-agent/core";
|
|
62
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
63
|
+
export declare const MEMORY_BUNDLE_EXPORT_PATH = "/v1/memory/export";
|
|
64
|
+
export declare const MEMORY_BUNDLE_IMPORT_PATH = "/v1/memory/import";
|
|
65
|
+
/**
|
|
66
|
+
* `MemoryImportReport` → wire 的**显式白名单**投影。
|
|
67
|
+
*
|
|
68
|
+
* 🔴 为什么是显式白名单而不是整行透传:报告是**治理处置**的清单,core 日后加一个新的处置座(例如又一
|
|
69
|
+
* 类"被扣下的行"),整行透传会让它**静默**上 wire 而没人给它写过契约;白名单让它变成**编译错误**
|
|
70
|
+
* (`test/memory-bundle-http.test.ts` 的差集断言 = `wire-whitelist-exhaustiveness` 同款门形)。
|
|
71
|
+
* 今天**每一个**键都在场:每一条都是 operator 判「这次导入到底发生了什么」必须看到的东西,一个都
|
|
72
|
+
* 不该省 —— 「每个否定处置都被报告,绝不静默」是 core 对这份报告的原话。
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildImportReportWire(r: MemoryImportReport): {
|
|
75
|
+
v: 1;
|
|
76
|
+
bundleHash: string;
|
|
77
|
+
sourceStoreId: string;
|
|
78
|
+
landed: {
|
|
79
|
+
id: string;
|
|
80
|
+
scope: string;
|
|
81
|
+
slug: string;
|
|
82
|
+
}[];
|
|
83
|
+
alreadyPresent: string[];
|
|
84
|
+
refusedErased: string[];
|
|
85
|
+
refusedUntrusted: string[];
|
|
86
|
+
conflicts: {
|
|
87
|
+
id: string;
|
|
88
|
+
reason: string;
|
|
89
|
+
}[];
|
|
90
|
+
governanceWithheld: string[];
|
|
91
|
+
erasedAtSource: string[];
|
|
92
|
+
pollutionDivergence: {
|
|
93
|
+
sessionId: string;
|
|
94
|
+
kept: "destination";
|
|
95
|
+
}[];
|
|
96
|
+
referentialOrphans: string[];
|
|
97
|
+
lineageDivergence: {
|
|
98
|
+
entryId: string;
|
|
99
|
+
sessionId: string;
|
|
100
|
+
kept: "destination";
|
|
101
|
+
}[];
|
|
102
|
+
custodyWithheld: {
|
|
103
|
+
srcEv: string;
|
|
104
|
+
channel: string;
|
|
105
|
+
}[];
|
|
106
|
+
custodyAppended: number;
|
|
107
|
+
};
|
|
108
|
+
export declare function handleMemoryBundle(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
109
|
+
//# sourceMappingURL=memory-bundle.d.ts.map
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { sendJson, sendError } from "../send.js";
|
|
3
|
+
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
4
|
+
export const MEMORY_BUNDLE_EXPORT_PATH = "/v1/memory/export";
|
|
5
|
+
export const MEMORY_BUNDLE_IMPORT_PATH = "/v1/memory/import";
|
|
6
|
+
/** export 请求体 —— **形**验(义归 core;见文件头「验型的分权」)。 */
|
|
7
|
+
const ExportRequestSchema = z.object({ scopes: z.array(z.string().min(1)).min(1) });
|
|
8
|
+
/** import 请求体 —— 只断言 `bundle` 是个**对象**;整包判决是 core 的单一属主面。
|
|
9
|
+
* `z.record(z.string(), z.unknown())` 而不是 `z.object({...})`:在本层写出 bundle 的字段表就是复制
|
|
10
|
+
* core 的 schema(第二真源),而 `z.unknown()` 的 record 恰好表达「结构由下游判」。 */
|
|
11
|
+
const ImportRequestSchema = z.object({ bundle: z.record(z.string(), z.unknown()) });
|
|
12
|
+
/**
|
|
13
|
+
* core 抛出的**已分类**错误 → HTTP 状态。闭集穷举 `switch`:core 新增一个码 ⇒ 这里落 `undefined`
|
|
14
|
+
* ⇒ 走 500 fail-loud(而不是被某个默认臂静默塞进 4xx)。安全轴上的静默默认值正是本仓禁止的东西。
|
|
15
|
+
*
|
|
16
|
+
* · `config.memory_{export,import}_request` = **调用方**给的请求参数不合法(非空/去重/可序列化)⇒ 400;
|
|
17
|
+
* · `memory.export_incomplete` = 店**声明自己说不清**(chain-degraded / 无导出复合面)⇒ 409:不是请求
|
|
18
|
+
* 的错、也不是服务器故障,是**当前状态**下这个包铸不出来(去修 chain 或换后端后重试即改判);
|
|
19
|
+
* · `memory.import_rejected` = 请求**形**合法但**内容**不可接受(整包拒批,零落地)⇒ 422 —— 与 400
|
|
20
|
+
* 分家是承重的:400 说"你的 HTTP 请求写错了",422 说"包本身被判不合格",两句话的运维动作不同。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 每一支都把码**再写一遍字面量**(而不是 `sendError(res, status, code, …)` 把变量透下去):错误码
|
|
23
|
+
* 目录门(test/error-code-catalog-live.test.ts)是**扫源码字面量**的,变量态的码整条对它隐形 ⇒ 这四个
|
|
24
|
+
* 码会「wire 上发得出来、附录 A 里查不到」。同门另有一张中继登记表可走,但那张表是**有上限的棘轮**
|
|
25
|
+
* (「码不写字面量」不许变成默认写法),而这里本来就该是一张闭集词表 —— 写字面量既过门又把闭集写实。
|
|
26
|
+
*/
|
|
27
|
+
// ⚠️ 形参叫 `coreCode` 而不是 `code`:错误体自 core 3.0.0 起**只有** `errorCode`,而 test/error-code-key-gate
|
|
28
|
+
// 扫的是「还在发 legacy `code` 键」的站点(文本判据)——一个叫 `code` 的形参会被它当成那种站点收走。
|
|
29
|
+
function sentCoreCode(res, coreCode, message) {
|
|
30
|
+
switch (coreCode) {
|
|
31
|
+
case "config.memory_export_request":
|
|
32
|
+
sendError(res, 400, "config.memory_export_request", message);
|
|
33
|
+
return true;
|
|
34
|
+
case "config.memory_import_request":
|
|
35
|
+
sendError(res, 400, "config.memory_import_request", message);
|
|
36
|
+
return true;
|
|
37
|
+
case "memory.export_incomplete":
|
|
38
|
+
sendError(res, 409, "memory.export_incomplete", message);
|
|
39
|
+
return true;
|
|
40
|
+
case "memory.import_rejected":
|
|
41
|
+
sendError(res, 422, "memory.import_rejected", message);
|
|
42
|
+
return true;
|
|
43
|
+
default:
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** core 的抛出形 = `Error` + `.code`(engine.js 逐字)。裸 as-cast 禁令下的判别式读法。 */
|
|
48
|
+
function codeOf(err) {
|
|
49
|
+
if (!(err instanceof Error))
|
|
50
|
+
return undefined;
|
|
51
|
+
const { code } = err;
|
|
52
|
+
return typeof code === "string" ? code : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 引擎调用的统一收尾:已分类的码**原样**上 wire(码与 core 的诊断文案都不改写 —— 那句文案是运维排障
|
|
56
|
+
* 的第一手材料,判据帖 G3 就断言它含 "chain-degraded");未分类的一律 500 且**不回显**内部文本。
|
|
57
|
+
* 返回 true = 已应答。
|
|
58
|
+
*/
|
|
59
|
+
function sentClassifiedFailure(res, ctx, face, err) {
|
|
60
|
+
const code = codeOf(err);
|
|
61
|
+
if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
|
|
62
|
+
return true;
|
|
63
|
+
ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
|
|
64
|
+
return false; // 未分类 ⇒ 调用方发它自己那句 500(见下面两处的**内联字面量**)
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* `MemoryImportReport` → wire 的**显式白名单**投影。
|
|
68
|
+
*
|
|
69
|
+
* 🔴 为什么是显式白名单而不是整行透传:报告是**治理处置**的清单,core 日后加一个新的处置座(例如又一
|
|
70
|
+
* 类"被扣下的行"),整行透传会让它**静默**上 wire 而没人给它写过契约;白名单让它变成**编译错误**
|
|
71
|
+
* (`test/memory-bundle-http.test.ts` 的差集断言 = `wire-whitelist-exhaustiveness` 同款门形)。
|
|
72
|
+
* 今天**每一个**键都在场:每一条都是 operator 判「这次导入到底发生了什么」必须看到的东西,一个都
|
|
73
|
+
* 不该省 —— 「每个否定处置都被报告,绝不静默」是 core 对这份报告的原话。
|
|
74
|
+
*/
|
|
75
|
+
// 🔴 **不要**给这个函数标 `: Record<string, unknown>` 返回型:那会让 `keyof ReturnType<…>` 塌成 `string`,
|
|
76
|
+
// 上面那道编译期差集门当场变成**恒真**(vacuous)—— 写这条钉时正踩到过。返回型交给推断,键集才是字面的。
|
|
77
|
+
export function buildImportReportWire(r) {
|
|
78
|
+
return {
|
|
79
|
+
v: r.v,
|
|
80
|
+
bundleHash: r.bundleHash,
|
|
81
|
+
sourceStoreId: r.sourceStoreId,
|
|
82
|
+
landed: r.landed,
|
|
83
|
+
alreadyPresent: r.alreadyPresent,
|
|
84
|
+
refusedErased: r.refusedErased,
|
|
85
|
+
refusedUntrusted: r.refusedUntrusted,
|
|
86
|
+
conflicts: r.conflicts,
|
|
87
|
+
governanceWithheld: r.governanceWithheld,
|
|
88
|
+
erasedAtSource: r.erasedAtSource,
|
|
89
|
+
pollutionDivergence: r.pollutionDivergence,
|
|
90
|
+
referentialOrphans: r.referentialOrphans,
|
|
91
|
+
lineageDivergence: r.lineageDivergence,
|
|
92
|
+
custodyWithheld: r.custodyWithheld,
|
|
93
|
+
custodyAppended: r.custodyAppended,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function handleMemoryBundle(req, res, url, ctx) {
|
|
97
|
+
const miss = { fell: false };
|
|
98
|
+
await handleMemoryBundleBody(req, res, url, ctx, miss);
|
|
99
|
+
return !miss.fell;
|
|
100
|
+
}
|
|
101
|
+
async function handleMemoryBundleBody(req, res, url, ctx, miss) {
|
|
102
|
+
const { deps } = ctx;
|
|
103
|
+
const path = url.split("?")[0] ?? url;
|
|
104
|
+
const isExport = req.method === "POST" && path === MEMORY_BUNDLE_EXPORT_PATH;
|
|
105
|
+
const isImport = req.method === "POST" && path === MEMORY_BUNDLE_IMPORT_PATH;
|
|
106
|
+
if (!isExport && !isImport) {
|
|
107
|
+
miss.fell = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// ── 身份(401)── direct-door safe:验证过的身份,不是可伪造的头。
|
|
111
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
112
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
113
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// ── 授权(403)── 治理面 fail-closed,与 adoption / retention-ops 同一条门。
|
|
117
|
+
// ⚠️ 文案写**内联字面量**而不是抽个模块常量:文案冻结门(test/api-error-text-freeze.test.ts)是**扫源码
|
|
118
|
+
// 文本**的,把 message 换成一个常量名的写法它结构性够不到 ⇒ 这句 wire 文案就没有被冻住。
|
|
119
|
+
// ⚠️ 本段注释里也不许照抄一个带状态码与引号码名的调用样例 —— 那道门的提取器会把**注释里**的样例
|
|
120
|
+
// 当成一条真站点收走(写这条注时真的踩到过:凭空多出一个 `403:code` 对)。
|
|
121
|
+
// 两口同受众同理由,所以两处逐字同一句(族断言按去重集合比,同串出现两次不改判)。
|
|
122
|
+
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
123
|
+
sendError(res, 403, "auth.operator_only", "memory governance bundles are an operator-only deployment action");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// ── 能力(501)── 在授权之后:部署形态不对越权者泄漏。
|
|
127
|
+
// ⚠️ 与 `capability.memory_store_required`(GET 导出面要 DB 平面)**刻意不合并**:两码的运维动作相反
|
|
128
|
+
// (那条要 pg|tidb,本条要引擎在场且后端带 bundle 复合面),消费端真会分支——附录 A 判据②。
|
|
129
|
+
// 🔴 缺席有**两个**成因,文案两个都点名(codex [medium] 后修:此前只说「引擎没接线」,而 pg/tidb 部署
|
|
130
|
+
// 是引擎接了、后端没那两个可选面 —— 只说一半会让运维去查一个根本没问题的旋钮)。两因同码:消费端
|
|
131
|
+
// 的分支一样(本部署做不了 bundle,换部署形态),分成两码只会让码表长一倍而判据不变(判据②)。
|
|
132
|
+
const engineExport = deps.memoryBundleExport;
|
|
133
|
+
const engineImport = deps.memoryBundleImport;
|
|
134
|
+
if (engineExport === undefined || engineImport === undefined) {
|
|
135
|
+
sendError(res, 501, "capability.memory_engine_required", "memory governance bundles are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend implements the v2-c bundle composite (exportSnapshotOf / importBundleCommit); the file memory backend does, the pg/tidb memory backends do not");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// ── 验型(400)── 体读的 413/400(超长 / 坏 JSON)由顶层 typed catch 统一处理(鉴权已完成)。
|
|
139
|
+
const raw = await ctx.helpers.readJson(req);
|
|
140
|
+
if (isExport) {
|
|
141
|
+
const parsed = ExportRequestSchema.safeParse(raw);
|
|
142
|
+
if (!parsed.success) {
|
|
143
|
+
sendError(res, 400, "request.body_shape", "invalid memory export request — expected an object with a non-empty `scopes` array of non-empty strings");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
// 🔴 bundle **原样**下发(含 `doc` 披露段)—— 投影它就是替铸包方复述披露,见文件头。
|
|
148
|
+
const bundle = await engineExport(parsed.data.scopes);
|
|
149
|
+
sendJson(res, 200, bundle);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
// 未分类 ⇒ 500,文案是**内联整串**(不是 `memory bundle ${face} failed` 模板):冻结门的模板臂只
|
|
153
|
+
// 钉到第一个 `${` 为止,"memory bundle " 这个前缀会把「哪一半炸了」留在门外,而那正是消费端能从
|
|
154
|
+
// 500 上读到的全部信息。同族先例:"memory export failed" / "memory sync failed"(memory-policy.ts)。
|
|
155
|
+
if (!sentClassifiedFailure(res, ctx, "export", err))
|
|
156
|
+
sendError(res, 500, "internal.error", "memory bundle export failed");
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const parsed = ImportRequestSchema.safeParse(raw);
|
|
161
|
+
if (!parsed.success) {
|
|
162
|
+
sendError(res, 400, "request.body_shape", "invalid memory import request — expected an object with a `bundle` object");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
// 整包判决(结构/完整性/授权信封)全在 core:本层递交的就是调用方给的那个对象。
|
|
167
|
+
// 递交的是**未校验**的对象:整包判决(结构/完整性/授权信封)是 core 的单一属主面,所以 seam 的
|
|
168
|
+
// 入参类型就写 `unknown` —— 在本层把它断言成 MemoryExportBundle 是**说谎**(我们并不知道它是),
|
|
169
|
+
// 而说谎的那一下正好把类型检查关灯(type-hygiene 门抓的就是这个)。
|
|
170
|
+
const report = await engineImport(parsed.data.bundle);
|
|
171
|
+
sendJson(res, 200, buildImportReportWire(report));
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
if (!sentClassifiedFailure(res, ctx, "import", err))
|
|
175
|
+
sendError(res, 500, "internal.error", "memory bundle import failed");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=memory-bundle.js.map
|
package/dist/http/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
|
-
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry } from "@sema-agent/core";
|
|
3
|
+
import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport } from "@sema-agent/core";
|
|
4
4
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
5
5
|
import type { ServiceConfig } from "../config-types.js";
|
|
6
6
|
import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
|
|
@@ -290,6 +290,20 @@ export interface ServiceSeamDeps {
|
|
|
290
290
|
* (MEMORY_ENGINE_BACKEND=pg|tidb); absent / file posture ⇒ 501 honestly (a single-user file plane
|
|
291
291
|
* IS the TOC side of a sync — it has no central-authority half to serve). */
|
|
292
292
|
memorySync?: (scope: string, syncReq: MemorySyncRequest) => Promise<MemorySyncResponse>;
|
|
293
|
+
/** #264 v2-c(core 5.40.0 `MemoryEngine.exportMemoryScopes`):治理完备包的**携出**面。main.ts 把它接到
|
|
294
|
+
* 装配好的 memory engine 上。启用 `POST /v1/memory/export`(operator-only)。
|
|
295
|
+
* 🔴 缺席 = 这个部署**没有记忆引擎**(`MEMORY_ENGINE=off` / 多租 file 底座等暗态)⇒ 501
|
|
296
|
+
* `capability.memory_engine_required`。注意与 {@link memoryExport} 的**方向相反**:那条要 DB 记忆
|
|
297
|
+
* 平面,本条只要引擎在场 —— 而 bundle 复合面(`exportSnapshotOf`)今天只有 core 自带的 File 后端
|
|
298
|
+
* 实装,SQL 孪生上引擎会自己响亮拒(`memory.export_incomplete` 原样上 wire)。理由全文见
|
|
299
|
+
* routes/memory-bundle.ts 的「能力面的诚实形」。 */
|
|
300
|
+
memoryBundleExport?: (scopes: readonly string[]) => Promise<MemoryExportBundle>;
|
|
301
|
+
/** #264 v2-c(core 5.40.0 `MemoryEngine.importMemoryBundle`):治理完备包的**导入**面(治理写面)。
|
|
302
|
+
* 启用 `POST /v1/memory/import`(operator-only)。缺席语义与 {@link memoryBundleExport} 逐字相同 ——
|
|
303
|
+
* 两口共用一个能力判据,因为它们是同一个引擎面的两半(一个部署不可能只有其中一半)。 */
|
|
304
|
+
memoryBundleImport?: (bundle: unknown, opts?: {
|
|
305
|
+
expectedScopes?: readonly string[];
|
|
306
|
+
}) => Promise<MemoryImportReport>;
|
|
293
307
|
/** design/170 件A §7 —— org 记忆授权目录,`boot/org-memory.ts` 装配的**同一个实例**(准入 seam 与
|
|
294
308
|
* 本面共享它的 TTL 缓存/退避窗/gen 高水位)。memory-policy 面的 `org:` 属主门用它回答成员性:
|
|
295
309
|
* 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键
|
|
@@ -574,8 +588,20 @@ export declare function isBillableSubmitPath(url: string): boolean;
|
|
|
574
588
|
* 🔴 **方法感知**(codex 对抗复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
|
|
575
589
|
* 合取里 —— 于是一个自称「持久改写」的谓词把爆炸半径最大的那条动词漏在门外,正是 `isDestructiveSessionWrite`
|
|
576
590
|
* 当年被抓到的同一形。签名因此收方法,与那只谓词逐字同形。
|
|
577
|
-
* 🔴
|
|
578
|
-
*
|
|
591
|
+
* 🔴 读面**原则上**不进本表:`GET /v1/adoption/:id` 与 `GET /v1/rules` 各有自己的**属主门**兜底,本门只拦写。
|
|
592
|
+
*
|
|
593
|
+
* 🔴 **第二个合取项的加宽**(codex 对抗复审 [high],#277 提货批,验真后修)——判据现在是
|
|
594
|
+
* 「持久改写 **或** 授权唯一输入是 principal 头的**跨租户治理读**」。加宽是为了收进 #264 v2-c 的两口:
|
|
595
|
+
* · `POST /v1/memory/import` —— 直接落在**原**判据上,而且是本仓最深的那一种持久改写:它把别处铸的
|
|
596
|
+
* 条目 / 挑战 / 污染标记 / 托管链落进本部署的记忆平面。伪造一个在册 operator 头 ⇒ 一份**攻击者自制的
|
|
597
|
+
* 治理史**成为本店的账;而记忆是模型**每次挂载都读**的面,比种一条常驻 allow 规则更深(那条只影响
|
|
598
|
+
* ask 会不会问,这条影响模型相信什么)。
|
|
599
|
+
* · `POST /v1/memory/export` —— 是**读**,按原措辞本不该进。但被点名豁免的那两条读各有属主门兜底,
|
|
600
|
+
* 而本条**没有**:它的授权判据就是 `explicitOperatorOk`,过了就是**全库**——一次调用取走每个 scope 的
|
|
601
|
+
* 条目正文 + 托管链 + 跨会话污染标记 + lineage。把一次全租户记忆外泄留在门外、只为守住「本门只拦写」
|
|
602
|
+
* 这句措辞,是把措辞看得比它要保护的东西更重。加宽后的判据仍然是**闭**的(不是「写门以外都进」):
|
|
603
|
+
* 它要求「授权的唯一输入是那个头」∧「一次调用的爆炸半径跨租户」,`GET /v1/rules` 那种属主门读不满足。
|
|
604
|
+
* 行为面钉:`test/rewrite-door-service-token-gate.test.ts`(六扇门各一正一反 + 逃生口格 + 读面负控)。
|
|
579
605
|
*/
|
|
580
606
|
export declare function isCredentialGatedRewrite(method: string, url: string): boolean;
|
|
581
607
|
/** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
|