@sema-agent/server 7.28.0 → 7.29.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/USAGE.md +12 -0
- package/dist/boot/config-center.js +19 -2
- package/dist/boot/runtime-caps.js +5 -2
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-center/facade.js +1 -0
- package/dist/config-center/read-face.d.ts +14 -0
- package/dist/config-center/read-face.js +170 -0
- package/dist/config-center/restart-signal.d.ts +6 -1
- package/dist/config-center/restart-signal.js +13 -1
- package/dist/config-center/types.d.ts +30 -0
- package/dist/http/routes/agents-roster.d.ts +33 -0
- package/dist/http/routes/agents-roster.js +184 -0
- package/dist/http/routes/capabilities.js +39 -0
- package/dist/http/server.js +9 -0
- package/dist/run-local.js +8 -1
- package/dist/runtime-caps-resolver.d.ts +16 -0
- package/dist/runtime-caps-resolver.js +17 -0
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -369,6 +369,17 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
|
|
|
369
369
|
一律启动期点名拒——一个手滑的 deny 集比没有 deny 集更糟(以为挡了其实没挡)。
|
|
370
370
|
- **缺席不铸**:两个键不设就**不传给引擎**——引擎抬默认/扩内建集那天,存量部署自动跟上(与重试上限
|
|
371
371
|
同款「引擎当家」纪律)。
|
|
372
|
+
- **组织下发腿(config-center,7.29 起)**:四键(`READ_FACE` / `READ_DENY_PATTERNS` /
|
|
373
|
+
`READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE`)也可由 config-center 的 `readFace` 域下发
|
|
374
|
+
(`{face?, denyPatterns?, denyBuiltinTiers?, denyBuiltinExclude?}`)。三条规则:
|
|
375
|
+
① **本机 env 显式恒赢**(逐键判)——env 是这台机器的部署主权,下发值不会静默盖掉它,被忽略的键留一条
|
|
376
|
+
`sema_registry_read_face_env_wins` warn;② **坏值整域拒**——任何一个成员过不了引擎自己的校验器就整域不落、
|
|
377
|
+
保留 env 前值 + `sema_registry_read_face_invalid` warn(半应用一份 READ 姿态比不应用更危险);
|
|
378
|
+
③ **restart-to-apply**——四键在 boot 期被引擎装配捕获,下发改动经 `/health` 的 `restart.reasons`
|
|
379
|
+
带 `read-face` 通告,由编排器重启兑现。当前生效档可从 `GET /v1/capabilities` 的 `readFace` 位读
|
|
380
|
+
(`"open"`/`"roots"`/`null`=本部署未钉,引擎默认当家;deny 表内容属策略内容,**不上**能力面)。
|
|
381
|
+
⚠️ 本地 `config.d/` 车道今天**接不上**这一域(registry-core 的可移植域表里没有它),只有
|
|
382
|
+
`CONFIG_PROVIDER=remote` 的部署走得通。
|
|
372
383
|
|
|
373
384
|
**可选 — MCP 入站表单(elicitation,E23):默认关**
|
|
374
385
|
|
|
@@ -623,6 +634,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
|
|
|
623
634
|
| `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验) |
|
|
624
635
|
| `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的 |
|
|
625
636
|
| `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
|
|
637
|
+
| `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方钉死自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
|
|
626
638
|
| `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
|
|
627
639
|
| `GET /health` | 健康(无需鉴权) |
|
|
628
640
|
| `GET /metrics` | Prometheus 指标(有 token 时需带) |
|
|
@@ -46,7 +46,7 @@ import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../confi
|
|
|
46
46
|
import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../config-provider.js";
|
|
47
47
|
import { createKeyResolver } from "../key-resolver.js";
|
|
48
48
|
import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
|
|
49
|
-
import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
|
|
49
|
+
import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
|
|
50
50
|
import { SHA256_HEX_RE } from "../digest-form.js";
|
|
51
51
|
export async function createConfigCenterRuntime(ctx) {
|
|
52
52
|
const { config, logger, metrics, localRoot } = ctx;
|
|
@@ -484,6 +484,19 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
+
// #A4([4147] 件1):READ 容纳面四键的 center 腿 —— **boot 期唯一装点**,两条 boot 路径(live 拉取与
|
|
488
|
+
// LKG 回落)在此汇合(`effective` 只在真应用的那两支被置;dry-run 只 diff 不 apply ⇒ 恒缺席 ⇒ 不落配,
|
|
489
|
+
// 与 observe-only 语义一致)。
|
|
490
|
+
// ⚠️ **位置即契约**:必须留在 `createConfigCenterRuntime` 的 boot 段里 —— 这四键在 main.ts 的
|
|
491
|
+
// `createRunnerDeps` 里被**取值捕获**(boot/runner-deps.ts 的四座),而那一步排在本函数 await 之后;
|
|
492
|
+
// 挪到 `applyCenterCapabilities`(skills/mcp 那条线,main.ts 后段)就晚了:配置改了、runner 已经拿着旧值,
|
|
493
|
+
// 是「半接=静默死特性」的教科书形。refresh 路径刻意不热应用(restart-to-apply,`read-face` 片通告)。
|
|
494
|
+
// 返回值 = 本机 env 显式占住的 readFace 键(部署事实,与本次发布内容无关)。喂给 `restartCtx()`,
|
|
495
|
+
// `read-face` 片于是只指纹**真会 boot-bake 的那部分** —— 否则 center 改一个本机永远用不上的键,
|
|
496
|
+
// 整片 fleet 会为一次对自己毫无影响的发布滚动重启一遍(codex 对抗复审 finding 2)。
|
|
497
|
+
// ⚠️ 必须在 apply **之前/之中**取:apply 之后 config 上的键分不清是 env 写的还是 center 刚落的。
|
|
498
|
+
// 域缺席(今天绝大多数部署)时函数只算这一件事就返回,零副作用。
|
|
499
|
+
const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined);
|
|
487
500
|
if (!modelReadyState.ready)
|
|
488
501
|
logger.warn("model_roster_pending", {
|
|
489
502
|
note: "CONFIG_REQUIRE_ROSTER=true and no center roster has landed yet — the env MODEL_ID is treated as a PLACEHOLDER, so billable submissions 503 (and /health reports ready:false) until the first effective pull lands ≥1 enabled model. Unset the knob if this worker's env model is authoritative.",
|
|
@@ -497,7 +510,11 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
497
510
|
// (MODEL_DEGRADE_TO / MODEL_DEGRADE_REACTIVE)开关,而它焊死的是 CENTER 目录里那条模型的路由面。读成
|
|
498
511
|
// 函数而非常量:`config.degrade` 今天由 env 独占(applyEffective 不写它),但若哪天中心接管这面,这里
|
|
499
512
|
// 自动跟着走,不会退化成 boot 期快照。车道关(默认)⇒ ctx 空 ⇒ 该片恒 null ⇒ 零行为变化。
|
|
500
|
-
const restartCtx = () => (
|
|
513
|
+
const restartCtx = () => ({
|
|
514
|
+
...(config.degrade?.reactive ? { reactiveDegradeTo: config.degrade.to } : {}),
|
|
515
|
+
// #A4:env 占住的 readFace 键(boot 期一次性事实,env 不会在进程内变)。
|
|
516
|
+
...(readFaceEnvHeld.length > 0 ? { readFaceEnvHeld } : {}),
|
|
517
|
+
});
|
|
501
518
|
// #131-2:60s 刷新环的句柄提到 runtime 闭包层——stopRefreshLoop(hardShutdown 收尾链)要能清它。
|
|
502
519
|
let ccTimer;
|
|
503
520
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyObserverEnvOptIn, createPrincipalEntitlementsClient, scopedTokenNeedsWorker } from "../runtime-caps-resolver.js";
|
|
1
|
+
import { applyObserverEnvOptIn, centerEntitlementSourceWired, createPrincipalEntitlementsClient, scopedTokenNeedsWorker } from "../runtime-caps-resolver.js";
|
|
2
2
|
export function createRuntimeCaps(ctx) {
|
|
3
3
|
const { config, logger } = ctx;
|
|
4
4
|
// design/99 §K (core 1.157 `RunnerDeps.runtimeCapsResolver`) — the ENFORCE last-link of the
|
|
@@ -38,7 +38,10 @@ export function createRuntimeCaps(ctx) {
|
|
|
38
38
|
// ONE per-principal caps client serves two faces off the same fetch/cache — `resolveRuntimeCaps`
|
|
39
39
|
// (core's seam, fail-closed) and `scenarioRuling` (resolveSpec's scenario gate, fail-open; center attaches the
|
|
40
40
|
// resolved {scenario, allowlist} to the caps body, single semantic source in center resolve-scenario.ts).
|
|
41
|
-
|
|
41
|
+
// #A5([4147] 件2):判据搬进 `centerEntitlementSourceWired` —— **同一只**谓词也被 `/v1/capabilities` 的
|
|
42
|
+
// `permissionModeAuto.entitlementSource` 披露位消费,于是「装配点认为有 entitlement 源」与「能力面对外
|
|
43
|
+
// 声称有源」结构上不可能分家(两处各写一份条件正是本仓在册的「诊断说亮、位说暗」病族入口)。
|
|
44
|
+
const principalCaps = centerEntitlementSourceWired(config) && config.configCenter
|
|
42
45
|
? createPrincipalEntitlementsClient({
|
|
43
46
|
baseUrl: config.configCenter.baseUrl,
|
|
44
47
|
token: config.configCenter.token,
|
|
@@ -43,5 +43,6 @@ export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCent
|
|
|
43
43
|
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
|
|
44
44
|
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, type RestartSlice, type RestartSliceCtx, type RestartSignal, } from "./restart-signal.js";
|
|
45
45
|
export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
|
|
46
|
-
export
|
|
46
|
+
export { applyCenterReadFace } from "./read-face.js";
|
|
47
|
+
export type { CenterSkillManifest, CenterMcpServer, CenterA2aPeer, CenterReadFace, EffectiveConfig, ExecutionRuling, SessionMirrorRuling, } from "./types.js";
|
|
47
48
|
//# sourceMappingURL=facade.d.ts.map
|
|
@@ -43,4 +43,5 @@ export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCent
|
|
|
43
43
|
export { mutateInPlace, applyEffective, applyRuntimeGates, applyRuntimeHot, resolveDefaultModelName, logEffectiveDiff, runtimeHasActiveGate, } from "./apply-effective.js";
|
|
44
44
|
export { restartReasons, planeHasActiveTiers, modelPlaneChanged, } from "./restart-signal.js";
|
|
45
45
|
export { applyCenterSkills, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./skills-mcp.js";
|
|
46
|
+
export { applyCenterReadFace } from "./read-face.js";
|
|
46
47
|
//# sourceMappingURL=facade.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ServiceConfig } from "../config-types.js";
|
|
2
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
|
+
import type { CenterReadFace, EffectiveConfig } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* 应用 center 的 readFace 域(**boot-only**:调用点在 boot 拉取之后、`createRunnerDeps` 之前)。
|
|
6
|
+
* 缺席 ⇒ 零触碰。规则见文件头注 ①②③。
|
|
7
|
+
*
|
|
8
|
+
* 返回值 = **本机 env 显式占住的 center 键名**(与本次发布内容无关的部署事实)。调用方把它喂给
|
|
9
|
+
* `RestartSliceCtx.readFaceEnvHeld`,`read-face` restart 片于是只指纹「真会 boot-bake 的那部分」——
|
|
10
|
+
* 否则 center 改一个本机永远用不上的键,整片 fleet 会为一次对自己毫无影响的发布滚动重启一遍
|
|
11
|
+
* (codex 对抗复审 finding 2,验真后采纳的那一半)。
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyCenterReadFace(config: ServiceConfig, eff: EffectiveConfig, logger?: Logger): readonly (keyof CenterReadFace)[];
|
|
14
|
+
//# sourceMappingURL=read-face.d.ts.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #A4([4147] 窗三件批 件1;[4073]/[4083] 承诺件)—— READ 容纳面四键的 **config-center 消费腿**。
|
|
3
|
+
*
|
|
4
|
+
* 部署 env 的四键(`READ_FACE` / `READ_DENY_PATTERNS` / `READ_DENY_BUILTIN_TIERS` /
|
|
5
|
+
* `READ_DENY_BUILTIN_EXCLUDE`,见 config.ts)此前是这条轴上的唯一入口:一个用 config-center 管理整片
|
|
6
|
+
* worker 的组织,想统一收紧 READ 面只能逐机改 env。本模块把 `EffectiveConfig.readFace`(见 types.ts
|
|
7
|
+
* {@link CenterReadFace})投影到同样的四个 `ServiceConfig` 键上,**restart-to-apply**(与 mcp/a2a 同线:
|
|
8
|
+
* `createRunnerDeps` 在 boot 期捕获这四键;改动由 `read-face` restart 片通告)。
|
|
9
|
+
*
|
|
10
|
+
* ── 三条规则,每条都有失败方向的论证 ──────────────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* ① **env 显式恒赢,逐键判**(operator-knob 律)。env 是**这台机器**的部署主权表达,center 是组织下发;
|
|
13
|
+
* 冲突时保留 env 并留一条点名 warn。为什么不照 6 闸的「center 覆盖 env」:那六闸是**限额/治理**面
|
|
14
|
+
* (组织比单机更知道该收多紧),而 READ 容纳面描述的是**这台机器的磁盘上有什么** —— 运维在这台机器上
|
|
15
|
+
* 写下 `READ_FACE=roots` 时知道的事情,组织的下发面不知道。静默盖掉它是本仓「部署级旋钮」纪律的反面。
|
|
16
|
+
* 逐键而非整域:env 只写了 face 时,center 的三个 deny 键照常落(否则一个 env 键会把整片组织策略挡掉)。
|
|
17
|
+
* 判据 = **键在场**(`!== undefined`):`readDenyBuiltinTiers: []` 是 env `none` 的显式空姿态,
|
|
18
|
+
* 不是「没设过」—— 用真值判会把「一档内建都不要」这个显式表态判成缺席。
|
|
19
|
+
*
|
|
20
|
+
* ② **坏值整域拒 + 响亮**(#157 安全轴)。任何一个成员过不了引擎自己的校验器 ⇒ 整个 readFace 域不落,
|
|
21
|
+
* 保留 env 前值 + warn 点名。两个方向都要:半应用一份 READ 姿态(收紧了一半)会让运维以为策略生效;
|
|
22
|
+
* 而 throw 会让一次坏发布把常驻进程炸掉(env 腿 throw 是对的 —— 那是启动期、由本机运维自己写的值)。
|
|
23
|
+
* 与 `stageApprovalRequire` 的域级拒绝先例同形。
|
|
24
|
+
*
|
|
25
|
+
* ③ **校验一律交给引擎**(`compileReadDeny` / `resolveReadDenyBuiltins`)。宿主不自造第二份判定语义,
|
|
26
|
+
* 与 env 腿逐字同款;且校验跑在**合成后的最终姿态**上(env 键 + center 键混合形),否则
|
|
27
|
+
* 「env 选了档位、center 删了一个不在该档里的行名」这类跨腿坏组合会漏过。
|
|
28
|
+
*/
|
|
29
|
+
import { compileReadDeny, resolveReadDenyBuiltins } from "@sema-agent/core";
|
|
30
|
+
/** center 域成员 → `ServiceConfig` 键的对照表(单一真源:warn 文案、env-wins 判定、落配三处同吃)。 */
|
|
31
|
+
const KEY_MAP = [
|
|
32
|
+
["face", "readFace"],
|
|
33
|
+
["denyPatterns", "readDenyPatterns"],
|
|
34
|
+
["denyBuiltinTiers", "readDenyBuiltinTiers"],
|
|
35
|
+
["denyBuiltinExclude", "readDenyBuiltinExclude"],
|
|
36
|
+
];
|
|
37
|
+
class ReadFaceDomainError extends Error {
|
|
38
|
+
}
|
|
39
|
+
/** `denyPatterns` 的归一:裸串简写 ≡ `{pattern, caseSensitive:false}`(core `ReadDenyEntry` 同宽,
|
|
40
|
+
* 与 env 腿 `parseReadDenyPatternsEnv` 逐字同词表)。形坏一律抛 ⇒ 整域拒(绝不静默丢元素:丢光会
|
|
41
|
+
* 塌成空表,而空表的语义恰好是「没有追加保护」)。 */
|
|
42
|
+
function normalizeDenyPatterns(raw) {
|
|
43
|
+
if (!Array.isArray(raw))
|
|
44
|
+
throw new ReadFaceDomainError("readFace.denyPatterns must be an array of \"pattern\" strings or {pattern, caseSensitive?} objects");
|
|
45
|
+
return raw.map((it, i) => {
|
|
46
|
+
if (typeof it === "string")
|
|
47
|
+
return { pattern: it, caseSensitive: false };
|
|
48
|
+
if (typeof it !== "object" || it === null || typeof it.pattern !== "string") {
|
|
49
|
+
throw new ReadFaceDomainError(`readFace.denyPatterns[${i}] must be a string or an object with a string 'pattern'`);
|
|
50
|
+
}
|
|
51
|
+
const cs = it.caseSensitive;
|
|
52
|
+
if (cs !== undefined && typeof cs !== "boolean")
|
|
53
|
+
throw new ReadFaceDomainError(`readFace.denyPatterns[${i}].caseSensitive must be boolean when present`);
|
|
54
|
+
return { pattern: it.pattern, caseSensitive: cs === true };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** `face` 的词表校验(闭集两词;坏词抛 ⇒ 整域拒)。 */
|
|
58
|
+
function assertFace(raw) {
|
|
59
|
+
if (raw !== "open" && raw !== "roots")
|
|
60
|
+
throw new ReadFaceDomainError(`readFace.face must be "open" or "roots" — got ${JSON.stringify(raw)}`);
|
|
61
|
+
return raw;
|
|
62
|
+
}
|
|
63
|
+
/** `resolveReadDenyBuiltins` 的入参拼装(两处调用同源:发布件自校验 + 合成后终校验)。 */
|
|
64
|
+
function builtinsArg(tiers, exclude) {
|
|
65
|
+
return {
|
|
66
|
+
...(tiers !== undefined ? { tiers } : {}),
|
|
67
|
+
...(exclude !== undefined ? { exclude } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** 名字表(档位 / 行名)的形校验:必须是非空串数组;`[]` 合法(显式空姿态)。 */
|
|
71
|
+
function normalizeNameList(field, raw) {
|
|
72
|
+
if (!Array.isArray(raw))
|
|
73
|
+
throw new ReadFaceDomainError(`readFace.${field} must be an array of names`);
|
|
74
|
+
return raw.map((n, i) => {
|
|
75
|
+
if (typeof n !== "string" || n.trim().length === 0)
|
|
76
|
+
throw new ReadFaceDomainError(`readFace.${field}[${i}] must be a non-empty name string`);
|
|
77
|
+
return n.trim();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 应用 center 的 readFace 域(**boot-only**:调用点在 boot 拉取之后、`createRunnerDeps` 之前)。
|
|
82
|
+
* 缺席 ⇒ 零触碰。规则见文件头注 ①②③。
|
|
83
|
+
*
|
|
84
|
+
* 返回值 = **本机 env 显式占住的 center 键名**(与本次发布内容无关的部署事实)。调用方把它喂给
|
|
85
|
+
* `RestartSliceCtx.readFaceEnvHeld`,`read-face` restart 片于是只指纹「真会 boot-bake 的那部分」——
|
|
86
|
+
* 否则 center 改一个本机永远用不上的键,整片 fleet 会为一次对自己毫无影响的发布滚动重启一遍
|
|
87
|
+
* (codex 对抗复审 finding 2,验真后采纳的那一半)。
|
|
88
|
+
*/
|
|
89
|
+
export function applyCenterReadFace(config, eff, logger) {
|
|
90
|
+
// 部署事实先算(与发布件在不在场无关):env 占住哪几个键,是这台机器的属性。
|
|
91
|
+
const envHeldKeys = KEY_MAP.filter(([, cfgKey]) => config[cfgKey] !== undefined).map(([centerKey]) => centerKey);
|
|
92
|
+
const domain = eff.readFace;
|
|
93
|
+
if (domain === undefined || domain === null)
|
|
94
|
+
return envHeldKeys;
|
|
95
|
+
if (typeof domain !== "object" || Array.isArray(domain)) {
|
|
96
|
+
logger?.warn("sema_registry_read_face_invalid", {
|
|
97
|
+
detail: `published readFace domain must be an object with optional {face,denyPatterns,denyBuiltinTiers,denyBuiltinExclude} — got ${Array.isArray(domain) ? "array" : typeof domain}`,
|
|
98
|
+
kept: "the deployment's env READ-face posture is unchanged (refusing a malformed domain WHOLE — a half-applied read-containment posture is worse than none)",
|
|
99
|
+
});
|
|
100
|
+
return envHeldKeys;
|
|
101
|
+
}
|
|
102
|
+
const d = domain;
|
|
103
|
+
// ── ① env 恒赢(逐键):env 写过的键,center 的同名成员只留声、不落配 ────────────────────────
|
|
104
|
+
const envHeld = KEY_MAP.filter(([centerKey, cfgKey]) => d[centerKey] !== undefined && config[cfgKey] !== undefined).map(([centerKey, cfgKey]) => `${cfgKey} (readFace.${centerKey})`);
|
|
105
|
+
// ── ②③ stage ────────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
let staged;
|
|
107
|
+
try {
|
|
108
|
+
// ② **全域预校验先行,不看 env 是否覆盖**(codex 对抗复审 finding 1,验真属实后重排的次序)。
|
|
109
|
+
// 旧序把校验挂在「取 center 值」之后,而被 env 占住的键根本不取值 ⇒ 那个成员**完全不过校验**:
|
|
110
|
+
// 「env 钉死 face × center 把 face 写错一个字母 + 同域发一个放宽的 denyBuiltinTiers:[]」这一形下,
|
|
111
|
+
// 非法成员静默无声、放宽成员照落 —— 本文件承诺的「任一坏值整域拒」被证否,且偏离方向是 fail-open。
|
|
112
|
+
// 现在:域里**每一个在场成员**都先过形/词表/引擎校验(且校验的是**这份发布自己的**姿态),
|
|
113
|
+
// 任一不合法 ⇒ 抛 ⇒ 整域拒;合法之后才做 env-wins 逐键合成。
|
|
114
|
+
const normalized = {
|
|
115
|
+
...(d.face !== undefined ? { readFace: assertFace(d.face) } : {}),
|
|
116
|
+
...(d.denyPatterns !== undefined ? { readDenyPatterns: normalizeDenyPatterns(d.denyPatterns) } : {}),
|
|
117
|
+
...(d.denyBuiltinTiers !== undefined ? { readDenyBuiltinTiers: normalizeNameList("denyBuiltinTiers", d.denyBuiltinTiers) } : {}),
|
|
118
|
+
...(d.denyBuiltinExclude !== undefined ? { readDenyBuiltinExclude: normalizeNameList("denyBuiltinExclude", d.denyBuiltinExclude) } : {}),
|
|
119
|
+
};
|
|
120
|
+
// 发布件自身的引擎校验(与 env 覆盖无关 —— 一个坏发布就是坏发布,不因本机恰好用不上它而变合法)。
|
|
121
|
+
if (normalized.readDenyPatterns !== undefined)
|
|
122
|
+
compileReadDeny(normalized.readDenyPatterns);
|
|
123
|
+
if (normalized.readDenyBuiltinTiers !== undefined || normalized.readDenyBuiltinExclude !== undefined) {
|
|
124
|
+
resolveReadDenyBuiltins(builtinsArg(normalized.readDenyBuiltinTiers, normalized.readDenyBuiltinExclude));
|
|
125
|
+
}
|
|
126
|
+
// ③ env-wins 逐键合成:只保留**没被 env 占住**的键。
|
|
127
|
+
staged = Object.fromEntries(KEY_MAP.filter(([, cfgKey]) => config[cfgKey] === undefined).map(([, cfgKey]) => [cfgKey, normalized[cfgKey]]).filter(([, v]) => v !== undefined));
|
|
128
|
+
// 引擎规则再验一次,这次跑在**最终合成姿态**上(env 键 + 本次 center 键):跨腿坏组合
|
|
129
|
+
// (env 选档位 × center 删一个不在该档里的行名)只有在合成后才看得见。
|
|
130
|
+
const finalPatterns = staged.readDenyPatterns ?? config.readDenyPatterns;
|
|
131
|
+
const finalTiers = staged.readDenyBuiltinTiers ?? config.readDenyBuiltinTiers;
|
|
132
|
+
const finalExclude = staged.readDenyBuiltinExclude ?? config.readDenyBuiltinExclude;
|
|
133
|
+
if (finalPatterns !== undefined)
|
|
134
|
+
compileReadDeny(finalPatterns);
|
|
135
|
+
if (finalTiers !== undefined || finalExclude !== undefined)
|
|
136
|
+
resolveReadDenyBuiltins(builtinsArg(finalTiers, finalExclude));
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
logger?.warn("sema_registry_read_face_invalid", {
|
|
140
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
141
|
+
kept: "the deployment's env READ-face posture is unchanged (refusing the readFace domain WHOLE — applying half of a read-containment posture would read as 'policy landed' while part of it silently did not)",
|
|
142
|
+
});
|
|
143
|
+
return envHeldKeys;
|
|
144
|
+
}
|
|
145
|
+
// ── commit:纯赋值 ────────────────────────────────────────────────────────────────────────────
|
|
146
|
+
if (staged.readFace !== undefined)
|
|
147
|
+
config.readFace = staged.readFace;
|
|
148
|
+
if (staged.readDenyPatterns !== undefined)
|
|
149
|
+
config.readDenyPatterns = staged.readDenyPatterns;
|
|
150
|
+
if (staged.readDenyBuiltinTiers !== undefined)
|
|
151
|
+
config.readDenyBuiltinTiers = staged.readDenyBuiltinTiers;
|
|
152
|
+
if (staged.readDenyBuiltinExclude !== undefined)
|
|
153
|
+
config.readDenyBuiltinExclude = staged.readDenyBuiltinExclude;
|
|
154
|
+
if (envHeld.length > 0) {
|
|
155
|
+
logger?.warn("sema_registry_read_face_env_wins", {
|
|
156
|
+
keys: envHeld,
|
|
157
|
+
note: "this machine's explicit env READ-face keys WIN over the published readFace domain (deployment sovereignty) — the published values for these keys were NOT applied; unset the env key if the org publication should own it",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (Object.keys(staged).length > 0) {
|
|
161
|
+
logger?.info("sema_registry_read_face", {
|
|
162
|
+
// 落的是**哪些键**,不是 deny 表的内容(策略内容不进日志面,与能力位的最小披露同判)。
|
|
163
|
+
applied: Object.keys(staged),
|
|
164
|
+
...(staged.readFace !== undefined ? { face: staged.readFace } : {}),
|
|
165
|
+
note: "restart-to-apply — these keys are captured when the runner deps are assembled at boot",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return envHeldKeys;
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=read-face.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EffectiveConfig } from "./types.js";
|
|
2
|
-
export type RestartSlice = "skills" | "mcp" | "a2a" | "scenarios" | "runtime-gates" | "models-tiers" | "degrade-route";
|
|
2
|
+
export type RestartSlice = "skills" | "mcp" | "a2a" | "scenarios" | "runtime-gates" | "models-tiers" | "degrade-route" | "read-face";
|
|
3
3
|
/** What the CALLER must contribute about boot-baked catalog consumers that live OUTSIDE the Runner — the
|
|
4
4
|
* slices cannot see them, because they are configured from env (`ServiceConfig`), not from the center's
|
|
5
5
|
* EffectiveConfig. Absent/empty = that lane is off ⇒ its slice is inert (null on both sides). */
|
|
@@ -7,6 +7,11 @@ export interface RestartSliceCtx {
|
|
|
7
7
|
/** `config.degrade.to` when REACTIVE degrade is on (env `MODEL_DEGRADE_TO` + `MODEL_DEGRADE_REACTIVE`);
|
|
8
8
|
* undefined = the lane is off ⇒ no degrading brain was composed ⇒ nothing is boot-frozen. */
|
|
9
9
|
reactiveDegradeTo?: string;
|
|
10
|
+
/** #A4(codex 对抗复审 finding 2,验真后采纳):本机 env **显式占住**的 readFace 键
|
|
11
|
+
* (`applyCenterReadFace` 的返回值,由 boot 存下)。这些键的 center 值在本机**永不 boot-bake**,
|
|
12
|
+
* 所以它们的变化不该产生 restart 理由 —— 否则 center 改一个本机用不上的键,整片 fleet 会为一次
|
|
13
|
+
* 对自己毫无影响的发布滚动重启一遍。缺席 = 没有 env 覆盖 ⇒ 整域都算数(与旧行为一致)。 */
|
|
14
|
+
readFaceEnvHeld?: readonly ("face" | "denyPatterns" | "denyBuiltinTiers" | "denyBuiltinExclude")[];
|
|
10
15
|
}
|
|
11
16
|
/** Structured restart signal an orchestrator consumes (GET /health → `restart`). Change-detected against the
|
|
12
17
|
* process's BOOT config, NOT presence — so it stays absent when nothing baked-at-boot changed (presence would
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { resolveActiveTiers } from "@sema-agent/registry-core";
|
|
9
9
|
import { RUNTIME_GATE_KEYS, runtimeGatePresent, resolveDefaultModelName } from "./apply-effective.js";
|
|
10
|
-
const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "runtime-gates", "models-tiers", "degrade-route"];
|
|
10
|
+
const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "runtime-gates", "models-tiers", "degrade-route", "read-face"];
|
|
11
11
|
/** Canonical, key-sorted JSON (array order preserved) so two semantically-equal effective slices fingerprint
|
|
12
12
|
* identically regardless of object key order from the center serializer. */
|
|
13
13
|
function stableStringify(v) {
|
|
@@ -106,6 +106,18 @@ function restartSliceValue(eff, slice, ctx) {
|
|
|
106
106
|
// Entries reduced by the shared deny-list (cost/quotaWeight/enabled are hot — see catalogEntryFingerprint).
|
|
107
107
|
return { models: planeEntriesFingerprint(eff.models?.models), tiers: active, default: def.name };
|
|
108
108
|
}
|
|
109
|
+
case "read-face": {
|
|
110
|
+
// 指纹的是**真会 boot-bake 的那部分**:域里被本机 env 占住的成员剔除(codex finding 2 —— 旧实现
|
|
111
|
+
// 指纹原始域,于是 center 改一个本机永远用不上的键也会让整片 fleet 滚动重启一遍,而重启后姿态
|
|
112
|
+
// 逐字不变)。域缺席、或全部成员都被 env 占住 ⇒ null:两者对**本机**是同一件事(这条腿不存在),
|
|
113
|
+
// 于是「组织开始/停止发布 READ 面」在真会生效的那些键上仍然可检测。
|
|
114
|
+
const d = eff.readFace;
|
|
115
|
+
if (!d)
|
|
116
|
+
return null;
|
|
117
|
+
const held = new Set(ctx?.readFaceEnvHeld ?? []);
|
|
118
|
+
const view = Object.entries(d).filter(([k, v]) => !held.has(k) && v !== undefined);
|
|
119
|
+
return view.length > 0 ? Object.fromEntries(view) : null;
|
|
120
|
+
}
|
|
109
121
|
case "degrade-route": {
|
|
110
122
|
const to = ctx?.reactiveDegradeTo;
|
|
111
123
|
if (to === undefined)
|
|
@@ -146,6 +146,32 @@ export interface CenterA2aPeer {
|
|
|
146
146
|
/** Optional allowlist of the peer's skill ids to mount (others are not mounted). */
|
|
147
147
|
allowSkills?: string[];
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* #A4([4147] 窗三件批 件1;[4073]/[4083] 承诺件)—— READ 容纳面的 center 下发形(`CenterA2aPeer` 的
|
|
151
|
+
* 同族:一个 additive 顶层域,server 声明它才不会被静默丢弃)。四个成员逐一对应部署 env 的四键
|
|
152
|
+
* (`READ_FACE` / `READ_DENY_PATTERNS` / `READ_DENY_BUILTIN_TIERS` / `READ_DENY_BUILTIN_EXCLUDE`),
|
|
153
|
+
* 消费半场 = `config-center/read-face.ts` `applyCenterReadFace`。
|
|
154
|
+
*
|
|
155
|
+
* 🔴 **env 显式恒赢**(operator-knob 律,消费侧执法):env 是这台机器的部署主权,center 是组织下发;
|
|
156
|
+
* 冲突时本机 env 原样保留 + 一条点名 warn。所以本类型描述的是「组织想要什么」,不是「这台机器最终是什么」。
|
|
157
|
+
*
|
|
158
|
+
* 🔴 **只有收敛面,没有旁路面**:`denyPatterns` 的元素与 core `ReadDenyEntry` 同宽(裸串简写 ≡
|
|
159
|
+
* `{pattern, caseSensitive:false}`),校验一律交给引擎自己的 `compileReadDeny` /
|
|
160
|
+
* `resolveReadDenyBuiltins` —— 宿主不自造第二份判定语义(env 腿逐字同款纪律)。
|
|
161
|
+
*/
|
|
162
|
+
export interface CenterReadFace {
|
|
163
|
+
/** READ 工具面的容纳姿态。缺席=本域不管这一键(env / 引擎默认接管)。 */
|
|
164
|
+
face?: "open" | "roots";
|
|
165
|
+
/** 部署级 READ deny **追加**表(additive-only,core 内建表永不可缩)。 */
|
|
166
|
+
denyPatterns?: Array<string | {
|
|
167
|
+
pattern: string;
|
|
168
|
+
caseSensitive?: boolean;
|
|
169
|
+
}>;
|
|
170
|
+
/** **内建** deny 表的档位选择(闭集四档);`[]` = 显式空姿态(一档内建都不要)。 */
|
|
171
|
+
denyBuiltinTiers?: string[];
|
|
172
|
+
/** 按行名删内建条目,在档位选择**之后**生效。 */
|
|
173
|
+
denyBuiltinExclude?: string[];
|
|
174
|
+
}
|
|
149
175
|
export interface EffectiveConfig {
|
|
150
176
|
version: number;
|
|
151
177
|
updatedAt: string;
|
|
@@ -236,6 +262,10 @@ export interface EffectiveConfig {
|
|
|
236
262
|
plugins?: {
|
|
237
263
|
plugins?: unknown[];
|
|
238
264
|
};
|
|
265
|
+
/** #A4:READ 容纳面的组织下发域(见 {@link CenterReadFace})。缺席(今天每个 center 都发的形)⇒
|
|
266
|
+
* 这条腿对本部署不存在,四键仍由 env / 引擎默认独占。restart-to-apply(boot 期被 `createRunnerDeps`
|
|
267
|
+
* 捕获),改动经 `read-face` restart 片通告。 */
|
|
268
|
+
readFace?: CenterReadFace;
|
|
239
269
|
/** prompts 域(additive key;双轴 wire=registry-core `EffectivePromptsWire` 或 legacy 平铺形——
|
|
240
270
|
* server 侧判形/采用全在 capabilities/center-prompts.ts `validatePromptsDomain`,本 interface 只
|
|
241
271
|
* 声明键位存在(unknown:候选门是唯一解释者,这里不复刻形)。 */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #A1([4147] 窗三件批 件3;C-R13 链的 server 前置)—— 后台 agent **名册读面**
|
|
3
|
+
* `GET /v1/agents/roster`。
|
|
4
|
+
*
|
|
5
|
+
* ## 读的是哪个店(两个「名册」概念的定界)
|
|
6
|
+
* 树上有两个:core 的 `RosterStore`(name→agentId 绑定,agent-teams 的按名解析)与
|
|
7
|
+
* `BackgroundAgentStore`(durable 后台子代行)。壳要渲的是「我有哪些后台 agent、各自什么状态」——
|
|
8
|
+
* `RosterEntry` 身上**没有** status / spawnedAt / settledAt 这些轴,答不了;而
|
|
9
|
+
* `BackgroundAgentRowSummary`(store 的 content-free 列投影)恰好就是它。故本面读后者的两只既有只读口
|
|
10
|
+
* (`listByScope` / `listBySession`),**零 SQL schema 面**。
|
|
11
|
+
*
|
|
12
|
+
* ## 路径为什么是顶层 `/v1/agents/roster`
|
|
13
|
+
* 名册的价值正在**跨会话**(agent-teams 的按名寻址要能越过单次 run);挂进 `/v1/sessions/:id/…` 会把
|
|
14
|
+
* 跨会话形从 wire 上抹掉,只剩「这一次会话派了谁」。会话维不是没有,是降为**可选窄化** `?session=`
|
|
15
|
+
* (与 `/v1/fleet/stream` 的 `?session=` 同姿势)。
|
|
16
|
+
*
|
|
17
|
+
* ## 三条边界(逐条对着失败形写)
|
|
18
|
+
* · **principal 门 + 反枚举**:身份判在店可用性判**之前**(否则「这台有没有装店」会先于 401 泄出去,
|
|
19
|
+
* 与 `GET /v1/sessions` 的同款注释);principal 调用方**钉死自己的 scope**(`?scope=` 对它无效),
|
|
20
|
+
* 于是别的租户的行不是「403」也不是「空 200 但 total 多 1」——它在这个名册里**根本不存在**,
|
|
21
|
+
* 没有任何可判别面(`total` 同样是属主口径:一个多出来的计数就是存在性 oracle)。
|
|
22
|
+
* · **投影白名单**(照 `routes/fleet.ts` `stripT` 的姿势):逐键**显式拼**,并用
|
|
23
|
+
* `satisfies Record<keyof BackgroundAgentRowSummary, …>` 的处置表把「行上新增键」变成**编译错误** ——
|
|
24
|
+
* 黑名单形在 fleet 那边被击穿过两次(currentTool / editedFiles),同一条信任边界不再靠人记全。
|
|
25
|
+
* · **能力位同源**:`capabilities.agentRoster` 与这里的 501 谓词是同一个 `deps.backgroundAgentStore`。
|
|
26
|
+
*/
|
|
27
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
28
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
29
|
+
/** 名册窗的扫描上限(与 fleet 的终态窗同姿势:读面要有界)。窗满时回体带 `truncated:true`,
|
|
30
|
+
* `total` 于是诚实地是「窗内行数」而不是租户全量 —— 一个悄悄封顶的 total 比没有 total 更坏。 */
|
|
31
|
+
export declare const ROSTER_SCAN_LIMIT = 500;
|
|
32
|
+
export declare function handleAgentRoster(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
33
|
+
//# sourceMappingURL=agents-roster.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { sendJson, sendError } from "../send.js";
|
|
2
|
+
/** 名册窗的扫描上限(与 fleet 的终态窗同姿势:读面要有界)。窗满时回体带 `truncated:true`,
|
|
3
|
+
* `total` 于是诚实地是「窗内行数」而不是租户全量 —— 一个悄悄封顶的 total 比没有 total 更坏。 */
|
|
4
|
+
export const ROSTER_SCAN_LIMIT = 500;
|
|
5
|
+
/** 状态闭集(= core `BackgroundAgentRecord["status"]`)。`satisfies` 让 core 加一个状态词时这里编译红,
|
|
6
|
+
* 而不是让一个合法状态在 `?status=` 上被 400 掉(#157 的词表纪律:未知词不许有静默臂,已知词不许漏)。 */
|
|
7
|
+
const ROSTER_STATUSES = ["running", "parked", "completed", "failed", "killed"];
|
|
8
|
+
/**
|
|
9
|
+
* 名册行的**逐键处置表**。新增一个 `BackgroundAgentRowSummary` 键 ⇒ 这里编译红(TS2741),强迫对
|
|
10
|
+
* 「它上不上名册」表态。`keep` 的键必须同步进下面的白名单构造。
|
|
11
|
+
*
|
|
12
|
+
* · `handle` / `status` / `spawnedAt` / `updatedAt` / `settledAt`:身份与生命周期,壳渲名册就靠它们。
|
|
13
|
+
* · `name` / `agentType`:**寻址与标签**——agent-teams 的按名寻址就是 `name`;二者随行读回给**行的属主
|
|
14
|
+
* 自己**,与 `GET /v1/sessions` 回本人会话的 objective 预览同一条租户边界(fleet 那边 scrub 它们是因为
|
|
15
|
+
* 那条流上有**未署名会话**的行会落到别的会话的壳里,本面没有那条腿:每一行都已按 scope 过门)。
|
|
16
|
+
* · `owner`:租户内部键(门的**输入**:task/session 属主 id),永不渲染 ⇒ strip。
|
|
17
|
+
* (`scope` 压根不在 summary 面上 —— 它是 store 的查询键;所以它不是本表的一行,而是结构上就到不了 wire。)
|
|
18
|
+
* · `sessionScoped`:门的形状位(行是任务锚还是会话锚),对壳无意义、对攻击者是拓扑线索 ⇒ strip。
|
|
19
|
+
* · `sessionId` / `parentSessionId` / `rootSessionId`:会话寻址键 —— 名册不做寻址面(要拿子代产物走
|
|
20
|
+
* `/v1/runs/:id/subagents/:handle/output`,那条口有它自己的完整谓词),多给一个可寻址 id 只扩暴露面。
|
|
21
|
+
* · `usage`:用量轴归 `/v1/usage` 与 run 面,名册是**姿态面**不是计费面(最小披露:不顺手多发)。
|
|
22
|
+
*/
|
|
23
|
+
const ROSTER_KEY_DISPOSITION = {
|
|
24
|
+
handle: "keep",
|
|
25
|
+
name: "keep",
|
|
26
|
+
agentType: "keep",
|
|
27
|
+
status: "keep",
|
|
28
|
+
spawnedAt: "keep",
|
|
29
|
+
updatedAt: "keep",
|
|
30
|
+
settledAt: "keep",
|
|
31
|
+
owner: "strip",
|
|
32
|
+
sessionScoped: "strip",
|
|
33
|
+
sessionId: "strip",
|
|
34
|
+
parentSessionId: "strip",
|
|
35
|
+
rootSessionId: "strip",
|
|
36
|
+
usage: "strip",
|
|
37
|
+
};
|
|
38
|
+
void ROSTER_KEY_DISPOSITION;
|
|
39
|
+
function projectRow(r) {
|
|
40
|
+
return {
|
|
41
|
+
handle: r.handle,
|
|
42
|
+
...(r.name !== undefined ? { name: r.name } : {}),
|
|
43
|
+
...(r.agentType !== undefined ? { agentType: r.agentType } : {}),
|
|
44
|
+
status: r.status,
|
|
45
|
+
spawnedAt: r.spawnedAt,
|
|
46
|
+
updatedAt: r.updatedAt,
|
|
47
|
+
...(r.settledAt !== undefined ? { settledAt: r.settledAt } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** 游标 = `(spawnedAt, handle)` 的不透明 base64url —— 排序键**逐字**是它,所以同 `spawnedAt` 的行
|
|
51
|
+
* (真实存在:同一拍派出的兄弟子代)不会因为无 tiebreak 而漏页或重页。 */
|
|
52
|
+
function encodeCursor(c) {
|
|
53
|
+
return Buffer.from(JSON.stringify(c)).toString("base64url");
|
|
54
|
+
}
|
|
55
|
+
function decodeCursor(raw) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(Buffer.from(raw, "base64url").toString());
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// 只带错误的**类名**,不回显 payload 片段(config-provider 的 redactConfigError 同一条纪律:
|
|
62
|
+
// 被误粘进查询串的凭据不该经由错误文案再走一圈)。
|
|
63
|
+
return { ok: false, why: `not decodable base64url JSON (${err instanceof Error ? err.name : "unknown error"})` };
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
66
|
+
return { ok: false, why: "cursor payload is not an object" };
|
|
67
|
+
const { spawnedAt, handle } = parsed;
|
|
68
|
+
if (typeof spawnedAt !== "number" || !Number.isFinite(spawnedAt))
|
|
69
|
+
return { ok: false, why: "cursor payload lacks a finite `spawnedAt`" };
|
|
70
|
+
if (typeof handle !== "string" || handle.length === 0)
|
|
71
|
+
return { ok: false, why: "cursor payload lacks a non-empty `handle`" };
|
|
72
|
+
return { ok: true, cursor: { spawnedAt, handle } };
|
|
73
|
+
}
|
|
74
|
+
export async function handleAgentRoster(req, res, url, ctx) {
|
|
75
|
+
const miss = { fell: false };
|
|
76
|
+
await handleAgentRosterBody(req, res, url, ctx, miss);
|
|
77
|
+
return !miss.fell;
|
|
78
|
+
}
|
|
79
|
+
async function handleAgentRosterBody(req, res, url, ctx, miss) {
|
|
80
|
+
const { deps } = ctx;
|
|
81
|
+
const { sessionOwnerScope } = ctx.helpers;
|
|
82
|
+
if (req.method !== "GET" || url !== "/v1/agents/roster") {
|
|
83
|
+
miss.fell = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// 鉴权在店可用性判**之前**(与 GET /v1/sessions 逐字同序):反过来会让「这台装没装 durable agent 店」
|
|
87
|
+
// 先于 401 泄给未认证调用方 —— 一个小的后端存在性 oracle。
|
|
88
|
+
const { fleetWide, gateOwner } = sessionOwnerScope(req); // direct-door safe:验证过的身份,不是可伪造的头
|
|
89
|
+
if (!fleetWide && gateOwner === null) {
|
|
90
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const store = deps.backgroundAgentStore;
|
|
94
|
+
if (!store) {
|
|
95
|
+
sendError(res, 501, "capability.background_agent_store_required", "the agent roster requires a durable background-agent store (DB_BACKEND=mysql|pg, or the local file backend)");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
99
|
+
const limitRaw = q.get("limit");
|
|
100
|
+
if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
|
|
101
|
+
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const limit = limitRaw === null ? 20 : Number(limitRaw);
|
|
105
|
+
const beforeRaw = q.get("before");
|
|
106
|
+
const decoded = beforeRaw === null ? undefined : decodeCursor(beforeRaw);
|
|
107
|
+
if (decoded !== undefined && !decoded.ok) {
|
|
108
|
+
// 文案冻结在 wire 上(api-error-text-freeze 的一行);`detail` 是 additive 的**为什么**,
|
|
109
|
+
// 供调用方自诊(它描述的是调用方自己传进来的串,零跨租户信息)。
|
|
110
|
+
sendError(res, 400, "request.query_invalid", "before must be the opaque `nextBefore` cursor from a previous page of this endpoint", { detail: decoded.why });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const cursor = decoded?.ok === true ? decoded.cursor : undefined;
|
|
114
|
+
const statusRaw = q.get("status");
|
|
115
|
+
if (statusRaw !== null && !ROSTER_STATUSES.includes(statusRaw)) {
|
|
116
|
+
sendError(res, 400, "request.query_invalid", `status must be one of: ${ROSTER_STATUSES.join(", ")}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const status = statusRaw === null ? undefined : statusRaw;
|
|
120
|
+
// scope:principal 调用方**钉死自己**(`?scope=` 对它无效 —— 与 GET /v1/sendfile-links 同判);
|
|
121
|
+
// fleet-wide(service token / dev-open 单用户)可以指定,缺省 `"default"` = 单租户拼法
|
|
122
|
+
// (core prepare-task 的 `spec.principal ?? "default"` 写侧同源,fleet 读侧的 `?? "default"` 同族)。
|
|
123
|
+
let scope;
|
|
124
|
+
if (gateOwner !== null) {
|
|
125
|
+
scope = gateOwner;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const raw = q.get("scope");
|
|
129
|
+
if (raw === null) {
|
|
130
|
+
scope = "default";
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const s = raw.trim();
|
|
134
|
+
if (s === "" || s.length > 190) {
|
|
135
|
+
sendError(res, 400, "request.query_invalid", "scope must be a non-blank tenant principal of at most 190 characters");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
scope = s;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const session = q.get("session");
|
|
142
|
+
if (session !== null && session.trim() === "") {
|
|
143
|
+
sendError(res, 400, "request.query_invalid", "session must be a non-blank session id when present");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// 两只既有只读口,同一套 scope 门:`listBySession` 按会话树锚(会话锚 ∪ root 锚)窄化,
|
|
147
|
+
// 缺席则整 scope。窗上限 +1 用来判「租户行数超窗」,不进结果。
|
|
148
|
+
const opts = { ...(status !== undefined ? { status } : {}), limit: ROSTER_SCAN_LIMIT + 1 };
|
|
149
|
+
const scanned = session !== null ? await store.listBySession(scope, session, opts) : await store.listByScope(scope, opts);
|
|
150
|
+
const truncated = scanned.length > ROSTER_SCAN_LIMIT;
|
|
151
|
+
let window = truncated ? scanned.slice(0, ROSTER_SCAN_LIMIT) : scanned;
|
|
152
|
+
// 🔴 窗边界的**同 spawnedAt 组整组丢弃**(codex 对抗复审 finding 3,验真属实)。
|
|
153
|
+
// store 契约只保证 `spawnedAt` 降序、**同毫秒无 tiebreak**;而本面的声明序是 `(spawnedAt, handle)`。
|
|
154
|
+
// 于是窗边界那一组兄弟行被 store 按它自己的任意序切开时,按本面的序本该入窗的那一行可能被切掉,
|
|
155
|
+
// 而 `before` 游标只在窗内翻 ⇒ 那一行**永远翻不到**(一个静默的洞)。
|
|
156
|
+
// 只用 store 保证得了的那条契约修:丢掉尾部整组之后,窗恒是「`spawnedAt > cut` 的**全部**行」——
|
|
157
|
+
// 按降序取前缀,这个集合必然完整,于是窗是本面声明序的**干净前缀**。代价=少几行,而 `truncated:true`
|
|
158
|
+
// 本就已经说了"还有更多"。**刻意不下推排序/游标契约到 store**:那是 core 的接口,归属上游(源头修复纪律)。
|
|
159
|
+
// 判据是**边界真的被切开**(第一个落窗外的行与窗内最后一行同 `spawnedAt`),不是"只要截断就丢一组"——
|
|
160
|
+
// 时间戳互不相同时切口本来就干净,无谓丢行会让窗白白变小。
|
|
161
|
+
if (truncated && window.length > 0 && scanned[ROSTER_SCAN_LIMIT].spawnedAt === window[window.length - 1].spawnedAt) {
|
|
162
|
+
const cut = window[window.length - 1].spawnedAt;
|
|
163
|
+
const clean = window.filter((r) => r.spawnedAt > cut);
|
|
164
|
+
// 退化形(整窗 501 行同一毫秒)⇒ 丢完就空了,那时保留原窗:少给不如给,且 `truncated` 已如实。
|
|
165
|
+
if (clean.length > 0)
|
|
166
|
+
window = clean;
|
|
167
|
+
}
|
|
168
|
+
// 排序键 = 游标键(新→旧,同 spawnedAt 时按 handle 降序):店已按 spawnedAt DESC 给,但 tiebreak 是
|
|
169
|
+
// 方言细节,名册自己定死一个确定序,分页才不会在同拍兄弟行上漏页/重页。
|
|
170
|
+
const sorted = [...window].sort((a, b) => (b.spawnedAt - a.spawnedAt) || (a.handle < b.handle ? 1 : a.handle > b.handle ? -1 : 0));
|
|
171
|
+
const scoped = cursor === undefined ? sorted : sorted.filter((r) => r.spawnedAt < cursor.spawnedAt || (r.spawnedAt === cursor.spawnedAt && r.handle < cursor.handle));
|
|
172
|
+
const page = scoped.slice(0, limit);
|
|
173
|
+
const last = page[page.length - 1];
|
|
174
|
+
const hasMore = last !== undefined && scoped.length > page.length;
|
|
175
|
+
sendJson(res, 200, {
|
|
176
|
+
agents: page.map(projectRow),
|
|
177
|
+
// `total` = **本名册窗**内的行数(属主口径 —— 跨租户的行既不进页也不进这个数)。窗被截断时
|
|
178
|
+
// `truncated:true` 同发,于是这个数字什么时候是"全量"、什么时候是"窗内"对消费端是可判别的。
|
|
179
|
+
total: window.length,
|
|
180
|
+
...(truncated ? { truncated: true } : {}),
|
|
181
|
+
...(hasMore ? { nextBefore: encodeCursor({ spawnedAt: last.spawnedAt, handle: last.handle }) } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=agents-roster.js.map
|
|
@@ -6,6 +6,7 @@ import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
|
6
6
|
import { sendJson, sendError } from "../send.js";
|
|
7
7
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
8
8
|
import { projectMemoryEngineCapability } from "../../memory-posture.js";
|
|
9
|
+
import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
|
|
9
10
|
import { gatedPrincipal } from "../principal-gate.js";
|
|
10
11
|
export async function handleCapabilities(req, res, url, ctx) {
|
|
11
12
|
const miss = { fell: false };
|
|
@@ -238,6 +239,39 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
238
239
|
// **所连 server 广告本位**,不是壳自身版本 —— 新壳连老 server / 回滚到旧二进制的副本 / 混版 fleet
|
|
239
240
|
// 三形下,壳看见本位缺席就继续注 env,于是"翻译还没到货但壳已经撤注"那个放宽窗结构上不存在。
|
|
240
241
|
modeShellGateTranslation: true,
|
|
242
|
+
// #A5([4147] 件2;[4074] DELIVERY A5「auto dark 武装可见性」):`permissionMode:"auto"` 的**缺省态**
|
|
243
|
+
// 披露。自查结论(施工期亲读 core dist + 本仓装配):
|
|
244
|
+
// · 收词是真的 —— 提交面按五词闭集 400,`shellGateForMode("auto")==="classify"`,而
|
|
245
|
+
// `deriveSettingsPolicy` 的 auto 与 default 走**同一条臂**(同一只 fs-write ask 门)。
|
|
246
|
+
// · 真正让 auto ≠ default 的是 core 的 auto 分类器,而它**两半才武装**:per-principal
|
|
247
|
+
// `runtimeCaps.autoMode === true`(center 解出的 entitlement)∧ `RunnerDeps.autoMode` 席位在场。
|
|
248
|
+
// · 席位这一半在**服务端装配**里无条件挂(`boot/runner-deps.ts` 的 `createRunnerDeps` —— 那是唯一
|
|
249
|
+
// 会去 serve 本能力面的装配路径;run-local 的一次性 CLI 不挂它,但它也不发这张表,所以本位不说谎);
|
|
250
|
+
// entitlement 那一半只有在配了 center 且
|
|
251
|
+
// 非 dry-run 时才可能存在 ⇒ **缺省态是暗的**,而此前 wire 上一个字都没有(taskSettings.permissionMode
|
|
252
|
+
// 只说「键被消费」;core 的 WiringManifest 没有 autoMode 段,诊断面也不报;而诊断面还是 operator-only)。
|
|
253
|
+
// 🔴 三项不合成一个布尔:合成后「这份二进制不认识 auto」与「认识但本机没有 entitlement 源」会得到
|
|
254
|
+
// 同一个 false,而壳对这两种要做的事不同(前者别给这一档;后者给,但别宣称在筛)。
|
|
255
|
+
// 🔴 `entitlementSource` 只说**源在不在**,绝不冒充「你被授权了」——授权是 per-principal 的,部署级
|
|
256
|
+
// 布尔对它说不了话(与 `permissionRules` 那一位刻意相反的方向,见其注)。谓词与 boot 的 caps 客户端
|
|
257
|
+
// 装配点**同一只函数**,两面不可能分家。
|
|
258
|
+
permissionModeAuto: {
|
|
259
|
+
accepted: true,
|
|
260
|
+
classifierSeat: true,
|
|
261
|
+
entitlementSource: centerEntitlementSourceWired(deps.config),
|
|
262
|
+
},
|
|
263
|
+
// #A4([4147] 件1):READ 容纳面的**生效档**披露位。值 = 本部署**显式**表态的那一档
|
|
264
|
+
// (env `READ_FACE`,或 config-center 的 `readFace.face` —— 两条腿落的是同一个 config 键,
|
|
265
|
+
// 所以这一位对「组织下发」与「本机 env」不做区分,它说的就是最终生效值)。
|
|
266
|
+
// 🔴 `null` = **本部署没有钉这一档**,由引擎默认接管。刻意不把 `null` 折成 `"roots"`:server 在
|
|
267
|
+
// 这条轴上从不复制上游默认(config-types.ts `readFace` 的「缺席=不铸」同一句),把引擎默认抄进 wire
|
|
268
|
+
// 会在 core 改默认的那天变成一句谎,而消费端无从察觉。
|
|
269
|
+
// 🔴 **最小披露**:deny 表(pattern 表 / 内建档位 / 排除行名)**不上**能力面 —— 那是策略内容,
|
|
270
|
+
// 与 governance 段同判(能力面回答「这台机器的 READ 面是什么姿态」,不回答「它具体挡了哪些路径」)。
|
|
271
|
+
// 定界:core 的 `WiringManifest`(GET /v1/diagnostics/wiring 的 `static` 段)**没有** READ-face 段,
|
|
272
|
+
// 诊断面也从不报它 —— 本位不是复制既有披露,是这条轴上第一次有可读面;且诊断面是 operator-only,
|
|
273
|
+
// 壳读不到,所以这一位必须在能力面上。
|
|
274
|
+
readFace: deps.config.readFace ?? null,
|
|
241
275
|
// [1476] R1 / [1478] R2: TaskRequest.appendSystemPrompt is accepted top-level → TaskSpec.appendSystemPrompt
|
|
242
276
|
// (first-class lane for the shell's product-knowledge block). A shell probes THIS flag: false/absent ⇒ fall
|
|
243
277
|
// back to the settings.outputStyle ride-along (older servers fold that into the same spec field).
|
|
@@ -366,6 +400,11 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
366
400
|
// Each flag is true ONLY when its PRODUCING path exists (never a bare const) — list needs an enumerator
|
|
367
401
|
// (session store OR legacy run-store aggregate), fork needs sessionStorage.fork, delete needs the
|
|
368
402
|
// purgeSession coordinator. Absent backend (in-memory dev) ⇒ false, so "says yes but 501s" can't happen.
|
|
403
|
+
// #A1([4147] 件3;C-R13 链):`GET /v1/agents/roster` —— 后台 agent 名册读面。谓词**逐字**是那条
|
|
404
|
+
// 路由的 501 门(`deps.backgroundAgentStore`),所以「说 yes ⟺ 路由真能用」是结构性的。
|
|
405
|
+
// ⚠️ 辖域=**列举面**:名册回的是 content-free 投影(handle/name/agentType/status/三个时间戳);
|
|
406
|
+
// 要子代产物正文走 `/v1/runs/:id/subagents/:handle/output`(那条口有它自己的谓词 `subagentOutput`)。
|
|
407
|
+
agentRoster: Boolean(deps.backgroundAgentStore),
|
|
369
408
|
sessionList: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
|
|
370
409
|
sessionSearch: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions), // GET /v1/sessions?q= (all lister backends carry it)
|
|
371
410
|
sessionFork: Boolean(deps.sessionStorage?.fork),
|
package/dist/http/server.js
CHANGED
|
@@ -42,6 +42,7 @@ import { handleMemoryPolicy } from "./routes/memory-policy.js";
|
|
|
42
42
|
import { handleSharedMemory } from "./routes/shared-memory.js";
|
|
43
43
|
import { handleRules } from "./routes/rules.js";
|
|
44
44
|
import { handleSessionsList } from "./routes/sessions-list.js";
|
|
45
|
+
import { handleAgentRoster } from "./routes/agents-roster.js";
|
|
45
46
|
import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
|
|
46
47
|
import { handleSessionSync, createSessionSyncLocal } from "./routes/session-sync.js";
|
|
47
48
|
import { handleAttachments } from "./routes/attachments.js";
|
|
@@ -212,6 +213,7 @@ const ROUTE_DOMAINS = [
|
|
|
212
213
|
handleRules,
|
|
213
214
|
handleSharedMemory,
|
|
214
215
|
handleSessionsList,
|
|
216
|
+
handleAgentRoster,
|
|
215
217
|
handleSessions,
|
|
216
218
|
handleSessionSync,
|
|
217
219
|
handleAttachments,
|
|
@@ -705,6 +707,10 @@ export function createHttpServer(rawDeps) {
|
|
|
705
707
|
// design/158 A9:会话/发件链接列表面(routes/sessions-list.ts)。
|
|
706
708
|
if (await handleSessionsList(req, res, url, ctx))
|
|
707
709
|
return;
|
|
710
|
+
// #A1([4147] 件3):后台 agent 名册读面(routes/agents-roster.ts)。位置跟着 sessions-list —— 同一族
|
|
711
|
+
// (租户只读列举面、同一只 `sessionOwnerScope` 门、同样在全局服务凭据门之后)。
|
|
712
|
+
if (await handleAgentRoster(req, res, url, ctx))
|
|
713
|
+
return;
|
|
708
714
|
// design/158 A9:单会话面(routes/sessions.ts:init/settings/mcp/head/events/audit/fork/delete/policy/workspace)。
|
|
709
715
|
if (await handleSessions(req, res, url, ctx))
|
|
710
716
|
return;
|
|
@@ -3307,6 +3313,9 @@ const ROUTE_LABEL_LITERALS = new Set([
|
|
|
3307
3313
|
// 它的 QPS 与 duration 是判断「有没有人在扫我们」的第一手材料;RPC 口是外部 peer 的提交面。
|
|
3308
3314
|
// 两条都落进 `other` 桶 = 对外暴露面在 metrics 上不可分。同上,写**字面量**不引常量(两道门扫源码文本)。
|
|
3309
3315
|
"/.well-known/agent-card.json", "/v1/a2a",
|
|
3316
|
+
// #A1([4147] 件3):后台 agent 名册读面。壳每次开面板打一次,是**每用户高频**的租户读 ——
|
|
3317
|
+
// 落进 `other` 桶会把它的 duration 和 4xx 混进一堆低频运维口里。同上,写字面量不引常量。
|
|
3318
|
+
"/v1/agents/roster",
|
|
3310
3319
|
]);
|
|
3311
3320
|
/** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
|
|
3312
3321
|
* [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
|
package/dist/run-local.js
CHANGED
|
@@ -59,7 +59,7 @@ import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
|
59
59
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
60
60
|
import { loadConfig, logConfigDiagnostics, numEnv } from "./config.js";
|
|
61
61
|
import { createConfigProvider } from "./config-provider.js";
|
|
62
|
-
import { applyEffective, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
62
|
+
import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
63
63
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
64
64
|
import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
|
|
65
65
|
import { loadSkills } from "./capabilities/skills.js";
|
|
@@ -347,6 +347,13 @@ export async function runLocal(argv, deps = {}) {
|
|
|
347
347
|
// 一域可投。补齐属于 registry-core(上游件),不在本车射程;在那之前别在文档里承诺 config.d/a2a.json。
|
|
348
348
|
if (effective?.a2a)
|
|
349
349
|
config.a2aPeers = resolveA2aPeers(effective.a2a, logger);
|
|
350
|
+
// #A4([4147] 件1):READ 容纳面四键的 center 腿 —— 与 server 腿同一只函数、同一条 restart-to-apply
|
|
351
|
+
// 线(本腿是一次性 CLI,"restart" 即下次运行)。env 显式恒赢的判据在函数内,两腿零分家。
|
|
352
|
+
// ⚠️ 如实记(与上面 a2a 那条同款诚实句):**本地 config.d 车道今天接不上** —— registry-core 的
|
|
353
|
+
// 可移植域表里没有 readFace 域,`config-provider.ts` 的 `mapToServiceEffective` 闭式投影也就没有这一
|
|
354
|
+
// 域可投;这一行今天只在 `CONFIG_PROVIDER=remote` 的 run-local 形上写得进值。补齐属于 registry-core
|
|
355
|
+
// (上游件),在那之前别在文档里承诺 config.d/read-face.json。
|
|
356
|
+
applyCenterReadFace(config, r.effective, logger);
|
|
350
357
|
logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version });
|
|
351
358
|
}
|
|
352
359
|
}
|
|
@@ -30,6 +30,7 @@ import type { RuntimeCaps } from "@sema-agent/core";
|
|
|
30
30
|
import type { EntitlementRuntimeCaps } from "@sema-agent/registry-core";
|
|
31
31
|
import { fetchPrincipalCaps, type ExecutionRuling } from "./config-center/facade.js";
|
|
32
32
|
import type { ScenarioRuling } from "./capabilities/scenarios.js";
|
|
33
|
+
import type { ServiceConfig } from "./config-types.js";
|
|
33
34
|
import { type Logger } from "./observability/logger.js";
|
|
34
35
|
/** test-only: reset the once-per-process trace so each test observes a fresh "first hit". */
|
|
35
36
|
export declare function resetExecutionLaneDefaultTraceForTest(): void;
|
|
@@ -75,6 +76,21 @@ export interface EntitlementsResolverOpts {
|
|
|
75
76
|
* 0.13.0 已把 autoMode 收进 EntitlementRuntimeCaps(types.d.ts:1786)——宽读逃逸期结束,现为类型直读
|
|
76
77
|
* (allowObservers 同款收编先例)。 */
|
|
77
78
|
export declare function toCoreRuntimeCaps(ent: EntitlementRuntimeCaps | null | undefined): RuntimeCaps | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* #A5([4147] 件2)—— 「这台 worker 上**存在** per-principal entitlement 源吗」的**单一谓词**。
|
|
81
|
+
*
|
|
82
|
+
* 判据逐字 = `boot/runtime-caps.ts` 建 caps 客户端的那一条(**配了 center ∧ 非 dry-run**):dry-run 的部署
|
|
83
|
+
* 拉取并记录 center 配置但**不应用、不执法**(observe-only),所以它解不出任何 per-principal caps —— 把它
|
|
84
|
+
* 算成源会让能力面对着一台永不武装的机器说「有源」。
|
|
85
|
+
*
|
|
86
|
+
* 为什么要抽成函数:这条判据现在有**两个**读者 —— boot 的装配点,与 `/v1/capabilities` 的
|
|
87
|
+
* `permissionModeAuto.entitlementSource` 披露位。两处各写一份条件,就是本仓在册的
|
|
88
|
+
* 「诊断页说亮、能力位说暗」病族的入口(装配与披露对同一件事各算各的)。
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ **辖域**:说的是「源在不在」,不是「某个 principal 被授权了」。授权是 per-principal 的
|
|
91
|
+
* (`RuntimeCaps.autoMode === true` 逐 principal 解),任何部署级布尔都对它说不了话。
|
|
92
|
+
*/
|
|
93
|
+
export declare function centerEntitlementSourceWired(config: Pick<ServiceConfig, "configCenter">): boolean;
|
|
78
94
|
/** A MISCONFIG diagnostic (review MEDIUM): a worker-scoped `wpt_<self>` pull token REQUIRES `?worker=<self>` to
|
|
79
95
|
* resolve per-principal caps — so a `wpt_` token with NO `SEMA_REGISTRY_WORKER` set will 403
|
|
80
96
|
* on every caps fetch → fail-closed DENY ALL workflows for ALL principals (correct degrade, but otherwise silent
|
|
@@ -86,6 +86,23 @@ export function toCoreRuntimeCaps(ent) {
|
|
|
86
86
|
caps.autoMode = autoMode;
|
|
87
87
|
return Object.keys(caps).length ? caps : undefined;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* #A5([4147] 件2)—— 「这台 worker 上**存在** per-principal entitlement 源吗」的**单一谓词**。
|
|
91
|
+
*
|
|
92
|
+
* 判据逐字 = `boot/runtime-caps.ts` 建 caps 客户端的那一条(**配了 center ∧ 非 dry-run**):dry-run 的部署
|
|
93
|
+
* 拉取并记录 center 配置但**不应用、不执法**(observe-only),所以它解不出任何 per-principal caps —— 把它
|
|
94
|
+
* 算成源会让能力面对着一台永不武装的机器说「有源」。
|
|
95
|
+
*
|
|
96
|
+
* 为什么要抽成函数:这条判据现在有**两个**读者 —— boot 的装配点,与 `/v1/capabilities` 的
|
|
97
|
+
* `permissionModeAuto.entitlementSource` 披露位。两处各写一份条件,就是本仓在册的
|
|
98
|
+
* 「诊断页说亮、能力位说暗」病族的入口(装配与披露对同一件事各算各的)。
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ **辖域**:说的是「源在不在」,不是「某个 principal 被授权了」。授权是 per-principal 的
|
|
101
|
+
* (`RuntimeCaps.autoMode === true` 逐 principal 解),任何部署级布尔都对它说不了话。
|
|
102
|
+
*/
|
|
103
|
+
export function centerEntitlementSourceWired(config) {
|
|
104
|
+
return config.configCenter !== undefined && !config.configCenter.dryRun;
|
|
105
|
+
}
|
|
89
106
|
/** A MISCONFIG diagnostic (review MEDIUM): a worker-scoped `wpt_<self>` pull token REQUIRES `?worker=<self>` to
|
|
90
107
|
* resolve per-principal caps — so a `wpt_` token with NO `SEMA_REGISTRY_WORKER` set will 403
|
|
91
108
|
* on every caps fetch → fail-closed DENY ALL workflows for ALL principals (correct degrade, but otherwise silent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.29.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",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "7.2.0
|
|
72
|
+
"@sema-agent/sdk": "7.2.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|