@sema-agent/server 7.72.0 → 7.73.1
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/README.md +1 -1
- package/USAGE.md +10 -0
- package/dist/boot/config-center.d.ts +6 -1
- package/dist/boot/config-center.js +3 -1
- package/dist/boot/memory-boundary.d.ts +27 -5
- package/dist/boot/memory-boundary.js +6 -6
- package/dist/boot/memory-consolidation.js +2 -1
- package/dist/boot/runner-deps.js +3 -1
- package/dist/boot/stores.js +6 -4
- package/dist/boot/unpriced-ceiling-verdict.d.ts +15 -0
- package/dist/boot/unpriced-ceiling-verdict.js +39 -0
- package/dist/budget.d.ts +10 -2
- package/dist/budget.js +2 -2
- package/dist/capabilities/tool-defer.d.ts +18 -12
- package/dist/config-types.d.ts +15 -7
- package/dist/config.d.ts +28 -12
- package/dist/config.js +26 -9
- package/dist/http/active-run-conflict.d.ts +3 -2
- package/dist/http/routes/runs.js +13 -10
- package/dist/http/server.js +15 -7
- package/dist/main.js +5 -5
- package/dist/memory-bundle-engine.d.ts +4 -3
- package/dist/memory-bundle-engine.js +2 -1
- package/dist/memory-incident-sink.d.ts +37 -0
- package/dist/memory-incident-sink.js +13 -0
- package/dist/memory-operator-faces.d.ts +9 -3
- package/dist/memory-operator-faces.js +4 -3
- package/dist/memory-scope.d.ts +37 -1
- package/dist/memory-scope.js +12 -2
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/plugins/approval-ask-store-memory.js +3 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +5 -0
- package/dist/plugins/approval-ask-store-sql.js +7 -0
- package/dist/plugins/file-run-store.js +1 -0
- package/dist/plugins/memory-run-store.js +1 -0
- package/dist/plugins/run-store-sql.d.ts +5 -2
- package/dist/plugins/run-store-sql.js +5 -3
- package/dist/plugins/store-contracts.d.ts +14 -0
- package/dist/run-local.js +15 -7
- package/dist/spec-fields.d.ts +4 -2
- package/dist/tool-approval.js +1 -1
- package/dist/trace/core-keyset-guard.d.ts +8 -2
- package/dist/trace/project.d.ts +43 -0
- package/dist/trace/project.js +26 -0
- package/dist/usage-analytics.d.ts +11 -1
- package/dist/usage-analytics.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
164
164
|
| `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
|
|
165
165
|
| `WRITE_PROTECTED_EXTRA` | unset (engine default table) | **Adds** rows to core's write-protection table (the literal-name table whose hit demotes a surviving `allow` to `ask` on Write/Edit/NotebookEdit). Comma-separated bare names (a bare name matches ANY path segment; a name containing `/` matches a consecutive segment run) or a JSON array of `"name"` strings / `{name, kind}` rows (`kind`: `basename` \| `segment` \| `segment-run`). The value is composed as `[...WRITE_PROTECTED_DEFAULT_TABLE, …]`, so **no default row can be lost**. Unset = no seat is wired = the engine's default table is in force (this server never copies that table). Form discrimination is by content, not by first character: a value containing any JSON structural character (`[ ] { } "`) is parsed as JSON and **must** be a top-level array (a missing pair of brackets refuses to start instead of being split into junk bare names). An empty value, a malformed entry (glob metacharacter, unknown kind, kind/name mismatch) or setting this together with `WRITE_PROTECTED_TABLE_REPLACE` **refuses to start**. Read faces: `GET /v1/capabilities` → `writeProtection.{armed,rows,replaced}`; `GET /v1/diagnostics/wiring` → `writeProtection.{rows,source,droppedDefaultRows}` (operator-only) |
|
|
166
166
|
| `WRITE_PROTECTED_TABLE_REPLACE` | unset (engine default table) | **Replaces the whole** write-protection table (core's seat is whole-table by contract). JSON array only — deliberately no comma shorthand, because a slipped bare string would swap 51 default rows for one. `[]` = the explicit "no write-protection table at all" posture. Replacing logs one **loud** boot line naming every default row you dropped (`write_protection_table_replaced`; the empty posture logs `write_protection_table_disabled`) — use `WRITE_PROTECTED_EXTRA` when you meant to ADD. Same refuse-to-start conditions as its sibling, plus: both knobs set = two writers on one surface = refuses to start |
|
|
167
|
-
| `MANUAL_MODE_SHELL_GATE` | unset | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence). **Unset is not "off"**: since 7.12.0 the caller's explicit `permissionMode` supplies the baseline this knob tightens from (`bypassPermissions` → `off`, `auto`/`default`/`acceptEdits`/`plan` → `classify`; **no** mode stated → no gate,
|
|
167
|
+
| `MANUAL_MODE_SHELL_GATE` | unset | `always`\|`classify` — tighten `Bash` into the approval chain, applied unconditionally at the governance layer (≥7.1.0: independent of client permission mode, lane, or settings presence). **Unset is not "off"**: since 7.12.0 the caller's explicit `permissionMode` supplies the baseline this knob tightens from (`bypassPermissions` → `off`, `auto`/`default`/`acceptEdits`/`plan` → `classify`; **no** mode stated → core's `off` default). Since 7.73.0 (core 7.15.0) `off` no longer means "no gate at all": the READ BOUNDARY (built-in read-deny tiers + workspace containment) is judged under **every** doctrine, so on the `off` lane a deny-listed read, an out-of-root read or a recursive read form still raises exactly ONE mandated approval (no stored rule and no auto-mode classifier can clear it); `shellGate` now governs only the RESIDUAL shell risk. Non-readers (`rm`/`curl`/`git`/`npm`) and ordinary in-workspace reads stay unasked on that lane. This knob only ever raises that baseline — it has no relax half, so `off` is accepted as an explicit **no-op** (a boot line says so; not symmetric with `SENSITIVE_WRITE_PATTERNS=off`, which really does clear a set). **Any other value refuses to start** (7.12.0, BREAKING for a deployment that had a typo: it was previously treated as unset, i.e. silently no gate at all) |
|
|
168
168
|
| `PERMISSIONS_DISABLE_AUTO_MODE` | `false` | Local mirror of CC `permissions.disableAutoMode` — the **org deny** bit for `permissionMode:"auto"` (paired with core's intent-arming rule). **Tighten-only**: `true` folds every principal's `runtimeCaps.autoMode` to `false` (a center grant cannot flip it back); unset leaves caps untouched, so on a center-less box a shell asking for `auto` **arms** the classifier once the paired core (intent-arming rule "requested ∧ classifier seat ∧ `autoMode !== false`" — absence is not a deny) is installed; on core 7.2.0 the engine still uses the old "org grant" rule (`permissionModeAuto.intentArming:false`), so the self-check answers `armed:true` only for a center-granted principal and `deployment_incapable` on a center-less box. Boolean word table; any other value refuses to start. Self-check: `GET /v1/capabilities?permissionMode=auto` → `permissionModeAuto.{armed, reason, model}` (USAGE §9.4) |
|
|
169
169
|
| `SCRATCHPAD_SWEEP_TTL_MS` | 7 days | Idle-reap window for per-session scratchpad dirs (by dir mtime; `0` disables). The scratchpad is **ephemeral by contract**: replica-local disk, NOT part of the durable-suspend persistence set — a resume on a different replica, or after a sweep, starts with an empty dir (same two-track posture as the Agent SDK hosting doc: conversation persists, working-directory artifacts don't). Raise/disable only on single-replica deployments that park approvals for longer than the window |
|
|
170
170
|
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
|
package/USAGE.md
CHANGED
|
@@ -1293,6 +1293,16 @@ tag 词表与每条的最坏后果见 `docs/FAIL-OPEN-CENSUS.md`。引擎装配
|
|
|
1293
1293
|
"bypassPermissions"` 时是调用方要的姿态,降为 **info** 并标 `expected`——只有没表态落到引擎默认 off 的无壳直连
|
|
1294
1294
|
形才 warn。
|
|
1295
1295
|
|
|
1296
|
+
🔴 **`shell-gate-off` 这条注的语义自 7.73.0(core 7.15.0)起收窄,文案由引擎改写**:`off` 档**不再**意味着
|
|
1297
|
+
「这条会话的 shell 完全没有门」。**读边界**(内建读拒表 + 工作区容纳面)现在**每一档都判**,`shellGate` 只管
|
|
1298
|
+
**剩余**风险 ⇒ `off` 车道下一条读拒表路径(`cat .claude/settings.json`)、工作区外路径,或递归读形
|
|
1299
|
+
(`grep -r … .` / `find .` / `ls -R` / `du .`)都会产**恰一个 mandated 审批**(规则与 auto 分类器都清不掉);
|
|
1300
|
+
**非读**命令(`rm` / `curl` / `git` / `npm`)与工作区内的普通读在 `off` 下仍然零审批。注文案里那句
|
|
1301
|
+
「the read boundary is still judged on every command」就是这件事。⇒ **运维侧后果**:一台把 `bypassPermissions`
|
|
1302
|
+
当「无人值守直跑」用的部署,现在会在这三类命令上落审批(无活体席 ⇒ durable park,出现在 `GET /v1/approvals`)。
|
|
1303
|
+
要让它们重新不问,得从**读面**下手(`READ_FACE` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE`,
|
|
1304
|
+
见 `docs/DEPLOY-PREREQS.md`),`permissionMode` 那根杆子够不着它。
|
|
1305
|
+
|
|
1296
1306
|
### 9.1 limits.approachNotice(6.1.0 起)
|
|
1297
1307
|
|
|
1298
1308
|
引擎默认在上下文用量到 80%/95% 时各注入一次接近提示(SSE 上是 `steering_injected` 帧、
|
|
@@ -7,6 +7,7 @@ import { type ServiceConfig } from "../config.js";
|
|
|
7
7
|
import { type ModelKeyRef } from "../key-resolver.js";
|
|
8
8
|
import type { Logger } from "../observability/logger.js";
|
|
9
9
|
import type { Metrics } from "../observability/metrics.js";
|
|
10
|
+
import type { UnpricedCeilingVerdict } from "./unpriced-ceiling-verdict.js";
|
|
10
11
|
import { type RestartSignal } from "../config-center/facade.js";
|
|
11
12
|
import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
|
|
12
13
|
import type { LimitSync } from "./limit-sync.js";
|
|
@@ -45,12 +46,16 @@ export interface ConfigCenterRuntime {
|
|
|
45
46
|
* `swapRunnerDeps`(core 7.10.0 `Runner.swapDeps` 一扇门;5.49 起的 `swapModels` 口在 7.10.0 退役、
|
|
46
47
|
* 本参数同语义改名)= **可换席**的换装口:模型面 plane 变更候选在 commit 前喂给全部 boot Runner
|
|
47
48
|
* 原子换代(在场 ⇒ models-tiers defer/restart 臂退役,缺席 ⇒ 旧 defer 行为逐字保留);S-174 起
|
|
48
|
-
* read **face** 席也走同一只函数(见 refresh 拍里的 `applyCenterReadFace` 调用点)。
|
|
49
|
+
* read **face** 席也走同一只函数(见 refresh 拍里的 `applyCenterReadFace` 调用点)。
|
|
50
|
+
* `ceilingVerdict`(S-232)= 「配了 $ 天花板但目录未定价」这条判决的活载体,与 `limitSync` **同族同序**
|
|
51
|
+
* (commit 之后、零 await 内重判一次;判决不翻转就静默)。缺席 = 这条腿本部署没装配(run-local 一次性
|
|
52
|
+
* CLI 之后不再有刷新拍,它继续用纯判据判一次)。 */
|
|
49
53
|
startRefreshLoop(a: {
|
|
50
54
|
runnerTierFrozen: boolean;
|
|
51
55
|
pricing: ReturnType<typeof buildPricing>;
|
|
52
56
|
limitSync?: LimitSync;
|
|
53
57
|
swapRunnerDeps?: (next: SwappableDeps) => void;
|
|
58
|
+
ceilingVerdict?: UnpricedCeilingVerdict;
|
|
54
59
|
}): void;
|
|
55
60
|
/** [ref]-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。 */
|
|
56
61
|
stopRefreshLoop(): void;
|
|
@@ -510,7 +510,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
510
510
|
}
|
|
511
511
|
},
|
|
512
512
|
startRefreshLoop(a) {
|
|
513
|
-
const { runnerTierFrozen, pricing, limitSync, swapRunnerDeps } = a;
|
|
513
|
+
const { runnerTierFrozen, pricing, limitSync, swapRunnerDeps, ceilingVerdict } = a;
|
|
514
514
|
if (configProvider) {
|
|
515
515
|
const ccRef = config.configCenter;
|
|
516
516
|
let refreshInFlight = false;
|
|
@@ -594,6 +594,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
594
594
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
595
595
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
596
596
|
limitSync?.sync();
|
|
597
|
+
ceilingVerdict?.judge();
|
|
597
598
|
mcpRevocations.applyEnabledEntries(enabledMcpEntries(r.effective.mcp));
|
|
598
599
|
applyLiveReadFace(r.effective, swapRunnerDeps, "refresh");
|
|
599
600
|
applyLedger.applied(tickGen, r.effective.version);
|
|
@@ -741,6 +742,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
741
742
|
mutateInPlace(pricing, buildPricing(config.models));
|
|
742
743
|
keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
|
|
743
744
|
limitSync?.sync();
|
|
745
|
+
ceilingVerdict?.judge();
|
|
744
746
|
applyLiveReadFace(r.effective, swapRunnerDeps, "boot-deferred");
|
|
745
747
|
applyLedger.applied(lateGen, r.effective.version);
|
|
746
748
|
if (planeDeferredLate)
|
|
@@ -9,10 +9,26 @@
|
|
|
9
9
|
* `~/.ai-agent`(或 `AGENT_DATA_DIR`),默认围栏是任务 cwd —— 两者天然不相交:用户说「记住 X」,模型照
|
|
10
10
|
* 提示词去 Write,拿回一个 `path_not_in_root`,而回执早就发出去了。
|
|
11
11
|
*
|
|
12
|
-
* 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount /
|
|
13
|
-
*
|
|
12
|
+
* 判据形态 = **纯函数 + 启动 warn,不拒启**。理由是诚实:共享挂载 / bind mount / 部署方自己在别处开了写通道,
|
|
13
|
+
* 都是真实可行的部署形,而我方在 boot 期看不全。看得全的是「按我们自己接的线,
|
|
14
14
|
* 这条链**结构上**走不通」——那句必须说出来。围栏根算不出来时**不说话**(判不了就闭嘴,别把猜测当告警)。
|
|
15
15
|
*
|
|
16
|
+
* 🔴 **已登记的判据限度(不在本批射程,别把它读成「判据已闭合」)**:② 号 warn 今天唯一算得出围栏根的
|
|
17
|
+
* 腿是 `REMOTE_EXEC` 未设的 in-process 形(`boot/stores.ts` 的 `containmentRoots`),而**恰恰是这条腿
|
|
18
|
+
* 没有文件工具** —— 亲读装的这台 core:`prepare-execution-env.js` 的
|
|
19
|
+
* `handsEnabled = ownedEnv !== undefined || deps.executionEnv !== undefined`,本仓不设 `REMOTE_EXEC` 时
|
|
20
|
+
* `executionEnvFactory` 恒 `undefined` ⇒ `handsEnabled=false` ⇒ `prepareHandsMount` 整个不跑。所以这条
|
|
21
|
+
* warn 描述的失败形(「模型照提示词去 Write,拿回 `path_not_in_root`」)在该腿上并不成立:真相是**一只
|
|
22
|
+
* 文件工具都没有**,而按它给的第一条恢复路径(把根挪进 workspace)会让 warn 闭嘴却仍然写不成。
|
|
23
|
+
* 把判据换成「执行能力 + core 自己的 advertised-writable-dir 准入结局」是一次**新的探测臂**(要读 per-task
|
|
24
|
+
* 的准入结果,而本模块是 boot 期纯函数),按复审停机纪律不在提货批里开;登记在此,免下一位重新发现。
|
|
25
|
+
*
|
|
26
|
+
* 🔴 **不再劝 `additionalDirectories`**(core 7.16.0 [ref] 提货):每个 scope 的家是 `<挂载平面>/<label>`,
|
|
27
|
+
* 平面本身谁的家都不是。交配置根 = 连引擎控制面(`.engine`)一起放行;交平面 = 把每个兄弟 scope 的家
|
|
28
|
+
* (含 org 只读层)一起交给模型;交写家 operator 事前说不出口(label 的真源是引擎注册簿),而 core 的
|
|
29
|
+
* 准入席([ref])本来就自动把写家教给模型。三条路各自要么危险要么多余 ⇒ 整条建议删,只留「挪根」与
|
|
30
|
+
* 「表态 capable=false」两条。
|
|
31
|
+
*
|
|
16
32
|
* ⚠️ 这不是 fail-open 的托词:真正的 fail-closed 归属面在别处(多租户记忆整体 dark、remote lane 默认 dark)。
|
|
17
33
|
* 本模块判的是「已经决定要开记忆」之后的**可达性**,而可达性的证据一半在部署环境里,不在进程里。
|
|
18
34
|
*
|
|
@@ -47,8 +63,8 @@
|
|
|
47
63
|
* 工具就跑在**这台机器**的 fs 上(`hostSemanticsLane`,与 core 的 remote 判决是两根轴,见下方 facts 注),
|
|
48
64
|
* 记忆根也在这台机器上,「执行车道与记忆根共享文件平面」这个 capable=true 所断言的**车道级事实**由
|
|
49
65
|
* 构造成立。剩下的围栏维(记忆根是否落在该次任务的 fs 授权边界内)在 in-process 上**同样存在**而 core
|
|
50
|
-
* 并不 withhold——它由本模块的 ② 号 warn、fs 门的 `path_not_in_root`
|
|
51
|
-
*
|
|
66
|
+
* 并不 withhold——它由本模块的 ② 号 warn、fs 门的 `path_not_in_root` 拒绝、以及 core 准入席自动放行写家
|
|
67
|
+
* ([ref])这套既有处置管,不归 withhold 判据管。所以 host 腿自动 true 与 in-process 的默认行为**同等诚实**。
|
|
52
68
|
* 沙箱腿(e2b/k8s/ssh/local-docker)文件平面在沙箱盘,withhold 是对的,恒不自动。
|
|
53
69
|
*
|
|
54
70
|
* ## 为什么不是「围栏覆盖记忆根」的更严合取
|
|
@@ -69,7 +85,13 @@ export declare function effectiveMemoryPersistenceCapable(config: {
|
|
|
69
85
|
}): boolean | undefined;
|
|
70
86
|
/** boot 期可知的全部事实。每一位都必须由调用点从真配置/真装配结果取,禁在本模块内重算(重算=第二真源)。 */
|
|
71
87
|
export interface MemoryWriteBoundaryFacts {
|
|
72
|
-
/**
|
|
88
|
+
/** 引擎的**挂载平面**根 —— 引擎真正读写条目的那棵树,由 `memoryMountRootOf(backend, root)` 单铸点取
|
|
89
|
+
* (file 腿 = `<配置根>/memory`,两只 SQL 孪生不钉平面 ⇒ 回到配置根)。`undefined` = 引擎未接 ⇒ 本判据
|
|
90
|
+
* 整体不适用。
|
|
91
|
+
* 🔴 **不是配置根**(2026 提货批订正):配置根比挂载平面高一层,拿它判包含会把「记忆目录在围栏里、
|
|
92
|
+
* 配置根不在」这一真实可行的部署形误报成「结构性写不进」——而那台机器上模型其实存得下。
|
|
93
|
+
* core 7.16.0([ref])之后写家还要再深一层(`<平面>/<label>`),但 label 归引擎注册簿,boot 期说不出口;
|
|
94
|
+
* 平面是**包含关系上等价**的那一只(家恒为平面的直接子目录),所以判据停在平面,不猜 label。 */
|
|
73
95
|
memoryRoot: string | undefined;
|
|
74
96
|
/** 执行车道名,只进文案(`config.remoteExec?.provider ?? "in-process"`)。 */
|
|
75
97
|
lane: string;
|
|
@@ -45,12 +45,12 @@ export function buildMemoryWriteBoundaryAudit(facts) {
|
|
|
45
45
|
warnings: [
|
|
46
46
|
{
|
|
47
47
|
tag: "memory_root_outside_write_boundary",
|
|
48
|
-
detail: `the memory engine
|
|
49
|
-
`(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into
|
|
50
|
-
`so a "remember this" turn will be answered with a save the file tools
|
|
51
|
-
`
|
|
52
|
-
`
|
|
53
|
-
`deployment cannot save instead of receipting a write that never lands.`,
|
|
48
|
+
detail: `the memory engine mount plane (${memoryRoot}) is outside every file-tool containment root known at boot ` +
|
|
49
|
+
`(${containmentRoots.join(", ")}). Memory has no write TOOL — the model saves by writing files into the write ` +
|
|
50
|
+
`scope's home under that plane — so a "remember this" turn will be answered with a save the file tools ` +
|
|
51
|
+
`structurally refuse (path_not_in_root). ` +
|
|
52
|
+
`Fix it in one of two ways: ${relocateRoot}, or declare MEMORY_PERSISTENCE_CAPABLE=false so the model is told ` +
|
|
53
|
+
`up front that this deployment cannot save instead of receipting a write that never lands.`,
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
+
import { memoryMountRootOf } from "../memory-scope.js";
|
|
2
3
|
import { expandTiers, MemoryEngine, readConsolidationDriverRun, resolveMemoryConsolidationDriver, runMemoryConsolidationDriver, } from "@sema-agent/core";
|
|
3
4
|
export function resolveConsolidationDriverSeat(deps) {
|
|
4
5
|
let options;
|
|
@@ -109,7 +110,7 @@ export function buildStopDetailAudit(input) {
|
|
|
109
110
|
export function createMemoryConsolidationFaces(store, input) {
|
|
110
111
|
const engine = new MemoryEngine({
|
|
111
112
|
backend: store.backend,
|
|
112
|
-
memoryDir: store.root,
|
|
113
|
+
memoryDir: memoryMountRootOf(store.backend, store.root),
|
|
113
114
|
consolidation: {},
|
|
114
115
|
...(input.provenance !== undefined ? { provenance: input.provenance } : {}),
|
|
115
116
|
...(input.onIncident !== undefined ? { onIncident: input.onIncident } : {}),
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createFileWorkflowScriptStore } from "@sema-agent/core";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
|
|
4
4
|
import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
|
|
5
|
+
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
5
6
|
import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
|
|
6
7
|
import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
|
|
7
8
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
@@ -91,6 +92,7 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
91
92
|
}
|
|
92
93
|
export function createRunnerDeps(ctx) {
|
|
93
94
|
const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, legacyRewindBoundaryProbe, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
|
|
95
|
+
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
94
96
|
const sharedRunnerDeps = createSharedRunnerDeps(ctx);
|
|
95
97
|
const workflowModelAllowlist = workflowModelAllowlistFor(config);
|
|
96
98
|
const selfOrchestrationDeps = config.selfOrchestrationEnabled
|
|
@@ -207,7 +209,7 @@ export function createRunnerDeps(ctx) {
|
|
|
207
209
|
return;
|
|
208
210
|
}
|
|
209
211
|
if (ctx.phase === "memory") {
|
|
210
|
-
|
|
212
|
+
memoryEngineIncident(err, ctx.sessionId !== undefined ? { sessionId: ctx.sessionId } : undefined);
|
|
211
213
|
return;
|
|
212
214
|
}
|
|
213
215
|
if (ctx.phase === "mcp") {
|
package/dist/boot/stores.js
CHANGED
|
@@ -20,7 +20,8 @@ import { ensurePgTaskListSchema, ensureTiDBTaskListSchema } from "../plugins/tas
|
|
|
20
20
|
import { assertDeviceAuditRebindEventSchema, ensurePgDeviceSchema, ensureTiDBDeviceSchema } from "../plugins/device-store-sql.js";
|
|
21
21
|
import { mysqlDriver, pgDriver } from "../plugins/sql-driver.js";
|
|
22
22
|
import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "../plugins/store-backend.js";
|
|
23
|
-
import {
|
|
23
|
+
import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
|
|
24
|
+
import { buildMemoryRemoteLaneWarn, memoryEngineBackendFor, memoryEngineRemoteLanePosture, memoryEngineRootFor, memoryMountRootOf } from "../memory-scope.js";
|
|
24
25
|
import { assertToolResultProvenanceSchema } from "../plugins/tool-result-store-sql.js";
|
|
25
26
|
import { assertPermissionRuleApprovalSchema, assertPermissionRuleTicketSchema } from "../plugins/permission-rule-store-sql.js";
|
|
26
27
|
import { assertApprovalAskRuleMaterialSchema } from "../plugins/approval-ask-store-sql.js";
|
|
@@ -193,7 +194,7 @@ export async function openStores(ctx) {
|
|
|
193
194
|
});
|
|
194
195
|
}
|
|
195
196
|
else {
|
|
196
|
-
memoryEngine = memoryEngineBackendFor(config);
|
|
197
|
+
memoryEngine = memoryEngineBackendFor(config, undefined, createMemoryEngineIncidentSink({ logger, metrics }));
|
|
197
198
|
if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
|
|
198
199
|
logger.warn("memory_engine_dir_defaulted", {
|
|
199
200
|
root: memoryEngine.root,
|
|
@@ -206,8 +207,9 @@ export async function openStores(ctx) {
|
|
|
206
207
|
const hostSemanticsLane = lane === undefined || lane === "host";
|
|
207
208
|
const coreRemoteExecutionEnv = config.remoteExec !== undefined;
|
|
208
209
|
const containmentRoots = lane === undefined ? [process.cwd()] : [];
|
|
210
|
+
const memoryMountRoot = memoryEngine ? memoryMountRootOf(memoryEngine.backend, memoryEngine.root) : undefined;
|
|
209
211
|
const audit = buildMemoryWriteBoundaryAudit({
|
|
210
|
-
memoryRoot:
|
|
212
|
+
memoryRoot: memoryMountRoot,
|
|
211
213
|
lane: lane ?? "in-process",
|
|
212
214
|
hostSemanticsLane,
|
|
213
215
|
coreRemoteExecutionEnv,
|
|
@@ -216,7 +218,7 @@ export async function openStores(ctx) {
|
|
|
216
218
|
engineBackend: config.memoryEngineBackend,
|
|
217
219
|
});
|
|
218
220
|
for (const w of audit.warnings)
|
|
219
|
-
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root:
|
|
221
|
+
logger.warn(w.tag, { detail: w.detail, lane: lane ?? "in-process", root: memoryMountRoot ?? null });
|
|
220
222
|
if (config.memoryPersistenceCapable === undefined && effectiveMemoryPersistenceCapable(config) === true) {
|
|
221
223
|
logger.info?.("memory_persistence_capable_auto_declared", {
|
|
222
224
|
lane: lane ?? "in-process",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
2
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
|
+
export interface UnpricedCeilingVerdictCtx {
|
|
4
|
+
/** 活配置(config-center 热应用就地改写的**同一个**对象——所以每次 `judge()` 读到的是当代值)。 */
|
|
5
|
+
readonly config: Pick<ServiceConfig, "models" | "maxTaskCostUsd" | "maxPrincipalCostUsd">;
|
|
6
|
+
readonly logger?: Logger;
|
|
7
|
+
}
|
|
8
|
+
export interface UnpricedCeilingVerdict {
|
|
9
|
+
/** 按**当下**的目录与限额重判一次。判决与上一次相同 ⇒ 静默;翻成「有」⇒ 一条 warn;翻成「无」⇒
|
|
10
|
+
* 一条 info `cost_ceiling_warning_cleared{reason}`(告警为什么不再成立,是运维要看见的事实;静默回落
|
|
11
|
+
* 读不出来,而不带原因的「恢复」在护栏被撤光时是假话)。幂等、零抛。 */
|
|
12
|
+
judge(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createUnpricedCeilingVerdict(ctx: UnpricedCeilingVerdictCtx): UnpricedCeilingVerdict;
|
|
15
|
+
//# sourceMappingURL=unpriced-ceiling-verdict.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { buildUnpricedCeilingNotice, costCeilingPricingPosture, unpricedCeilingClearReasonOf } from "../config.js";
|
|
2
|
+
function verdictKey(notice) {
|
|
3
|
+
return notice?.fields.ceilings;
|
|
4
|
+
}
|
|
5
|
+
export function createUnpricedCeilingVerdict(ctx) {
|
|
6
|
+
const { config, logger } = ctx;
|
|
7
|
+
let last;
|
|
8
|
+
let judged = false;
|
|
9
|
+
return {
|
|
10
|
+
judge() {
|
|
11
|
+
const notice = buildUnpricedCeilingNotice(config.models, config);
|
|
12
|
+
const key = verdictKey(notice);
|
|
13
|
+
const first = !judged;
|
|
14
|
+
judged = true;
|
|
15
|
+
if (!first && key === last)
|
|
16
|
+
return;
|
|
17
|
+
last = key;
|
|
18
|
+
if (notice) {
|
|
19
|
+
logger?.warn?.(notice.event, notice.fields);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (first)
|
|
23
|
+
return;
|
|
24
|
+
const reason = unpricedCeilingClearReasonOf(costCeilingPricingPosture(config.models, config));
|
|
25
|
+
if (reason === undefined)
|
|
26
|
+
throw new Error("unpriced-ceiling verdict: notice absent but no clear reason — judge and posture disagree");
|
|
27
|
+
logger?.info?.("cost_ceiling_warning_cleared", { reason, note: CLEAR_NOTE[reason] });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const CLEAR_NOTE = {
|
|
32
|
+
no_ceiling: "every $ ceiling (MAX_TASK_COST_USD / MAX_PRINCIPAL_COST_USD) is now unset — the earlier cost_ceiling_without_pricing " +
|
|
33
|
+
"warning no longer applies, but note this deployment has NO $ cost guardrail (by configuration, not by omission).",
|
|
34
|
+
empty_catalog: "the effective catalog is empty — there is nothing to price, so the earlier cost_ceiling_without_pricing warning is " +
|
|
35
|
+
"moot until models arrive; the $ ceilings still cannot fire.",
|
|
36
|
+
priced: "the effective catalog declares a price table again — the $ ceilings (MAX_TASK_COST_USD / MAX_PRINCIPAL_COST_USD) " +
|
|
37
|
+
"can fire once more. This reverses the earlier cost_ceiling_without_pricing warning.",
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=unpriced-ceiling-verdict.js.map
|
package/dist/budget.d.ts
CHANGED
|
@@ -14,7 +14,12 @@ import { type PromptManifestTracker } from "./observability/prompt-manifest.js";
|
|
|
14
14
|
/** ③ Per-model pricing keyed by model id (core falls back to each Model.cost when a model is absent).
|
|
15
15
|
* B-089:`m.cost` 运行期可缺席(未定价的模型不铸这个键 —— `config.ts` 的 `buildModelCostKey`),于是
|
|
16
16
|
* 未定价模型在本表里**没有行**;core 的 `isModelPriced` 两边都读(`pricing[id]` 或 `model.cost`),
|
|
17
|
-
* 两边同时缺席 = 这只模型未定价 ⇒ 成本读面整键缺席,而不是编造的 0。
|
|
17
|
+
* 两边同时缺席 = 这只模型未定价 ⇒ 成本读面整键缺席,而不是编造的 0。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 S-231 —— 建行判据就是 core 那只谓词本身,不再同形手抄。第二参传 `undefined` 是**语义**不是省事:
|
|
20
|
+
* 这张表正是本函数在建,建之前它不存在,于是谓词落在它的另一半「模型自报价了吗」上。表建好之后
|
|
21
|
+
* 「本部署配没配价目表」这一问归 {@link isPricingConfigured}(它数的是行;每一行都只由本谓词放进来,
|
|
22
|
+
* 所以两问不可能漂成两个答案)。 */
|
|
18
23
|
export declare function buildPricing(models: Record<string, Model>): Record<string, ModelPricing>;
|
|
19
24
|
/**
|
|
20
25
|
* 缝合审 M3:`capabilities.pricingConfigured` 的判据 —— 提成函数放在 pricing 自己的文件里,因为
|
|
@@ -34,7 +39,10 @@ export declare function buildPricing(models: Record<string, Model>): Record<stri
|
|
|
34
39
|
* 本键在 wire 契约里的原话(「价目表是否已配」)一致。
|
|
35
40
|
* 三态读回:未定价 ⇒ 空表 ⇒ false(与改前同值);声明免费 ⇒ 有行(全零)⇒ true(**唯一变的一格**);
|
|
36
41
|
* 配了真价 ⇒ true(与改前同值)。
|
|
37
|
-
|
|
42
|
+
*
|
|
43
|
+
* 🔴 S-231 —— 本函数**不是** `isModelPriced` 的第二份实现,它问的是**部署级**的另一问:「这张表里有行吗」。
|
|
44
|
+
* 「某一只模型定价了吗」只有 core 那一只谓词回答,而这张表的每一行都只由它放进来(见 {@link buildPricing}),
|
|
45
|
+
* 所以两问结构上不可能漂成两个答案。 */
|
|
38
46
|
export declare function isPricingConfigured(pricing: Record<string, ModelPricing>): boolean;
|
|
39
47
|
/**
|
|
40
48
|
* Bound the cardinality of a free-form metric label value (BL-11/BL-21). A label fed by untrusted/unbounded
|
package/dist/budget.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { modelCostToPricing } from "@sema-agent/core";
|
|
1
|
+
import { isModelPriced, modelCostToPricing } from "@sema-agent/core";
|
|
2
2
|
import { currentPrincipal } from "./observability/principal-context.js";
|
|
3
3
|
import { isPermissionRuleEventKind, permissionRuleEventWordOf } from "./observability/permission-rule-events.js";
|
|
4
4
|
import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
|
|
5
5
|
export function buildPricing(models) {
|
|
6
6
|
const pricing = {};
|
|
7
7
|
for (const m of Object.values(models))
|
|
8
|
-
if (m
|
|
8
|
+
if (isModelPriced(m, undefined))
|
|
9
9
|
pricing[m.id] = modelCostToPricing(m.cost);
|
|
10
10
|
return pricing;
|
|
11
11
|
}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref]④ defer face — EXPERIMENTAL long-tail per-tool defer (default OFF, `TOOL_DEFER_LONGTAIL=true`).
|
|
3
3
|
*
|
|
4
|
-
* Mechanics
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Mechanics (core 7.15.0 `core/runner/tool-defer-gate.ts`, 亲读装树 dist —— 规则从八条收成两条):
|
|
5
|
+
* - 候选**只可能**是 caller spec 工具(scenario/user tools)、远端协议铸名(MCP / A2A)与内置自声明
|
|
6
|
+
* defer 的那对;core 一方名册(Bash/Read/Edit/Grep/Glob/Write/…)结构上永不进候选,所以 CC「12 只
|
|
7
|
+
* 一方工具内联」的姿态**零 server 动作**即成立;
|
|
8
8
|
* ⚠️ **这句在中间那些世代上一度不成立**([ref] 件⑤ 对表):`deferMode:"auto"` 的压力阀曾把内建 fs 族
|
|
9
9
|
* 也卷进候选,小窗模型上首用即激活错;core 5.40.0([ref] a 案,判据帖 [ref] 翻面清单 #3)把阀候选
|
|
10
10
|
* 收回 caller specs,**内建恒 inline**,于是这句话重新为真。**对本仓零行为影响**——理由是下一条:
|
|
11
11
|
* 我们从不开 auto。这一段留着是因为它是那次漂移的出处(数字对了而理由没对,是最难查的一类)。
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* - 🔴 **候选 ≠ 延迟**(core 7.15.0 [ref] 的行为面收窄,本仓两条真后果见下):候选集整体受**一道**
|
|
13
|
+
* 阈值门,全或无 —— 候选的内联字节(name+description+schema)≥ `floor(上下文 token × 10%) × 3`
|
|
14
|
+
* 才延迟,否则**全内联且不挂 `ToolSearch`**。远端协议铸名因此**不再无条件占位**:本仓缺省窗
|
|
15
|
+
* (`MODEL_CONTEXT_WINDOW=262144`)下阈值 = 78 642 字符,一个中小 MCP 名册够不着 ⇒ 首请求带全 schema。
|
|
16
|
+
* 唯一不受门的是调用方的 `TaskSpec.deferTools` 字面名单(按字面恒延迟、不计量)。
|
|
17
|
+
* - `RunnerDeps.deferMode:"auto"` 不是第二道门,而是把候选集**拓宽**到全部 caller spec 后走同一道门 ——
|
|
18
|
+
* 在本仓缺省场景下那会连 Agent 与 Task 四件一起卷进候选(与 CC 姿态相反),所以我们**刻意不开**;
|
|
19
|
+
* 🔴 该判断在 core 5.40.0 / 7.15.0 上**逐字仍成立**(亲验):两次收窄动的都是「候选集是谁」,而
|
|
20
|
+
* Agent / Task 四件是 **caller spec 工具**,收窄后照样在候选池里 —— 不开 auto 的理由没有被消解。
|
|
19
21
|
* 我方**从不接线**这个座:全树零 `RunnerDeps.deferMode` 赋值(`deferModelPlane` 是 config-center 的
|
|
20
22
|
* 同形近名,与本轴无关)。机器钉见 test/tool-defer.test.ts 的 [ref] 件⑤ 格 —— 这条「零接线」正是本仓
|
|
21
23
|
* 在两个 core 世代之间行为逐字不变的**全部**依据,所以它值一根钉而不只是一句注。
|
|
22
|
-
* - per-tool `ToolSpec.defer:true`
|
|
24
|
+
* - per-tool `ToolSpec.defer:true` 是本 helper 施加的那一位。**7.15.0 起它是一次「进候选集」的声明,
|
|
25
|
+
* 不再是「一定占位」的保证** —— 长尾集单独一只(下面实测 ≈842 字符)远在阈值之下,于是这根旋钮在
|
|
26
|
+
* 没有 MCP/A2A 名册的部署上是个 no-op;有名册时它把自己的字节并进同一张账,帮助(或不帮助)开门。
|
|
27
|
+
* ⚠️ 不要为了「恢复旧效果」把它改挂 `TaskSpec.deferTools`:那是调用方的轴,由下游改写上游刚裁定的
|
|
28
|
+
* 语义,属于跨仓纪律明禁的旁路;要改口径请回 core 提。
|
|
23
29
|
*
|
|
24
30
|
* Measured inline surface (chars = description + JSON schema; ÷4 ≈ tokens), default scenario @ core 1.289:
|
|
25
31
|
* Agent 5400 · TaskUpdate 3286 · TaskCreate 2661 · SendUserFile 1282 · TaskList 1075 · SelectEnvironment 842 ·
|
package/dist/config-types.d.ts
CHANGED
|
@@ -1419,12 +1419,15 @@ export interface ServiceConfigFlat {
|
|
|
1419
1419
|
* settings ⇒ 整块跳过;bypassPermissions 臂不回 shellGate;非 host lane wiring 不建 ⇒ 沙箱 lane 全失效)。
|
|
1420
1420
|
* rank 合成:与 autonomy 派生值取大(`ask`→`"always"` 压过 `"classify"`),对 base 只升不降。
|
|
1421
1421
|
* 客户端 settings 折叠在其后、tighten-only,拿不掉这个值;SUP 路由姿态只会把它抬到 `"always"`。
|
|
1422
|
-
* 🔴 **语义随 core 版本演进(2026-07-31
|
|
1423
|
-
*
|
|
1422
|
+
* 🔴 **语义随 core 版本演进(2026-07-31 核;core 7.15.0 再核)**:`"classify"` 下「纯读自动放行」这句
|
|
1423
|
+
* 自 core 2.7.0 起**不再无条件成立** —— [ref] 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
|
|
1424
1424
|
* (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots+additionalReadRoots,
|
|
1425
|
-
* cwd=活 handsCwd),
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1425
|
+
* cwd=活 handsCwd),**读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,
|
|
1426
|
+
* 边界是 core 自算的)。
|
|
1427
|
+
* 🔴 **而自 core 7.15.0([ref])起,那道读边界不再是本旋钮买来的** —— 它**每一档都判**(`off` 含在内),
|
|
1428
|
+
* 本旋钮买的只剩**剩余**风险那一半:`"classify"` = 可证只读且在界内的命令自动放行、其余发问;
|
|
1429
|
+
* `"always"` = 每条命令都发问。⇒ 别再把「越界读会问」写成开这根旋钮的理由(它在不开的部署上同样成立);
|
|
1430
|
+
* 这根旋钮仍然是 tighten-only、仍然无条件施加,语义一字未改。
|
|
1428
1431
|
* Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a
|
|
1429
1432
|
* default — a deployment must explicitly choose it); `"always"` = CC-strict parity (every Bash asks). Any
|
|
1430
1433
|
* other value (incl. `"off"`) is treated as unset — this knob has no "off" value to opt BACK OUT with once a
|
|
@@ -1452,8 +1455,13 @@ export interface ServiceConfigFlat {
|
|
|
1452
1455
|
* ToolSearch-loads the schema). Assessment says the win is marginal for us (spec.tools total ≈4.2k tokens, of
|
|
1453
1456
|
* which Agent + the Task quartet ≈3.2k are CC-core posture = must stay inline; the deferrable tail is
|
|
1454
1457
|
* ≈200-500 tokens/turn) and SendUserFile is deliberately NOT in the defer set (proactive-delivery tool —
|
|
1455
|
-
* hiding it behind ToolSearch suppresses exactly the spontaneous use it exists for).
|
|
1456
|
-
*
|
|
1458
|
+
* hiding it behind ToolSearch suppresses exactly the spontaneous use it exists for). `TOOL_DEFER_LONGTAIL=true`
|
|
1459
|
+
* opts in.
|
|
1460
|
+
* 🔴 **core 7.15.0([ref])起 `defer:true` 只是「进候选集」的声明,不是「一定占位」的保证**:候选集
|
|
1461
|
+
* (caller spec 的 `defer:true` + 远端协议铸名 + 内置自声明 defer)整体受**一道**阈值门,全或无 ——
|
|
1462
|
+
* 内联字节 ≥ 上下文窗的 **10%** 才延迟,否则全内联且不挂 `ToolSearch`。同一条收窄也让「MCP 工具恒被
|
|
1463
|
+
* core 自动延迟」这句(本行旧文案)作废:缺省窗下一个中小 MCP 名册够不着阈值,首请求带全 schema。
|
|
1464
|
+
* 唯一不受门的是调用方的 `TaskSpec.deferTools` 字面名单。口径全文见 src/capabilities/tool-defer.ts 头注。 */
|
|
1457
1465
|
toolDeferLongtail: boolean;
|
|
1458
1466
|
/** LSP code-intelligence tool ([ref] §13.1): mount core's `lsp` tool, routed to an E2B LSP sidecar. Requires
|
|
1459
1467
|
* REMOTE_EXEC=e2b. Default OFF (the on-the-fly language-server install is heavy until an E2B template bakes it).
|
package/dist/config.d.ts
CHANGED
|
@@ -100,20 +100,17 @@ export declare function logConfigDiagnostics(logger: {
|
|
|
100
100
|
debug?: (event: string, fields?: Record<string, unknown>) => void;
|
|
101
101
|
}): void;
|
|
102
102
|
/** B-089:`Model.cost` 的四项(per-1M USD)。类型从 core 的 `Model` 取,不在本仓重列一份。 */
|
|
103
|
-
type ModelCost = Model["cost"]
|
|
103
|
+
type ModelCost = NonNullable<Model["cost"]>;
|
|
104
104
|
/**
|
|
105
105
|
* 🔴 B-089 —— `Model.cost` 这**一个键**的唯一铸点(可展开片段):有价目表 ⇒ 键在;没配 ⇒ **键不出**。
|
|
106
106
|
* 三个铸点(env 主槽 / env cheap 槽 / config-center 车道)共用它,所以「缺席怎么表达」在本仓只有一处答案。
|
|
107
107
|
*
|
|
108
|
-
*
|
|
109
|
-
* (`
|
|
110
|
-
*
|
|
111
|
-
* 只是类型面还没跟上(core 也未把 `isModelPriced` 从 index 导出,本仓无法直接复用那只谓词)。
|
|
112
|
-
* 把这条唯一的类型让步收在这里,调用点一处不必再 cast。
|
|
108
|
+
* `Model.cost` 自 core 7.16.0([ref])起在**类型面**上也是可选的(`dist/engine/llm/types.d.ts` 的
|
|
109
|
+
* `cost?:`),与它一直以来的运行期读法([ref] 的 `isModelPriced`:这个键在不在)对齐 ——
|
|
110
|
+
* 于是本函数不再需要任何类型让步,返回 `{}` 就是「不写这个键」的字面表达。
|
|
113
111
|
*
|
|
114
|
-
* ⚠️
|
|
115
|
-
*
|
|
116
|
-
* 终态是 core 把该键改成可选,届时本函数的 cast 与本段注一起删。
|
|
112
|
+
* ⚠️ 连带纪律仍在:**读** `Model.cost` 一律先判在场(catalog 行的 `c.model.cost?.input`),
|
|
113
|
+
* 别写 `model.cost.input`;而「这只模型定价了吗」这一问不许在本仓重写一遍,调 `isModelPriced`。
|
|
117
114
|
*/
|
|
118
115
|
export declare function buildModelCostKey(cost: ModelCost | undefined): Pick<Model, "cost">;
|
|
119
116
|
/**
|
|
@@ -130,10 +127,29 @@ export declare function buildModelCostKey(cost: ModelCost | undefined): Pick<Mod
|
|
|
130
127
|
* (或天花板只由 center 的 limits 下发)⇒ **漏警**。故本函数只给判据,席位在两只入口各自的
|
|
131
128
|
* **采纳之后**(`main.ts` 的 `createConfigCenterRuntime` 之后 / `run-local.ts` 的 `applyEffective` 之后)。
|
|
132
129
|
*
|
|
133
|
-
* 判据 =
|
|
134
|
-
*
|
|
135
|
-
*
|
|
130
|
+
* 判据 = **目录里没有任何一只带价模型**,问法与 `budget.buildPricing` 的填表判据是**同一只函数**
|
|
131
|
+
* (S-231:core 7.16.0 导出的 `isModelPriced`;第二参传 `undefined` = 这一问发生在注入表建起来之前,
|
|
132
|
+
* 谓词落在「模型自报价了吗」那一半)。本模块刻意不 import `budget.ts`(那边挂着 metrics/fleet 一串
|
|
133
|
+
* 运行期依赖,config 是纯解析层)—— 单源因此不是「谁 import 谁」,而是两边都问引擎那一只。
|
|
136
134
|
*/
|
|
135
|
+
/** 「$ 天花板 × 价目表」的三位事实 —— 告警判据与清警原因的**同一只**读数(S-232 合并复审:清警要说得出为什么清)。 */
|
|
136
|
+
export interface CostCeilingPricingPosture {
|
|
137
|
+
/** 配了的天花板旋钮名(空 = 一根都没配)。 */
|
|
138
|
+
ceilings: string[];
|
|
139
|
+
/** 生效目录里的模型 id(空 = 目录还空着)。 */
|
|
140
|
+
modelIds: string[];
|
|
141
|
+
/** 目录里至少一只模型自报了价(`isModelPriced(m, undefined)`,S-231 单源)。 */
|
|
142
|
+
anyPriced: boolean;
|
|
143
|
+
}
|
|
144
|
+
export declare function costCeilingPricingPosture(models: Record<string, Model>, limits: {
|
|
145
|
+
maxTaskCostUsd: number;
|
|
146
|
+
maxPrincipalCostUsd: number;
|
|
147
|
+
}): CostCeilingPricingPosture;
|
|
148
|
+
/** 「天花板告警此刻**不**成立」的原因(闭集三词);`undefined` = 告警成立。序 = 先判护栏在不在(`no_ceiling`),
|
|
149
|
+
* 再判有没有东西可计价(`empty_catalog`),最后才是「真配回了价」(`priced`)—— 只有最后一词可以说
|
|
150
|
+
* 「护栏能再触发」,前两词下这句话是假的。 */
|
|
151
|
+
export type UnpricedCeilingClearReason = "no_ceiling" | "empty_catalog" | "priced";
|
|
152
|
+
export declare function unpricedCeilingClearReasonOf(p: CostCeilingPricingPosture): UnpricedCeilingClearReason | undefined;
|
|
137
153
|
export declare function buildUnpricedCeilingNotice(models: Record<string, Model>, limits: {
|
|
138
154
|
maxTaskCostUsd: number;
|
|
139
155
|
maxPrincipalCostUsd: number;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
|
|
5
|
+
import { BUILTIN_COMPLIANCE_DENIES, COMPLIANCE_CAPABILITIES, CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES, compileReadDeny, resolveReadDenyBuiltins, CODE_AGENT_PROMPT, formatUserScope, isModelPriced, PROTOCOL_TABLE, protocolOf, RECOMMENDED_SENSITIVE_PATTERNS, RETIRED_TOOL_NAMES, resolveFileHistoryRetention, resolveLockedKeys, validatePermissionRules, WRITE_PROTECTED_DEFAULT_TABLE, resolveWriteProtectedTable, parseRuleText } from "@sema-agent/core";
|
|
6
6
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/settings-schema";
|
|
7
7
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js";
|
|
8
8
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
@@ -284,21 +284,38 @@ function buildCostOrAbsent(read, inherited) {
|
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
286
|
export function buildModelCostKey(cost) {
|
|
287
|
-
return
|
|
287
|
+
return cost === undefined ? {} : { cost };
|
|
288
288
|
}
|
|
289
|
-
export function
|
|
290
|
-
const ceilings = [
|
|
291
|
-
...(limits.maxTaskCostUsd > 0 ? ["MAX_TASK_COST_USD"] : []),
|
|
292
|
-
...(limits.maxPrincipalCostUsd > 0 ? ["MAX_PRINCIPAL_COST_USD"] : []),
|
|
293
|
-
];
|
|
289
|
+
export function costCeilingPricingPosture(models, limits) {
|
|
294
290
|
const entries = Object.values(models);
|
|
295
|
-
|
|
291
|
+
return {
|
|
292
|
+
ceilings: [
|
|
293
|
+
...(limits.maxTaskCostUsd > 0 ? ["MAX_TASK_COST_USD"] : []),
|
|
294
|
+
...(limits.maxPrincipalCostUsd > 0 ? ["MAX_PRINCIPAL_COST_USD"] : []),
|
|
295
|
+
],
|
|
296
|
+
modelIds: [...new Set(entries.map((m) => m.id))],
|
|
297
|
+
anyPriced: entries.some((m) => isModelPriced(m, undefined)),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export function unpricedCeilingClearReasonOf(p) {
|
|
301
|
+
if (p.ceilings.length === 0)
|
|
302
|
+
return "no_ceiling";
|
|
303
|
+
if (p.modelIds.length === 0)
|
|
304
|
+
return "empty_catalog";
|
|
305
|
+
if (p.anyPriced)
|
|
306
|
+
return "priced";
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
export function buildUnpricedCeilingNotice(models, limits) {
|
|
310
|
+
const posture = costCeilingPricingPosture(models, limits);
|
|
311
|
+
if (unpricedCeilingClearReasonOf(posture) !== undefined)
|
|
296
312
|
return undefined;
|
|
313
|
+
const { ceilings, modelIds } = posture;
|
|
297
314
|
return {
|
|
298
315
|
event: "cost_ceiling_without_pricing",
|
|
299
316
|
fields: {
|
|
300
317
|
ceilings: ceilings.join(","),
|
|
301
|
-
models:
|
|
318
|
+
models: modelIds.join(","),
|
|
302
319
|
note: `${ceilings.join(" / ")} is configured but NO model in the effective catalog declares a price table — an ` +
|
|
303
320
|
"unpriced model reports NO cost at all (core RB-368), so these $ ceilings can never fire and this deployment " +
|
|
304
321
|
"has no cost guardrail despite looking like it does. Set MODEL_COST_INPUT / MODEL_COST_OUTPUT / " +
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
|
|
15
15
|
*/
|
|
16
16
|
import { type Autonomy } from "../runtime-governance.js";
|
|
17
|
+
import type { ShellGateDoctrine } from "@sema-agent/core";
|
|
17
18
|
import { type RunRecord } from "../plugins/store-contracts.js";
|
|
18
19
|
import { type PendingRowBinding } from "../plugins/checkpoint-store-sql.js";
|
|
19
20
|
/** [ref] 归因的部署侧入参(两个治理旋钮;判据属主见 `governanceMandatesShellGateAlways`)。 */
|
|
@@ -124,7 +125,7 @@ export declare function resumeEntryForGate(kind: string, ids: {
|
|
|
124
125
|
}): string | null;
|
|
125
126
|
export declare function governanceOriginOf(gate: {
|
|
126
127
|
riskDescriptor?: {
|
|
127
|
-
shellGateDoctrine?:
|
|
128
|
+
shellGateDoctrine?: ShellGateDoctrine;
|
|
128
129
|
};
|
|
129
130
|
} | undefined, governance: GovernancePosture | undefined): true | undefined;
|
|
130
131
|
/** token 泛型:真身是 branded CheckpointToken(秘密能力,只在本函数内部流转,绝不进响应体)。 */
|
|
@@ -183,7 +184,7 @@ interface ConflictProbeDeps<TToken> {
|
|
|
183
184
|
gate?: {
|
|
184
185
|
kind?: string;
|
|
185
186
|
riskDescriptor?: {
|
|
186
|
-
shellGateDoctrine?:
|
|
187
|
+
shellGateDoctrine?: ShellGateDoctrine;
|
|
187
188
|
};
|
|
188
189
|
};
|
|
189
190
|
} | null | undefined>;
|