@sema-agent/server 7.0.0 → 7.2.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/README.md +1 -0
- package/dist/boot/org-memory.d.ts +8 -5
- package/dist/boot/org-memory.js +23 -12
- package/dist/boot/resolve-spec.d.ts +16 -1
- package/dist/boot/resolve-spec.js +31 -21
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +19 -0
- package/dist/config-center/http-client.js +81 -32
- package/dist/config-types.d.ts +15 -6
- package/dist/config.js +4 -2
- package/dist/http/routes/capabilities.js +4 -2
- package/dist/http/routes/tasks.js +39 -3
- package/dist/http/server.js +10 -4
- package/dist/leader/wire.js +4 -0
- package/dist/main.js +6 -3
- package/dist/org-memory-admission.d.ts +2 -1
- package/dist/org-memory-admission.js +10 -4
- package/dist/parked-decide.js +5 -2
- package/dist/question.d.ts +15 -3
- package/dist/question.js +18 -4
- package/dist/runtime-governance.d.ts +25 -10
- package/dist/runtime-governance.js +42 -10
- package/dist/task-settings.d.ts +10 -8
- package/dist/task-settings.js +7 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ The server is configured entirely through environment variables. The most import
|
|
|
142
142
|
| `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none |
|
|
143
143
|
| `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
|
|
144
144
|
| `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` disables |
|
|
145
|
+
| `MANUAL_MODE_SHELL_GATE` | unset (off) | `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) |
|
|
145
146
|
| `MODEL_CONNECT_TIMEOUT_MS` | `30000` | Gateway connect timeout |
|
|
146
147
|
| `MODEL_FIRST_TOKEN_TIMEOUT_MS` | `120000` | First-token timeout |
|
|
147
148
|
| `MODEL_IDLE_TIMEOUT_MS` | `300000` | Mid-stream idle timeout (`0` = off) |
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 目录源三态(单选,授权面不做双源合并):
|
|
6
6
|
* · center 在场(configCenter 且非 dryRun)⇒ 远程腿:per-principal caps 响应的 `orgMemory` 段
|
|
7
|
-
* (
|
|
7
|
+
* (走 `fetchPrincipalOrgMemory` —— 与 caps 腿同一条 HTTP 请求形与同一道 principal 回声核,但**只
|
|
8
|
+
* 投影 org 段**读,治理面值漂移不连坐;段键缺席=旧 center 能力握手 ⇒ 目录记 section_absent 瞬时)。
|
|
8
9
|
* **独立 fetch,不复用 caps 缓存条目**(C3:org 授权段自带判别联合缓存,两张表的失败域互不干扰
|
|
9
10
|
* ——caps 腿的 deny 缓存/304 复用机制一旦共享,org 面的 gen 拒退与退避语义就会被 caps 语义污染)。
|
|
10
11
|
* 流量记账:每 principal 每 grant TTL(60s)至多多一拍 caps 拉取,有界。
|
|
@@ -14,10 +15,12 @@
|
|
|
14
15
|
* · 都缺 ⇒ resolver 缺席:core 对 request-origin org scope 铸 `memory.admission_required`(fail-closed),
|
|
15
16
|
* deployment-origin 走 `deploymentMemoryScopes` 自证通道不受影响(单用户零迁移,v4 §0 伤害①)。
|
|
16
17
|
*
|
|
17
|
-
* 能力探测(C12,fail-loud):多租户(requirePrincipal)
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* 能力探测(C12,fail-loud):多租户(requirePrincipal)部署 **且记忆面真点亮**(MEMORY_ENGINE 开 +
|
|
19
|
+
* backend 非 file——见下方 `memoryPlaneLive` 注)的 projects 登记簿里挂了 org 键 ——这些键在多租户下
|
|
20
|
+
* 按 request-origin 盖章(N2 部署形态维)——而目录源缺席(含 configCenter dryRun 形)⇒ 启动报错:
|
|
21
|
+
* 该部署的每个 projectId 请求都会在 prepare 期被整拒,「audit 在跑但恒拒」不是可运行状态,响亮拒
|
|
22
|
+
* 启动比静默全拒服务诚实(clay 三裁 [2687]:直接 BREAKING,不留兼容臂)。记忆面 dark 的部署里这些
|
|
23
|
+
* 键根本到不了准入门,不在探测范围(复审 D-1)。
|
|
21
24
|
*/
|
|
22
25
|
import type { RunnerDeps } from "@sema-agent/core";
|
|
23
26
|
import type { ServiceConfig } from "../config.js";
|
package/dist/boot/org-memory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchPrincipalOrgMemory } from "../config-center/http-client.js";
|
|
2
2
|
import { createMemoryScopeAdmission, createOrgMemoryDirectory, parseOrgDirectoryStatic } from "../org-memory-admission.js";
|
|
3
3
|
const ORG_PREFIX = "org:";
|
|
4
4
|
/** 部署自证 org scope 集(core 准入门的 deployment origin 词表):env 钉的 MEMORY_SCOPE(org 形时)
|
|
@@ -31,14 +31,11 @@ export function createOrgMemoryAdmissionWiring(opts) {
|
|
|
31
31
|
}
|
|
32
32
|
const directory = centerLeg !== undefined
|
|
33
33
|
? createOrgMemoryDirectory({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
|
|
40
|
-
return r.orgMemory;
|
|
41
|
-
},
|
|
34
|
+
// 复审 D-2 修:走 org 段专用读取口而不是整封 caps 读。两者同一条 HTTP 腿、同一道 C8 回声核,
|
|
35
|
+
// 差别只在**解析投影**:org 腿只读 principal+orgMemory 两字段,治理面(runtimeCaps/budget)的
|
|
36
|
+
// 值漂移因此不再连坐 org 授权面(此前:一封 runtimeCaps 漂移的响应会让形状完全正确的 org 段
|
|
37
|
+
// 读不出来 ⇒ 目录记 fetch_failed ⇒ 准入瞬时拒)。C3「两张表失败域互不干扰」至此是双向的。
|
|
38
|
+
fetchSection: (principal) => fetchPrincipalOrgMemory(centerLeg.baseUrl, centerLeg.token, principal, fetch, centerLeg.worker),
|
|
42
39
|
grantTtlMs: config.memoryOrgGrantTtlMs,
|
|
43
40
|
unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
|
|
44
41
|
})
|
|
@@ -49,8 +46,17 @@ export function createOrgMemoryAdmissionWiring(opts) {
|
|
|
49
46
|
unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
|
|
50
47
|
})
|
|
51
48
|
: undefined;
|
|
52
|
-
// C12 能力探测:多租户 + 登记簿有 org 用面 + 目录源缺席 ⇒ 拒启动(见文件头注)。
|
|
53
|
-
|
|
49
|
+
// C12 能力探测:多租户 + **记忆面真点亮** + 登记簿有 org 用面 + 目录源缺席 ⇒ 拒启动(见文件头注)。
|
|
50
|
+
//
|
|
51
|
+
// 复审 D-1(7.1.0 后修):探测原先少了「记忆面是否真点亮」这一维,是假阳。多租户下记忆引擎只在
|
|
52
|
+
// `MEMORY_ENGINE` 开 **且** backend 非 file 时才装(boot/stores.ts:file 形在 requirePrincipal 下恒
|
|
53
|
+
// 走 memoryEngineBackendFor() = undefined,「file 底座无租户隔离」);记忆面 dark ⇒ resolve-spec 的
|
|
54
|
+
// `memoryEngine ? memorySpecForRequest(...) : undefined` 根本不铸 spec.memory ⇒ 登记簿里的 org 键
|
|
55
|
+
// 一次也到不了 core 准入门。此时拒启动既拦下了一个本来跑得好好的部署,报错串里那句「every request
|
|
56
|
+
// selecting these projects would be refused at prepare」也是假陈述——探测的成立前提就是「这些键真会
|
|
57
|
+
// 被请求带到准入门」。
|
|
58
|
+
const memoryPlaneLive = config.memoryEngineEnabled && config.memoryEngineBackend !== "file";
|
|
59
|
+
if (directory === undefined && config.requirePrincipal === true && memoryPlaneLive) {
|
|
54
60
|
const orgProjects = Object.entries(config.projects)
|
|
55
61
|
.filter(([, p]) => (p.defaultScopes ?? []).some((s) => s.startsWith(ORG_PREFIX)))
|
|
56
62
|
.map(([id]) => id);
|
|
@@ -64,8 +70,13 @@ export function createOrgMemoryAdmissionWiring(opts) {
|
|
|
64
70
|
? createMemoryScopeAdmission(directory, {
|
|
65
71
|
mode: config.memoryOrgAdmissionMode,
|
|
66
72
|
// §5 观测:outcome 闭集 metric + 审计日志线(per-principal 归因走日志,principal 不入 metric label)。
|
|
73
|
+
// 复审 D-3(7.1.0 后修)两处:①`mode` 进 metric label —— 不然 audit 模式记的 would-refuse 与
|
|
74
|
+
// enforce 模式的真拒在同一条 `outcome="denied"` 曲线上不可辨,而「照算不真拒、看曲线」正是
|
|
75
|
+
// audit 这个诊断位存在的唯一用途(二值闭集,基数代价为零);②principal 真的进日志行 —— 归因
|
|
76
|
+
// 由 resolver 塞进 details(此前 details 里根本没有 principal,「per-principal 归因走日志」
|
|
77
|
+
// 这句注释与 CHANGELOG 7.0.0 的同款公开陈述都不成立:运维只看得到「某个 org scope 被拒」)。
|
|
67
78
|
onOutcome: (outcome, details) => {
|
|
68
|
-
metrics.inc("memory_admission_total", { outcome });
|
|
79
|
+
metrics.inc("memory_admission_total", { outcome, mode: config.memoryOrgAdmissionMode });
|
|
69
80
|
if (outcome !== "ok" || details !== undefined)
|
|
70
81
|
logger.warn("memory_admission_outcome", { outcome, ...details });
|
|
71
82
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MemoryBackend, type RunnerDeps } from "@sema-agent/core";
|
|
1
|
+
import { type MemoryBackend, type RunnerDeps, type ToolPolicy } from "@sema-agent/core";
|
|
2
2
|
import type { PromptsDomainFaces } from "../prompts-domain-validate.js";
|
|
3
3
|
import { SessionEnvironmentSelection, selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
|
|
4
4
|
import { sendUserFileTool } from "../capabilities/send-user-file-tool.js";
|
|
@@ -69,6 +69,21 @@ export interface ResolveSpecCtx {
|
|
|
69
69
|
perTaskImage: PerTaskImageRegistry;
|
|
70
70
|
sessionEnvSelection: SessionEnvironmentSelection;
|
|
71
71
|
}
|
|
72
|
+
/** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
73
|
+
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
74
|
+
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
75
|
+
* RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
|
|
76
|
+
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(此时执行只会拿到 headless
|
|
77
|
+
* 空答,park 才是把问题送到人面前的那条腿)。
|
|
78
|
+
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
|
|
79
|
+
* ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
|
|
80
|
+
* 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
|
|
81
|
+
* 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker)
|
|
82
|
+
* 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整
|
|
83
|
+
* 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。 */
|
|
84
|
+
export declare function createDurableQuestionGate(live: {
|
|
85
|
+
hasLiveContext(): boolean;
|
|
86
|
+
} | undefined): ToolPolicy;
|
|
72
87
|
export declare function createResolveSpec(ctx: ResolveSpecCtx): ServiceDeps["resolveSpec"];
|
|
73
88
|
export {};
|
|
74
89
|
//# sourceMappingURL=resolve-spec.d.ts.map
|
|
@@ -50,11 +50,17 @@ import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationF
|
|
|
50
50
|
import { redactSecrets } from "../trace/redact.js";
|
|
51
51
|
/** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
52
52
|
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
53
|
-
* 活流腿(bg/SSE,coordinator.runWithContext
|
|
54
|
-
* 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade
|
|
55
|
-
* 原语义逐字保留(此时执行只会拿到 headless
|
|
56
|
-
*
|
|
57
|
-
|
|
53
|
+
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
54
|
+
* RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
|
|
55
|
+
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(此时执行只会拿到 headless
|
|
56
|
+
* 空答,park 才是把问题送到人面前的那条腿)。
|
|
57
|
+
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
|
|
58
|
+
* ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
|
|
59
|
+
* 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
|
|
60
|
+
* 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker)
|
|
61
|
+
* 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整
|
|
62
|
+
* 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。 */
|
|
63
|
+
export function createDurableQuestionGate(live) {
|
|
58
64
|
if (live === undefined)
|
|
59
65
|
return createDurableQuestionPolicy();
|
|
60
66
|
return {
|
|
@@ -736,7 +742,7 @@ export function createResolveSpec(ctx) {
|
|
|
736
742
|
// policy is LIVE-AWARE when the QuestionCoordinator is wired — a live-stream leg adjudicates `allow`
|
|
737
743
|
// (the live human answers over the stream) instead of unconditionally parking; see durableQuestionPolicy.
|
|
738
744
|
toolPolicy: durableEnabled
|
|
739
|
-
? combinePolicies(
|
|
745
|
+
? combinePolicies(createDurableQuestionGate(liveQuestionFace), createDurableAskPolicy({
|
|
740
746
|
requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
|
|
741
747
|
// The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
|
|
742
748
|
// durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
|
|
@@ -812,7 +818,13 @@ export function createResolveSpec(ctx) {
|
|
|
812
818
|
// per-task across suspend is a larger, separate decision that must cover the approval baseline too — tracked
|
|
813
819
|
// for center (adversarial-review HIGH-1), deliberately NOT a governance-only partial freeze here (that would
|
|
814
820
|
// be an inconsistent false-safety: approval would still be live).
|
|
815
|
-
|
|
821
|
+
// #153([2713] 分单②):MANUAL_MODE_SHELL_GATE 在此无条件施加(与 autonomy 同拍)——部署旋钮不再
|
|
822
|
+
// 依赖客户端 settings 在场性/模式臂/host lane(三重条件曾让 print/headless 腿与沙箱 lane 全静默失效)。
|
|
823
|
+
const governedBase = applyRuntimeGovernance(spec, {
|
|
824
|
+
autonomy: config.autonomy,
|
|
825
|
+
commandPolicy: config.commandPolicy,
|
|
826
|
+
...(config.manualModeShellGate ? { manualModeShellGate: config.manualModeShellGate } : {}),
|
|
827
|
+
});
|
|
816
828
|
// Fold the client's per-request settings stamp onto the governed spec, TIGHTEN-ONLY (deny-wins) — AFTER
|
|
817
829
|
// the approval baseline + operator governance, so the order is deployment ⊇ operator ⊇ client (each can only
|
|
818
830
|
// narrow). tightenTaskSpec THROWS if the client settings would LOOSEN a stricter baseline → a 422 client error
|
|
@@ -890,10 +902,8 @@ export function createResolveSpec(ctx) {
|
|
|
890
902
|
// (deny 恒赢 —— 豁免/acceptDirs 越不过;集合取舍 core 成文,server 只透传 config 旋钮:
|
|
891
903
|
// 缺省 = core RECOMMENDED_SENSITIVE_PATTERNS,SENSITIVE_WRITE_PATTERNS 显式替换/off 关闭)。
|
|
892
904
|
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
893
|
-
// [1557]§四
|
|
894
|
-
//
|
|
895
|
-
// auto/acceptEdits 分支照旧不碰 TaskSpec.shellGate,零行为变化。
|
|
896
|
-
...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
|
|
905
|
+
// [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
|
|
906
|
+
// 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
|
|
897
907
|
// [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
|
|
898
908
|
// remember="session" 的授予、ask 政策层探针同店同 canonical toolName 键空间)——「本会话不再询问」
|
|
899
909
|
// 对 fs-write 门同 turn 即时生效(父+继承子任务,ask 路径不再重入)。has() reject ⇒ core 按
|
|
@@ -933,16 +943,16 @@ export function createResolveSpec(ctx) {
|
|
|
933
943
|
throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
|
|
934
944
|
}
|
|
935
945
|
}
|
|
936
|
-
// subagent-hands follow-on (adversarial-review finding):
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
+
// subagent-hands follow-on (adversarial-review finding): a read-only (permissionMode=plan) parent that mounts
|
|
947
|
+
// the delegation tool used to let the CHILD get WRITABLE hands — reachable once subRunner grew an
|
|
948
|
+
// executionEnvFactory — which defeats plan mode's read-only contract. So a hands-read-only run mounts no
|
|
949
|
+
// delegation tool at all.
|
|
950
|
+
// 🔴 复审 2026-08-05 亲验安装包(core 5.13.x)后修口:当年那句「core 不把 handsReadOnly/shellGate 传给
|
|
951
|
+
// 子任务」**已过期** —— 子 spec 现在两者都继承(subagent.js / prepare-task.js 的 inheritedGateForChildren,
|
|
952
|
+
// 引文在 stripDelegationTools 的注释里)。本行因此是同向第二层(更严不更松),不是唯一防线;要拆得单独决定。
|
|
953
|
+
// `stripDelegationTools`(runtime-governance.ts,过滤器本体在那里被测钉住)按 core 单源常量
|
|
954
|
+
// `DEFAULT_SUBAGENT_TOOL_NAME`(="Agent")RAW 比对——5.0.0 RB-476 退役了折叠面,旧名 "Task"/"Fork" 在
|
|
955
|
+
// roster 层就 miss、不再需要归一(常量引用本身即防当年 1.202 改名时裸字符串失配的那类 fail-open)。
|
|
946
956
|
if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
|
|
947
957
|
governed = { ...governed, tools: stripDelegationTools(governed.tools) };
|
|
948
958
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
|
|
40
40
|
* importers to update, which was done in the same commit.
|
|
41
41
|
*/
|
|
42
|
-
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
42
|
+
export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
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 } from "./skills-mcp.js";
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* across a rename of this file's *contents*; a path rename (this move, 2026-07-29) does require the
|
|
40
40
|
* importers to update, which was done in the same commit.
|
|
41
41
|
*/
|
|
42
|
-
export { fetchEffective, fetchPrincipalCaps, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
42
|
+
export { fetchEffective, fetchPrincipalCaps, fetchPrincipalOrgMemory, ConfigCenterHttpError, fetchSkillContent, fetchPromptArtifact, fetchPromptBlob, } from "./http-client.js";
|
|
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 } from "./skills-mcp.js";
|
|
@@ -41,6 +41,25 @@ export declare function fetchPrincipalCaps(baseUrl: string, token: string, princ
|
|
|
41
41
|
orgMemory?: unknown;
|
|
42
42
|
etag?: string;
|
|
43
43
|
} | null>;
|
|
44
|
+
/**
|
|
45
|
+
* design/170 件A —— 读 `?principal=` 响应里的 org 记忆解析段(org 目录腿的取数口)。
|
|
46
|
+
*
|
|
47
|
+
* 🔬 **为什么不复用 {@link fetchPrincipalCaps}**(复审车 D-2 修):C3「两张表失败域互不干扰」是在档
|
|
48
|
+
* 裁定,registry-core 的 `PrincipalCapsWire` 也正是为此把 org 段留成 `unknown`(「在本 schema 里校验
|
|
49
|
+
* 会把 org 段坏形连坐进 caps 腿」)。但隔离此前只做了**单向**:org 段坏不连坐 caps,caps 段坏却整体
|
|
50
|
+
* 连坐 org —— 治理面 `runtimeCaps`/`budget` 的一次值漂移会让整封 safeParse 失败、函数 throw,于是一个
|
|
51
|
+
* **形状完全正确**的 org 授权段读不出来,目录记 `fetch_failed`、准入铸瞬时拒。治理面的 fail-closed
|
|
52
|
+
* 极性是对的(它管的是「别静默放松治理」),但它不该决定 org 授权面的可用性。
|
|
53
|
+
*
|
|
54
|
+
* 所以这条腿用 {@link PrincipalOrgMemoryEnvelope} 只读它真正消费的两个字段:治理面漂移在这里是被
|
|
55
|
+
* `.passthrough()` 原样放过的未知键,不构成本腿的故障。段本身的形状校验仍在目录腿
|
|
56
|
+
* (`PrincipalOrgMemoryWire`),本腿保持薄透传不预判。
|
|
57
|
+
*
|
|
58
|
+
* 返回值三值语义:段键缺席 ⇒ `undefined`(旧 center 能力握手 ⇒ 目录判 `section_absent` 瞬时);
|
|
59
|
+
* 段在场 ⇒ 原值(空表 = 取数成功的负结果,与缺席不同格)。条件请求不适用(本腿恒不带 etag),
|
|
60
|
+
* 因此 304 在传输层就是 fail-loud。
|
|
61
|
+
*/
|
|
62
|
+
export declare function fetchPrincipalOrgMemory(baseUrl: string, token: string, principal: string, fetchImpl?: typeof fetch, worker?: string): Promise<unknown>;
|
|
44
63
|
/** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
|
|
45
64
|
* (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
|
|
46
65
|
export declare class ConfigCenterHttpError extends Error {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config-center HTTP client — the transport calls the service makes against the config-center
|
|
3
|
-
* (`fetchEffective`/`fetchPrincipalCaps`/`fetchSkillContent`/`fetchPromptArtifact
|
|
4
|
-
* the shared HTTP-error type.
|
|
3
|
+
* (`fetchEffective`/`fetchPrincipalCaps`/`fetchPrincipalOrgMemory`/`fetchSkillContent`/`fetchPromptArtifact`/
|
|
4
|
+
* `fetchPromptBlob`) plus the shared HTTP-error type. 后两条 `?principal=` 腿共用 `requestPrincipalView`
|
|
5
|
+
* 传输半场与 `assertPrincipalEcho` 回声核,只在**解析投影**上分家(见 `fetchPrincipalOrgMemory` 头注)。 Split out of `facade.ts` (design/158 A13, internal-lossless — the
|
|
5
6
|
* facade re-exports every symbol below unchanged).
|
|
6
7
|
*/
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
@@ -72,44 +73,19 @@ export async function fetchEffective(baseUrl, token, etag, fetchImpl = fetch, wo
|
|
|
72
73
|
* content-hashed (`p-<hash>`); an `if-none-match` poll is cheap. Mirrors `fetchEffective` (Bearer + 8s + scheme guard).
|
|
73
74
|
*/
|
|
74
75
|
export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchImpl = fetch, worker) {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
78
|
-
// `?principal=<id>[&worker=<self>]` — worker authorizes the wpt_<self> caps-only path; harmless on a full token.
|
|
79
|
-
const params = new URLSearchParams({ principal });
|
|
80
|
-
if (worker)
|
|
81
|
-
params.set("worker", worker);
|
|
82
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
|
|
83
|
-
const res = await fetchImpl(url, {
|
|
84
|
-
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
85
|
-
signal: AbortSignal.timeout(8000),
|
|
86
|
-
});
|
|
87
|
-
if (res.status === 304) {
|
|
88
|
-
// A 304 is only valid as a reply to OUR conditional. If we sent no etag and still got 304, center is
|
|
89
|
-
// misbehaving — DON'T treat it as "unchanged" (that surfaces undefined caps = no restriction = fail OPEN).
|
|
90
|
-
// Throw so the resolver fail-closes (review B). With an etag, 304 = genuine "unchanged" → null.
|
|
91
|
-
if (!etag)
|
|
92
|
-
throw new Error("config-center returned 304 to a non-conditional principal-caps request");
|
|
76
|
+
const raw = await requestPrincipalView(baseUrl, token, principal, etag, fetchImpl, worker);
|
|
77
|
+
if (raw === null)
|
|
93
78
|
return null;
|
|
94
|
-
}
|
|
95
|
-
if (!res.ok)
|
|
96
|
-
throw new Error(`config-center principal-caps HTTP ${res.status}`);
|
|
97
79
|
// 边界读=schema(clay 宪法 [2704] §2,同源锚=registry-core PrincipalCapsWire):治理面
|
|
98
80
|
// runtimeCaps/budget 严格+passthrough(值漂移整体拒 ⇒ 本函数 throw ⇒ resolver fail-close,
|
|
99
81
|
// 「outage 绝不静默 GRANT」同向);[565]/[580] fail-open 面与 orgMemory(C3 失败域隔离)在
|
|
100
82
|
// schema 里保持宽读,归一/校验仍在下方与目录腿(极性不因 schema 化翻转)。
|
|
101
|
-
const parsedBody = PrincipalCapsWire.safeParse(
|
|
83
|
+
const parsedBody = PrincipalCapsWire.safeParse(raw.json);
|
|
102
84
|
if (!parsedBody.success) {
|
|
103
85
|
throw new Error(`config-center principal-caps body failed schema validation: ${parsedBody.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ").slice(0, 600)}`);
|
|
104
86
|
}
|
|
105
87
|
const body = parsedBody.data;
|
|
106
|
-
|
|
107
|
-
// 回带 principal——在场且与请求不符 = 中间层/LB 把别人的 caps 视图递了过来,弃用整响应(throw ⇒
|
|
108
|
-
// caps 腿 fail-close、org 目录腿记 unavailable)。键缺席 = 旧 center 形,按无证据容忍(回声核只拦
|
|
109
|
-
// 「有回声且对不上」的确定性错递,不把旧形升级成故障)。
|
|
110
|
-
if (body.principal !== undefined && body.principal !== principal) {
|
|
111
|
-
throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(body.principal)} but ${JSON.stringify(principal)} was requested — discarding the whole response (cross-tenant delivery defense)`);
|
|
112
|
-
}
|
|
88
|
+
assertPrincipalEcho(body.principal, principal);
|
|
113
89
|
return {
|
|
114
90
|
runtimeCaps: body.runtimeCaps ?? null,
|
|
115
91
|
configured: Boolean(body.configured),
|
|
@@ -166,9 +142,82 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
|
|
|
166
142
|
// org 目录腿据此判 section_absent 瞬时;形状校验是 org-memory-admission 模块用 PrincipalOrgMemoryWire
|
|
167
143
|
// 的事,本腿保持薄透传不预判)。
|
|
168
144
|
...("orgMemory" in body ? { orgMemory: body.orgMemory } : {}),
|
|
169
|
-
etag:
|
|
145
|
+
etag: raw.etag, // 与提取共用件之前逐字等价:键恒在场,无 ETag 头时值为 undefined(消费端只读真值)
|
|
170
146
|
};
|
|
171
147
|
}
|
|
148
|
+
/** design/170 件A C8(响应 principal 回声核):center 两条 `?principal=` 腿的 body 都回带 principal——
|
|
149
|
+
* 在场且与请求不符 = 中间层/LB 把别的 principal 的视图递了过来,弃用整响应(throw ⇒ caps 腿
|
|
150
|
+
* fail-close、org 目录腿记 unavailable)。键缺席 = 旧 center 形,按无证据容忍(回声核只拦「有回声
|
|
151
|
+
* 且对不上」的确定性错递,不把旧形升级成故障)。两条腿共用同一道闸,不各写一遍。 */
|
|
152
|
+
function assertPrincipalEcho(echoed, principal) {
|
|
153
|
+
if (echoed !== undefined && echoed !== principal) {
|
|
154
|
+
throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(echoed)} but ${JSON.stringify(principal)} was requested — discarding the whole response`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** 两条 `?principal=` 腿共用的**传输**半场:scheme 闸 / `?principal=[&worker=]` / Bearer / 8s 超时 /
|
|
158
|
+
* 304 语义。逐字来自原 {@link fetchPrincipalCaps} 函数体,提成共用件是为了第二条腿(org 段读取)
|
|
159
|
+
* 不必抄一遍传输纪律——抄一遍就等于给同一条边界立两套会各自漂移的规矩。
|
|
160
|
+
* 返回 `null` = 合法的 304(仅当我们真的发了条件请求);否则是**解析前**的 body 原值。 */
|
|
161
|
+
async function requestPrincipalView(baseUrl, token, principal, etag, fetchImpl, worker) {
|
|
162
|
+
const scheme = new URL(baseUrl).protocol;
|
|
163
|
+
if (scheme !== "http:" && scheme !== "https:")
|
|
164
|
+
throw new Error(`SEMA_REGISTRY_URL must be http(s), got "${scheme}"`);
|
|
165
|
+
// `?principal=<id>[&worker=<self>]` — worker authorizes the wpt_<self> caps-only path; harmless on a full token.
|
|
166
|
+
const params = new URLSearchParams({ principal });
|
|
167
|
+
if (worker)
|
|
168
|
+
params.set("worker", worker);
|
|
169
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/config/effective?${params.toString()}`;
|
|
170
|
+
const res = await fetchImpl(url, {
|
|
171
|
+
headers: { authorization: `Bearer ${token}`, ...(etag ? { "if-none-match": etag } : {}) },
|
|
172
|
+
signal: AbortSignal.timeout(8000),
|
|
173
|
+
});
|
|
174
|
+
if (res.status === 304) {
|
|
175
|
+
// A 304 is only valid as a reply to OUR conditional. If we sent no etag and still got 304, center is
|
|
176
|
+
// misbehaving — DON'T treat it as "unchanged" (that surfaces undefined caps = no restriction = fail OPEN).
|
|
177
|
+
// Throw so the resolver fail-closes (review B). With an etag, 304 = genuine "unchanged" → null.
|
|
178
|
+
if (!etag)
|
|
179
|
+
throw new Error("config-center returned 304 to a non-conditional principal-caps request");
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (!res.ok)
|
|
183
|
+
throw new Error(`config-center principal-caps HTTP ${res.status}`);
|
|
184
|
+
const responseEtag = res.headers.get("etag");
|
|
185
|
+
return { json: await res.json(), ...(responseEtag !== null ? { etag: responseEtag } : {}) };
|
|
186
|
+
}
|
|
187
|
+
/** 件A C3 失败域隔离的读取投影:org 目录腿只消费两个字段——`principal`(回声核)与 `orgMemory` 段
|
|
188
|
+
* 原值。用 `PrincipalCapsWire.pick()` 取子集(schema 属主仍是 registry-core,这里不复制形状定义,
|
|
189
|
+
* `.passthrough()` 随 `_def` 一并继承)。 */
|
|
190
|
+
const PrincipalOrgMemoryEnvelope = PrincipalCapsWire.pick({ principal: true, orgMemory: true });
|
|
191
|
+
/**
|
|
192
|
+
* design/170 件A —— 读 `?principal=` 响应里的 org 记忆解析段(org 目录腿的取数口)。
|
|
193
|
+
*
|
|
194
|
+
* 🔬 **为什么不复用 {@link fetchPrincipalCaps}**(复审车 D-2 修):C3「两张表失败域互不干扰」是在档
|
|
195
|
+
* 裁定,registry-core 的 `PrincipalCapsWire` 也正是为此把 org 段留成 `unknown`(「在本 schema 里校验
|
|
196
|
+
* 会把 org 段坏形连坐进 caps 腿」)。但隔离此前只做了**单向**:org 段坏不连坐 caps,caps 段坏却整体
|
|
197
|
+
* 连坐 org —— 治理面 `runtimeCaps`/`budget` 的一次值漂移会让整封 safeParse 失败、函数 throw,于是一个
|
|
198
|
+
* **形状完全正确**的 org 授权段读不出来,目录记 `fetch_failed`、准入铸瞬时拒。治理面的 fail-closed
|
|
199
|
+
* 极性是对的(它管的是「别静默放松治理」),但它不该决定 org 授权面的可用性。
|
|
200
|
+
*
|
|
201
|
+
* 所以这条腿用 {@link PrincipalOrgMemoryEnvelope} 只读它真正消费的两个字段:治理面漂移在这里是被
|
|
202
|
+
* `.passthrough()` 原样放过的未知键,不构成本腿的故障。段本身的形状校验仍在目录腿
|
|
203
|
+
* (`PrincipalOrgMemoryWire`),本腿保持薄透传不预判。
|
|
204
|
+
*
|
|
205
|
+
* 返回值三值语义:段键缺席 ⇒ `undefined`(旧 center 能力握手 ⇒ 目录判 `section_absent` 瞬时);
|
|
206
|
+
* 段在场 ⇒ 原值(空表 = 取数成功的负结果,与缺席不同格)。条件请求不适用(本腿恒不带 etag),
|
|
207
|
+
* 因此 304 在传输层就是 fail-loud。
|
|
208
|
+
*/
|
|
209
|
+
export async function fetchPrincipalOrgMemory(baseUrl, token, principal, fetchImpl = fetch, worker) {
|
|
210
|
+
const raw = await requestPrincipalView(baseUrl, token, principal, undefined, fetchImpl, worker);
|
|
211
|
+
// etag 恒缺席 ⇒ 304 已在传输层 throw;这一支只为类型收口(不可达)。
|
|
212
|
+
if (raw === null)
|
|
213
|
+
throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
|
|
214
|
+
const parsed = PrincipalOrgMemoryEnvelope.safeParse(raw.json);
|
|
215
|
+
if (!parsed.success) {
|
|
216
|
+
throw new Error(`config-center org-memory section failed schema validation: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ").slice(0, 600)}`);
|
|
217
|
+
}
|
|
218
|
+
assertPrincipalEcho(parsed.data.principal, principal);
|
|
219
|
+
return parsed.data.orgMemory;
|
|
220
|
+
}
|
|
172
221
|
/** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
|
|
173
222
|
* (e.g. translate a 404 to "unknown hash → undefined") WITHOUT parsing the message string. */
|
|
174
223
|
export class ConfigCenterHttpError extends Error {
|
package/dist/config-types.d.ts
CHANGED
|
@@ -717,14 +717,23 @@ export interface ServiceConfigFlat {
|
|
|
717
717
|
* recommended set (not a merge); `off` or an empty value = disabled (empty array); unset = core's recommended
|
|
718
718
|
* set. Deny composes deny-wins, so a session exemption / acceptEdits auto-allow never crosses it. */
|
|
719
719
|
sensitiveWritePatterns: string[];
|
|
720
|
-
/** [1557]§四 opt-in (cli[1555]② finding, core[1556] suggested mechanism): CC manual-family permission
|
|
721
|
-
* (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched
|
|
722
|
-
* a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE`
|
|
723
|
-
* deployment into ALSO tightening `TaskSpec.shellGate`
|
|
724
|
-
*
|
|
720
|
+
/** [1557]§四 opt-in (cli[1555]② finding, core[1556] suggested mechanism): the CC manual-family permission
|
|
721
|
+
* modes (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched
|
|
722
|
+
* Bash — a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE`
|
|
723
|
+
* opts a deployment into ALSO tightening `TaskSpec.shellGate` (core `bashReversibilityProbe` under
|
|
724
|
+
* `"classify"` asks on constructively-irreversible commands — shell redirects etc. — **plus** the boundary
|
|
725
|
+
* case in the 🔴 note below; it is not a "constructive-irreversibility only" filter).
|
|
726
|
+
* 🔴 **施加点 = governance 层,无条件(#153 / [2713] 分单②;搬家前的旧描述已作废)**:与 `AUTONOMY`
|
|
727
|
+
* 同拍在 `applyRuntimeGovernance`(runtime-governance.ts)里施加,**不看客户端 settings/permissionMode
|
|
728
|
+
* 的在场性、不看落在哪个模式臂、不看执行 lane**。旧家挂在 task-settings 的 default/auto/acceptEdits 三臂
|
|
729
|
+
* + host-lane fs-write wiring 上,等于让一个部署级旋钮的生死由客户端表态决定(print/headless 腿不 stamp
|
|
730
|
+
* settings ⇒ 整块跳过;bypassPermissions 臂不回 shellGate;非 host lane wiring 不建 ⇒ 沙箱 lane 全失效)。
|
|
731
|
+
* rank 合成:与 autonomy 派生值取大(`ask`→`"always"` 压过 `"classify"`),对 base 只升不降。
|
|
732
|
+
* 客户端 settings 折叠在其后、tighten-only,拿不掉这个值;SUP 路由姿态只会把它抬到 `"always"`。
|
|
725
733
|
* 🔴 **语义随 core 版本演进(2026-07-31 核)**:`"classify"` 下「纯读自动放行」这句自 core 2.7.0 起
|
|
726
734
|
* **不再无条件成立** —— RB-412 给 classifier 加了 root 边界扫描,core 内部自铸 boundary
|
|
727
|
-
* (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots,
|
|
735
|
+
* (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots+additionalReadRoots,
|
|
736
|
+
* cwd=活 handsCwd),
|
|
728
737
|
* **读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate,边界是 core 自算的)。
|
|
729
738
|
* 旧 core(<2.7.0)下仍是名字级判定=越界读放行,两者都能工作、后者更严 ⇒ floor 不因此抬。
|
|
730
739
|
* Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a
|
package/dist/config.js
CHANGED
|
@@ -911,8 +911,10 @@ function parseApprovalDomain(ctx) {
|
|
|
911
911
|
return [];
|
|
912
912
|
return trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
|
913
913
|
})(process.env.SENSITIVE_WRITE_PATTERNS),
|
|
914
|
-
// [1557]
|
|
915
|
-
// (no field on the config object at all), so
|
|
914
|
+
// [1557]§四 → #153: opt-in only — any value other than the two real gate shapes (incl. "off"/unset) stays
|
|
915
|
+
// undefined (no field on the config object at all), so `applyRuntimeGovernance` never receives the knob and
|
|
916
|
+
// THIS knob contributes nothing to the governance pass (zero behavior change from it). Note that pass can
|
|
917
|
+
// still set `TaskSpec.shellGate` on its own account — `AUTONOMY=ask` derives "always" independently.
|
|
916
918
|
...(process.env.MANUAL_MODE_SHELL_GATE === "always" || process.env.MANUAL_MODE_SHELL_GATE === "classify"
|
|
917
919
|
? { manualModeShellGate: process.env.MANUAL_MODE_SHELL_GATE }
|
|
918
920
|
: {}),
|
|
@@ -200,8 +200,10 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
200
200
|
mcpElicitation: Boolean(deps.elicitation),
|
|
201
201
|
// §4④: AskUserQuestion LIVE HITL — true ONLY when the onQuestion producing path is wired
|
|
202
202
|
// (ASK_QUESTION_ENABLED). Same SCOPE as mcpElicitation (streaming + durable legs; a synchronous /v1/tasks or a
|
|
203
|
-
// sub-task with no live stream headless-defaults — honest, not a hang).
|
|
204
|
-
//
|
|
203
|
+
// sub-task with no live stream headless-defaults — honest, not a hang). #152: on a DURABLE deployment the
|
|
204
|
+
// disconnected-human leg (checkpoint park + /decide) is no longer unconditional — it is what a leg with NO
|
|
205
|
+
// reachable live face adjudicates to, while a live-stream leg asks the human over the stream instead. So this
|
|
206
|
+
// flag being true also means "a live leg's question does NOT park"; a leg with no live face still parks.
|
|
205
207
|
askUserQuestion: Boolean(deps.question),
|
|
206
208
|
// [816]/[820]②: live tool-approval HITL — true ONLY when the onAsk producing path is wired
|
|
207
209
|
// (TOOL_APPROVAL_ENABLED). LIVE sync-stream leg only; since [879] G1 (OnAsk 三值化) every other leg
|
|
@@ -21,6 +21,37 @@ function sseData(res, payload) {
|
|
|
21
21
|
const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
|
|
22
22
|
res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* 同步提交腿的**唯一**应答口:`retryAfterSec` 在体里 ⇒ 标准 `retry-after` 头同值在头里。
|
|
26
|
+
*
|
|
27
|
+
* 🔴 为什么必须是同一个口(复审车 C,2026-08-05):`memory.admission_required`(503)与
|
|
28
|
+
* `usage.window_exhausted`(429)是同族「等一会儿再来」,而后者在 `http/server.ts` 的 pre-admission 门里
|
|
29
|
+
* **头体双给**。本腿此前只给体——但**自动重试引擎读的是头**:SDK `Transport.request` 取
|
|
30
|
+
* `parseRetryAfter(res.headers.get("retry-after"))`,退避睡 `retryAfterMs ?? backoff(attempt)`,而 503 在
|
|
31
|
+
* `RETRYABLE_STATUS` 内。头缺席 ⇒ 引擎回落 250ms/500ms 通用退避,默认 `maxRetries: 2` 的三次尝试**全部**
|
|
32
|
+
* 落在 core 的不可用窗内(core 侧 `Math.max(backoffMs, 1000)`),提交方在 1 秒内烧光重试预算然后报错 ——
|
|
33
|
+
* 这个提示存在的唯一目的正好被绕过。头体同源(体有才给、值逐字相同)也让「体说等 11 秒、头说别的」不可能。
|
|
34
|
+
*
|
|
35
|
+
* 放在应答口而不是铸体处:同步腿的 4xx/5xx **不进** idem 缓存(谓词 `status < 400`),但并发同键的调用方会
|
|
36
|
+
* 被 `peek` 喂在飞 promise 的**同一份**结果 —— 那条腿不重跑铸体处的代码,只走这里。
|
|
37
|
+
*/
|
|
38
|
+
function sendSubmitResult(res, resp) {
|
|
39
|
+
const body = resp.body;
|
|
40
|
+
// 两个判定各司其职,都不能省:`Object.hasOwn` 管**语义**——`in` 认原型链,而 `JSON.stringify` 只序列化
|
|
41
|
+
// 自有可枚举键,只用 `in` 就存在「头说等 11 秒、体里根本没这个键」的形(继承来的键足以点亮头);`in` 管
|
|
42
|
+
// **窄化**——本仓 lib 里的 `Object.hasOwn` 没有类型谓词重载,少了它就得写断言(边界禁项)。
|
|
43
|
+
// 🔴 诚实边界:这两个判定合起来**不等于**「JSON 里一定看得见」的通用证明(不可枚举自有键、数组上的具名键
|
|
44
|
+
// 都能同时通过而不被序列化)。本函数的真保证是**受控生产者**级的:本腿流经这里的应答体形状不止一种
|
|
45
|
+
// (200 的 `stripCheckpointToken(result)` 是引擎对象、409 是冲突材料……),而其中**唯一**带
|
|
46
|
+
// `retryAfterSec` 的是下面那个 memory.admission_required 臂里的对象字面量。写死这条而不是宣称一个更强的
|
|
47
|
+
// 不变式 —— 宣称过强正是「过期声明」的铸法。
|
|
48
|
+
if (typeof body === "object" && body !== null && Object.hasOwn(body, "retryAfterSec") && "retryAfterSec" in body) {
|
|
49
|
+
const sec = body.retryAfterSec;
|
|
50
|
+
if (typeof sec === "number" && Number.isFinite(sec))
|
|
51
|
+
res.setHeader("retry-after", String(sec));
|
|
52
|
+
}
|
|
53
|
+
sendJson(res, resp.status, resp.body);
|
|
54
|
+
}
|
|
24
55
|
export async function handleTasks(req, res, url, ctx) {
|
|
25
56
|
const miss = { fell: false };
|
|
26
57
|
await handleTasksBody(req, res, url, ctx, miss);
|
|
@@ -70,7 +101,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
70
101
|
}
|
|
71
102
|
else {
|
|
72
103
|
const resp = await cached;
|
|
73
|
-
|
|
104
|
+
sendSubmitResult(res, resp); // 头体同源(见 sendSubmitResult 顶注:并发同键的这条腿不重跑铸体处)
|
|
74
105
|
}
|
|
75
106
|
return;
|
|
76
107
|
}
|
|
@@ -746,8 +777,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
746
777
|
const withElicit = deps.elicitation
|
|
747
778
|
? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
|
|
748
779
|
: streamBody;
|
|
780
|
+
// #152 复审 A1:`deliverable` 是 durable 部署上 park-vs-live 判决的投递面谓词(见 question.ts 该键注)。
|
|
781
|
+
// 本腿的投递面就是这条 SSE 连接,而 ①b detach 车道下客户端断连**不 abort、不结束这条腿**——run 继续在
|
|
782
|
+
// 本 runWithContext 作用域里跑,emitAsk 往已死 socket 写被静默丢弃(question 帧无 ledgerSink 双写)。
|
|
783
|
+
// 谓词让判决面此时如实读作「无活流」⇒ durable park(运维可经 /decide 补答),而不是 allow 后挂满 ttl
|
|
784
|
+
// 以空答喂模型。断连发生在判决之后的窗口仍落 ttl 空答(与 elicit/approval 同栏,各自 TTL 语义)。
|
|
749
785
|
const withQuestion = () => deps.question
|
|
750
|
-
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, withElicit)
|
|
786
|
+
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal, deliverable: () => !res.writableEnded && !res.destroyed }, withElicit)
|
|
751
787
|
: withElicit();
|
|
752
788
|
// [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
|
|
753
789
|
// (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
|
|
@@ -1116,7 +1152,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1116
1152
|
return { status: 200, body: stripCheckpointToken(result) };
|
|
1117
1153
|
};
|
|
1118
1154
|
const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400); // cache only success-range outcomes (a completed/failed BILLED result, replay to avoid double-bill); never a session-active 409 or a resume-at 4xx (billed nothing — a retry must re-check)
|
|
1119
|
-
|
|
1155
|
+
sendSubmitResult(res, resp); // 体里有 retryAfterSec ⇒ 标准 retry-after 头同值(见 sendSubmitResult 顶注)
|
|
1120
1156
|
}
|
|
1121
1157
|
return;
|
|
1122
1158
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -1396,10 +1396,16 @@ export function createHttpServer(rawDeps) {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
// Durable-ask request/state coherence (TC-5.4), all pre-CAS so a mismatched request never consumes the
|
|
1398
1398
|
// checkpoint:
|
|
1399
|
-
// - approving a pending AskUserQuestion REQUIRES body.answer. Without it the
|
|
1400
|
-
//
|
|
1401
|
-
//
|
|
1402
|
-
//
|
|
1399
|
+
// - approving a pending AskUserQuestion REQUIRES body.answer. Without it the redeemed call executes against
|
|
1400
|
+
// whatever question face the rebuilt spec has, and the operator's question is unrecoverably LOST (checkpoint
|
|
1401
|
+
// consumed, no answer ever delivered). Fail the request instead; the operator retries with the answer.
|
|
1402
|
+
// 🔴 #152: this check is now the ONLY one on this route. core has its own pre-CAS refusal for
|
|
1403
|
+
// "approve a content-ask with no answer" (runtask.js: `taskConfig.onQuestion ?? deps.onQuestion` is
|
|
1404
|
+
// `undefined` or QUESTION_AWAITS_RESUME), but on an ASK_QUESTION_ENABLED deployment the spec no longer
|
|
1405
|
+
// stamps the sentinel and `deps.onQuestion` is the coordinator, so core's condition is never met and it
|
|
1406
|
+
// does NOT refuse. Without the guard below the resume would execute the question on a leg with no live
|
|
1407
|
+
// context, take the coordinator's empty "no human" default, and consume the checkpoint. Do not delete it
|
|
1408
|
+
// on the belief that core still catches this (parked twin: src/parked-decide.ts).
|
|
1403
1409
|
// - an answer is only meaningful on an approve of a question gate; on a deny or a non-question gate it
|
|
1404
1410
|
// signals the operator is deciding a different pending action than the one parked — reject, don't guess.
|
|
1405
1411
|
if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
|
package/dist/leader/wire.js
CHANGED
|
@@ -445,6 +445,10 @@ export function createLeaderRunner(cfg) {
|
|
|
445
445
|
if (cfg.durable) {
|
|
446
446
|
const d = cfg.durable;
|
|
447
447
|
durableSpec = {
|
|
448
|
+
// AskUserQuestion 判决:本腿是 resolve-spec `createDurableQuestionGate` 单一属主声明的**登记豁免**
|
|
449
|
+
// (复审车B 上抛/属主裁定 2026-08-05)——leader worker 无活体问答面可装(不是 HTTP 活流腿,
|
|
450
|
+
// QuestionCoordinator 的 ALS 永不覆盖到这里),core 原形+下方 sentinel 即完整正确语义,且 leader
|
|
451
|
+
// 子系统不 import boot 装配层(分层)。⚠️ 若 leader 腿将来接活体面,必须改调该工厂并撤本注。
|
|
448
452
|
toolPolicy: combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({ requireApproval: d.requireApproval, ...(d.deny ? { deny: d.deny } : {}), ...(d.autoBudget ? { autoBudget: d.autoBudget } : {}), ...(d.neverAuto ? { neverAuto: d.neverAuto } : {}) })),
|
|
449
453
|
checkpointStore: d.checkpointStore,
|
|
450
454
|
durableApproval: { scope: "_leader", ...(d.ttlMs ? { ttlMs: d.ttlMs } : {}) },
|
package/dist/main.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies,
|
|
4
|
+
import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend } from "@sema-agent/core";
|
|
5
5
|
import { createSessionTitler } from "./session-titler.js";
|
|
6
6
|
import { posIntEnv } from "./session-watch.js";
|
|
7
7
|
import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
|
|
@@ -37,7 +37,7 @@ import { createHookLlm } from "./hooks/hook-llm.js";
|
|
|
37
37
|
import { startFleetClientFromEnv } from "./fleet-client.js";
|
|
38
38
|
import { reclaimOrphansAtBoot } from "./boot-reclaim.js";
|
|
39
39
|
import { createConfigCenterRuntime } from "./boot/config-center.js";
|
|
40
|
-
import { createResolveSpec } from "./boot/resolve-spec.js";
|
|
40
|
+
import { createDurableQuestionGate, createResolveSpec } from "./boot/resolve-spec.js";
|
|
41
41
|
import { startReapers } from "./boot/reapers.js";
|
|
42
42
|
import { openStores } from "./boot/stores.js";
|
|
43
43
|
import { createBudgetAndTracing } from "./boot/budget-tracing.js";
|
|
@@ -621,10 +621,13 @@ async function main() {
|
|
|
621
621
|
// count=1,durableMandate=true(host 有 durableApproval 且 durable 车道无 live onAsk 的恒形)。
|
|
622
622
|
// exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
|
|
623
623
|
// 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
|
|
624
|
+
// #152 复审 A2:「同参重建」是本工厂自述的不变式,所以 question 门必须调 resolve-spec 的**同一个**
|
|
625
|
+
// 工厂并传同一个活体面,不能自折 core 原形——分腿改动只落一侧就是两份拷贝开始漂移的那一刻。
|
|
626
|
+
// 赎回腿今天无 question ALS ctx(/decide 路由触发,core driveParkedResume 内驱动)⇒ 判 ask,与原形同判。
|
|
624
627
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
625
628
|
? (row) => ({
|
|
626
629
|
parentConstraints: [{
|
|
627
|
-
policy: combinePolicies(
|
|
630
|
+
policy: combinePolicies(createDurableQuestionGate(question), createDurableAskPolicy({
|
|
628
631
|
requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
|
|
629
632
|
...(approvalExemptionStore && row.rootSessionId
|
|
630
633
|
? {
|
|
@@ -78,7 +78,8 @@ export interface MemoryScopeAdmissionOptions {
|
|
|
78
78
|
onOutcome?: (outcome: string, details?: Record<string, unknown>) => void;
|
|
79
79
|
}
|
|
80
80
|
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
81
|
-
* memory.admission_required
|
|
81
|
+
* **瞬时**码 memory.admission_required(503,重试可改判——与终局的 memory.admission_denied 恰好相对)
|
|
82
|
+
* 并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
82
83
|
export declare class OrgMemoryAdmissionRetryError extends Error {
|
|
83
84
|
readonly retryAfterMs: number;
|
|
84
85
|
constructor(message: string, retryAfterMs: number);
|
|
@@ -131,7 +131,8 @@ export function createOrgMemoryDirectory(opts) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
134
|
-
* memory.admission_required
|
|
134
|
+
* **瞬时**码 memory.admission_required(503,重试可改判——与终局的 memory.admission_denied 恰好相对)
|
|
135
|
+
* 并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
135
136
|
export class OrgMemoryAdmissionRetryError extends Error {
|
|
136
137
|
retryAfterMs;
|
|
137
138
|
constructor(message, retryAfterMs) {
|
|
@@ -158,6 +159,11 @@ export function createMemoryScopeAdmission(directory, opts) {
|
|
|
158
159
|
}
|
|
159
160
|
};
|
|
160
161
|
return async ({ principal, requested, requestedWriteScope }) => {
|
|
162
|
+
/** 复审 D-3(7.1.0 后修):归因面。判决记录若不带 principal,运维只看得到「某个 org scope 被拒」而
|
|
163
|
+
* 无从知道**谁**被拒——「per-principal 归因走日志」这句设计承诺(与 CHANGELOG 7.0.0 同款公开陈述)
|
|
164
|
+
* 此前在代码里没有落点。principal 缺席时**不**塞键:干净 admit 那一路必须保持 `details === undefined`
|
|
165
|
+
* (装配点据此判「无事发生不打日志」;塞个 `principal: undefined` 会把每个成功任务变成一行 warn)。 */
|
|
166
|
+
const attributed = (details) => principal === undefined ? details : { principal, ...details };
|
|
161
167
|
const requestOriginRequested = requested.filter((r) => r.origin === "request");
|
|
162
168
|
const writeIsRequestOrigin = requestedWriteScope?.origin === "request";
|
|
163
169
|
const hasRequestOrigin = requestOriginRequested.length > 0 || writeIsRequestOrigin;
|
|
@@ -203,7 +209,7 @@ export function createMemoryScopeAdmission(directory, opts) {
|
|
|
203
209
|
// audit 模式对「读面通过但 write 未显式授」的判决只记 would-narrow,返回未收窄的全额;
|
|
204
210
|
// enforce 照常收窄。ok:true 分支的 writeScope 恒为 fullWriteScope 或 null(decide 只做这两值)。
|
|
205
211
|
if (opts.mode === "audit" && decision.verdict.ok && decision.verdict.writeScope !== fullWriteScope) {
|
|
206
|
-
emit("ok", { audited: true, wouldNarrowWriteScope: true });
|
|
212
|
+
emit("ok", attributed({ audited: true, wouldNarrowWriteScope: true }));
|
|
207
213
|
return admitFull();
|
|
208
214
|
}
|
|
209
215
|
emit("ok");
|
|
@@ -212,10 +218,10 @@ export function createMemoryScopeAdmission(directory, opts) {
|
|
|
212
218
|
if (opts.mode === "audit") {
|
|
213
219
|
// Zero behavior change: a would-deny (terminal or transient) is overridden to a full, UNNARROWED admit —
|
|
214
220
|
// the real decision is only ever recorded via onOutcome, never enforced.
|
|
215
|
-
emit(decision.outcome, { ...decision.details, audited: true, wouldDeny: true, ...(decision.kind === "unavailable" ? { retryAfterMs: decision.retryAfterMs } : { reason: decision.reason }) });
|
|
221
|
+
emit(decision.outcome, attributed({ ...decision.details, audited: true, wouldDeny: true, ...(decision.kind === "unavailable" ? { retryAfterMs: decision.retryAfterMs } : { reason: decision.reason }) }));
|
|
216
222
|
return admitFull();
|
|
217
223
|
}
|
|
218
|
-
emit(decision.outcome, decision.details);
|
|
224
|
+
emit(decision.outcome, attributed(decision.details));
|
|
219
225
|
if (decision.kind === "deny")
|
|
220
226
|
return { ok: false, reason: decision.reason };
|
|
221
227
|
throw new OrgMemoryAdmissionRetryError(`org memory directory unavailable (${decision.lookupReason})`, decision.retryAfterMs);
|
package/dist/parked-decide.js
CHANGED
|
@@ -79,9 +79,12 @@ export async function decideParkedAgent(deps, req) {
|
|
|
79
79
|
const pendingTool = req.pendingAction?.toolName;
|
|
80
80
|
if (pendingTool === "AskUserQuestion" && req.decision === "approve" && req.answer === undefined) {
|
|
81
81
|
// RB-459(core 5.7.0,F1/F6 翻案后唯一剩下的 claim 前拒):问题门的 approve 必须携 answer——
|
|
82
|
-
// parked 腿无 live answering face
|
|
83
|
-
// 并空跑一次 revive;这里 claim 前拒是同一判据的免损前置,不是 server 另立规则。带 answer 的
|
|
82
|
+
// parked 腿无 live answering face,消费 claim 后只会空跑一次 revive;claim 前拒是免损前置。带 answer 的
|
|
84
83
|
// approve 与任何 deny 都放行透传,core 是语义权威(header 逐字匹配等)。
|
|
84
|
+
// 🔴 #152:本判据**不再**有 core 侧同判兜底。core 的 pre-CAS 拒条件是「`taskConfig.onQuestion ??
|
|
85
|
+
// deps.onQuestion` 为 undefined 或 QUESTION_AWAITS_RESUME」;ASK_QUESTION_ENABLED 部署上 spec 不再 stamp
|
|
86
|
+
// 哨兵、deps.onQuestion 是 coordinator,条件恒不成立 ⇒ core 放行,赎回腿会拿 coordinator 的空答当人答
|
|
87
|
+
// 并消费掉 checkpoint。这一手现在是该腿唯一的拒绝点(任务级孪生:src/http/server.ts 的 decide 路由)。
|
|
85
88
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve requires body.answer (the operator's answers[]); deny needs none", errorCode: "decide.parked_answer_required", taskId: match.handle } };
|
|
86
89
|
}
|
|
87
90
|
if (row.name === undefined) {
|
package/dist/question.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface QuestionRunContext {
|
|
|
19
19
|
owner: string | null;
|
|
20
20
|
emit: (frame: QuestionFrame) => void | Promise<void>;
|
|
21
21
|
abortSignal?: AbortSignal;
|
|
22
|
+
/** OPTIONAL reachability predicate for this leg's delivery face, consulted by {@link QuestionCoordinator.hasLiveContext}
|
|
23
|
+
* (the park-vs-live adjudication probe), NOT by `question` itself. CONSTRAINT: a leg whose face can go away WHILE the
|
|
24
|
+
* leg keeps running MUST supply it — `/v1/tasks/stream` under `x-detach-on-disconnect: true` keeps executing inside
|
|
25
|
+
* this ALS scope after the client left, and `emit` there writes to a dead socket and is silently dropped (question
|
|
26
|
+
* frames have no durable ledger twin, unlike SendUserFile's ledgerSink). Without the predicate the adjudication would
|
|
27
|
+
* read "live" and hand the leg to a face nobody can reach. ABSENT ⇒ always reachable, which is exactly right for the
|
|
28
|
+
* background leg (emit appends to the durable events tail, readable whenever a client attaches). */
|
|
29
|
+
deliverable?: () => boolean;
|
|
22
30
|
}
|
|
23
31
|
export interface QuestionThrottle {
|
|
24
32
|
/** Max concurrent in-flight questions per run leg (parallel tool calls can each ask). Breach ⇒ headless default. */
|
|
@@ -67,10 +75,14 @@ export declare class QuestionCoordinator {
|
|
|
67
75
|
};
|
|
68
76
|
/** Test/observability hook: number of currently-parked questions. */
|
|
69
77
|
pendingCount(): number;
|
|
70
|
-
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion
|
|
71
|
-
*
|
|
78
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
|
|
79
|
+
* 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
|
|
72
80
|
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
73
|
-
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
81
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
82
|
+
* 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
|
|
83
|
+
* 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
|
|
84
|
+
* 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
|
|
85
|
+
* fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
|
|
74
86
|
hasLiveContext(): boolean;
|
|
75
87
|
private countersFor;
|
|
76
88
|
}
|
package/dist/question.js
CHANGED
|
@@ -192,12 +192,26 @@ export class QuestionCoordinator {
|
|
|
192
192
|
pendingCount() {
|
|
193
193
|
return this.pending.size;
|
|
194
194
|
}
|
|
195
|
-
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion
|
|
196
|
-
*
|
|
195
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条**投递得到人的**
|
|
196
|
+
* 活流腿上(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade、durable resume 驱动=false)。
|
|
197
197
|
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
198
|
-
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
198
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。
|
|
199
|
+
* 🔴 判据是「投递面此刻可达」而不只是「ALS 在场」(复审 A1):detach 腿断连后 run 仍在本作用域里跑,
|
|
200
|
+
* 只判 ALS 会把一条谁也收不到的问题判成 allow(挂满 ttl 后以空答喂模型),而 park 才是它该走的腿。
|
|
201
|
+
* 谓词缺席 ⇒ 恒可达(后台腿的 durable events tail 语义)。谓词本身抛错按不可达处理:判决面
|
|
202
|
+
* fail-closed 到 park(park 可由运维补答,空答不可回收)。 */
|
|
199
203
|
hasLiveContext() {
|
|
200
|
-
|
|
204
|
+
const ctx = this.als.getStore();
|
|
205
|
+
if (ctx === undefined)
|
|
206
|
+
return false;
|
|
207
|
+
if (ctx.deliverable === undefined)
|
|
208
|
+
return true;
|
|
209
|
+
try {
|
|
210
|
+
return ctx.deliverable();
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
201
215
|
}
|
|
202
216
|
countersFor(taskId) {
|
|
203
217
|
let rc = this.counters.get(taskId);
|
|
@@ -35,10 +35,17 @@ export type CommandRule = {
|
|
|
35
35
|
* fail-open(那个教训的解=引用常量,不是折叠)。 */
|
|
36
36
|
export declare function isDelegationToolName(name: string): boolean;
|
|
37
37
|
/** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter —
|
|
38
|
-
* not just the name predicate — is pinned by tests):
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation
|
|
39
|
+
* tool at all.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly`
|
|
42
|
+
* (`dist/agents/subagent.js:1501` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume
|
|
43
|
+
* 腿 `:405` 同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以
|
|
44
|
+
* 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松):
|
|
45
|
+
* 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将
|
|
46
|
+
* 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。
|
|
47
|
+
*
|
|
48
|
+
* Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */
|
|
42
49
|
export declare function stripDelegationTools<T extends {
|
|
43
50
|
name: string;
|
|
44
51
|
}>(tools: readonly T[]): T[];
|
|
@@ -86,15 +93,23 @@ export declare function compileCommandPolicy(rules: CommandRule[] | undefined):
|
|
|
86
93
|
*/
|
|
87
94
|
export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial<TaskSpec>;
|
|
88
95
|
/**
|
|
89
|
-
* Apply the operator's runtime governance (autonomy + commandPolicy) onto a base
|
|
90
|
-
* SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
91
|
-
* baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
92
|
-
* (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
93
|
-
* field — surfacing a misconfiguration loudly instead of silently
|
|
94
|
-
* when there is nothing to apply
|
|
96
|
+
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
97
|
+
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
98
|
+
* override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
99
|
+
* `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
100
|
+
* override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
|
|
101
|
+
* weakening the gate. Returns `base` untouched when there is nothing to apply.
|
|
102
|
+
*
|
|
103
|
+
* #153([2713] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
|
|
104
|
+
* (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
|
|
105
|
+
* 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
|
|
106
|
+
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
107
|
+
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
108
|
+
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
95
109
|
*/
|
|
96
110
|
export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
|
|
97
111
|
autonomy?: Autonomy;
|
|
98
112
|
commandPolicy?: CommandRule[];
|
|
113
|
+
manualModeShellGate?: "always" | "classify";
|
|
99
114
|
}): TaskSpec;
|
|
100
115
|
//# sourceMappingURL=runtime-governance.d.ts.map
|
|
@@ -32,10 +32,17 @@ export function isDelegationToolName(name) {
|
|
|
32
32
|
return name === DEFAULT_SUBAGENT_TOOL_NAME;
|
|
33
33
|
}
|
|
34
34
|
/** The hands-read-only DELEGATION STRIP (the main.ts governed-spec site, extracted here so the actual filter —
|
|
35
|
-
* not just the name predicate — is pinned by tests):
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* not just the name predicate — is pinned by tests): a read-only (plan-mode) run does not mount a delegation
|
|
36
|
+
* tool at all.
|
|
37
|
+
*
|
|
38
|
+
* 🔴 原始理由已被上游收编(2026-08-05 亲验安装包 core 5.13.x):子任务 spec 现在**继承** `handsReadOnly`
|
|
39
|
+
* (`dist/agents/subagent.js:1501` 的 `...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {})`,resume
|
|
40
|
+
* 腿 `:405` 同款)与 `shellGate`(`dist/core/runner/prepare-task.js` 的 `inheritedGateForChildren`),所以
|
|
41
|
+
* 「子任务会拿到可写的手」这个当年的缺口在 core 侧已经补上。本 strip 因此是**同向的第二层**(更严,不更松):
|
|
42
|
+
* 它保证 read-only 一档下连委派工具面都不铸,与继承是否存在无关。拆掉它是一次行为变更(read-only 任务将
|
|
43
|
+
* 重新能委派),需要单独决定——不要因为「上游修好了」就顺手删。
|
|
44
|
+
*
|
|
45
|
+
* Identity (the same array) when nothing matches, so the caller's spec-object churn stays minimal. */
|
|
39
46
|
export function stripDelegationTools(tools) {
|
|
40
47
|
const kept = tools.filter((t) => !isDelegationToolName(t.name));
|
|
41
48
|
return kept.length === tools.length ? tools : kept;
|
|
@@ -204,19 +211,44 @@ export function autonomyOverrides(autonomy) {
|
|
|
204
211
|
return {};
|
|
205
212
|
}
|
|
206
213
|
}
|
|
214
|
+
/** core `tighten-task-spec` 的 SHELL_GATE_RANK 镜像(单一属主:#153 搬家后本文件是 server 侧唯一副本;
|
|
215
|
+
* task-settings 的旧副本随 settings 折叠臂一并拆除)。tightenTaskSpec 的契约是「override 自身必须收紧」
|
|
216
|
+
* 而非「取两者较严」——低于 base 的 override 直接 throw,所以施加前要按 rank 只升不降。 */
|
|
217
|
+
const SHELL_GATE_RANK = { off: 0, classify: 1, always: 2 };
|
|
207
218
|
/**
|
|
208
|
-
* Apply the operator's runtime governance (autonomy + commandPolicy) onto a base
|
|
209
|
-
* SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
210
|
-
* baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
211
|
-
* (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
212
|
-
* field — surfacing a misconfiguration loudly instead of silently
|
|
213
|
-
* when there is nothing to apply
|
|
219
|
+
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
220
|
+
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
221
|
+
* override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
222
|
+
* `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
223
|
+
* override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
|
|
224
|
+
* weakening the gate. Returns `base` untouched when there is nothing to apply.
|
|
225
|
+
*
|
|
226
|
+
* #153([2713] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
|
|
227
|
+
* (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
|
|
228
|
+
* 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
|
|
229
|
+
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
230
|
+
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
231
|
+
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
214
232
|
*/
|
|
215
233
|
export function applyRuntimeGovernance(base, governance) {
|
|
216
234
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
|
217
235
|
const coarse = compileCommandPolicy(governance.commandPolicy);
|
|
218
236
|
if (coarse)
|
|
219
237
|
overrides.toolPolicy = coarse;
|
|
238
|
+
if (governance.manualModeShellGate !== undefined) {
|
|
239
|
+
const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
|
|
240
|
+
? overrides.shellGate
|
|
241
|
+
: governance.manualModeShellGate;
|
|
242
|
+
// base 已更严 ⇒ 省略,让 base 原样保留(省略=行为等价,不 throw)。当前**装配上不可达、纯防御**:
|
|
243
|
+
// resolveSpec 的 spec 字面量从不写 shellGate,governance 是本进程里第一个产它的人(SUP 路由姿态在其**后**
|
|
244
|
+
// 才叠),所以每次真实调用的 base.shellGate 都是缺席。若将来有人给 base 种上更严的值,这里省略而不是让
|
|
245
|
+
// tightenTaskSpec 因「override 更松」throw——candidate 已是 autonomy 派生值与旋钮的较大者,省略它不会
|
|
246
|
+
// 丢掉 autonomy 那一半(autonomy 只产 "always",即最高 rank,永远不会落进这一支)。
|
|
247
|
+
if (SHELL_GATE_RANK[candidate] >= SHELL_GATE_RANK[base.shellGate ?? "off"])
|
|
248
|
+
overrides.shellGate = candidate;
|
|
249
|
+
else
|
|
250
|
+
delete overrides.shellGate;
|
|
251
|
+
}
|
|
220
252
|
if (overrides.toolPolicy === undefined && overrides.handsReadOnly === undefined && overrides.shellGate === undefined) {
|
|
221
253
|
return base; // nothing to govern → avoid a needless tightenTaskSpec pass
|
|
222
254
|
}
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -181,19 +181,21 @@ export interface FsWriteGateWiring {
|
|
|
181
181
|
* now closed). Source = config `SENSITIVE_WRITE_PATTERNS` (unset ⇒ core RECOMMENDED_SENSITIVE_PATTERNS; the
|
|
182
182
|
* set curation is core's — the server passes through). Absent/empty ⇒ no sensitive policy (pre-1.295 shape). */
|
|
183
183
|
sensitivePatterns?: string[];
|
|
184
|
-
/** [1557]§四 opt-in(cli[1555]②「echo -n … > file 绕写门」缺口的部署侧补丁,core[1556] 建议):
|
|
185
|
-
* CC manual 系模式(default/auto/acceptEdits — 见 {@link deriveSettingsPolicy})額外把
|
|
186
|
-
* `TaskSpec.shellGate` 收紧到这个值,让 Bash 走同一 fs-write 审批链(core `bashReversibilityProbe`
|
|
187
|
-
* 在 `"classify"` 下只拦构造性不可逆命令——含 shell 重定向 `>`/`>>` 等,纯读命令白名单自动放行)。
|
|
188
|
-
* **Absent(缺省)= 零行为变化**(config `MANUAL_MODE_SHELL_GATE` 未设置时不传本字段,pre-此批行为
|
|
189
|
-
* byte-for-byte;这是个 tighten-only opt-in,不是默认开启——运营方需要显式选择开启)。 */
|
|
190
|
-
shellGate?: "always" | "classify";
|
|
191
184
|
}
|
|
192
185
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
193
186
|
toolPolicy?: ToolPolicy;
|
|
194
187
|
handsReadOnly?: boolean;
|
|
195
188
|
enablePlanMode?: boolean;
|
|
196
|
-
shellGate?: "always" | "classify";
|
|
197
189
|
};
|
|
190
|
+
/**
|
|
191
|
+
* Apply a client's parsed per-request settings onto a base `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link
|
|
192
|
+
* tightenTaskSpec} pass (so the permission override composes deny-wins onto the baseline — TRAP #1 — and
|
|
193
|
+
* `handsReadOnly`/`onAsk` can only narrow). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any override would
|
|
194
|
+
* LOOSEN a stricter base — surfaced by the caller as a 4xx (a client settings stamp that tries to widen is a
|
|
195
|
+
* client error, never a silent weakening). `model`/`outputStyle` are applied directly (model is the caller's
|
|
196
|
+
* choice WITHIN the catalog — the caller gates it before calling; outputStyle appends to the system prompt).
|
|
197
|
+
*
|
|
198
|
+
* Returns `base` untouched when the parsed settings project nothing onto the spec.
|
|
199
|
+
*/
|
|
198
200
|
export declare function applyTaskSettings(base: TaskSpec, settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): TaskSpec;
|
|
199
201
|
//# sourceMappingURL=task-settings.d.ts.map
|
package/dist/task-settings.js
CHANGED
|
@@ -426,18 +426,16 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
426
426
|
return { toolPolicy: compose(createWorkflowPlanDenyPolicy()), handsReadOnly: true, enablePlanMode: true };
|
|
427
427
|
case "acceptEdits": {
|
|
428
428
|
const p = compose(fsWriteGate(true), createWorkflowAskPolicy(workflowGate?.isExempt));
|
|
429
|
-
return
|
|
429
|
+
return p ? { toolPolicy: p } : {};
|
|
430
430
|
}
|
|
431
431
|
case "default":
|
|
432
432
|
case "auto": {
|
|
433
433
|
// (historical note kept: the pre-[816] impl fell through to base-only here — with no gate to attach,
|
|
434
434
|
// acceptEdits-as-loosen could not be honored. The [820] factory inverts the frame: default now ADDS an ask
|
|
435
435
|
// gate and acceptEdits ADDS a narrower one, both tighten-safe under the deny-wins fold.)
|
|
436
|
-
// [1557]
|
|
437
|
-
// gate on the SAME "CC manual" modes — closes the "echo > file bypasses the write gate" hole (cli[1555]②)
|
|
438
|
-
// when the deployment explicitly opts in (config MANUAL_MODE_SHELL_GATE).
|
|
436
|
+
// ([1557]§四 的 shellGate 便车已随 #153 搬 governance 层——见 FsWriteGateWiring 尾注。)
|
|
439
437
|
const p = compose(fsWriteGate(false), createWorkflowAskPolicy(workflowGate?.isExempt));
|
|
440
|
-
return
|
|
438
|
+
return p ? { toolPolicy: p } : {};
|
|
441
439
|
}
|
|
442
440
|
case "bypassPermissions":
|
|
443
441
|
case undefined:
|
|
@@ -454,27 +452,17 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
|
454
452
|
*
|
|
455
453
|
* Returns `base` untouched when the parsed settings project nothing onto the spec.
|
|
456
454
|
*/
|
|
457
|
-
// [1557]
|
|
458
|
-
//
|
|
459
|
-
// handsReadOnly, which ORs). Red-first repro: tightenTaskSpec({shellGate:"always"}, {shellGate:"classify"})
|
|
460
|
-
// throws — so a deployment that layers autonomy=ask (→"always") UNDER this opt-in (→"classify") would 422
|
|
461
|
-
// EVERY request on that mode. Mirrors core's own SHELL_GATE_RANK (tighten-task-spec.js) — only pass the
|
|
462
|
-
// override through when it would not be a downgrade; otherwise the baseline already covers it (tightenTaskSpec's
|
|
463
|
-
// base-only branch keeps `base.shellGate` unchanged either way — omitting is behaviorally identical, never a
|
|
464
|
-
// silent weakening, just avoids the throw).
|
|
465
|
-
const SHELL_GATE_RANK = { off: 0, classify: 1, always: 2 };
|
|
455
|
+
// ([1557]§四 的 SHELL_GATE_RANK 副本与 shellGateSafe 守卫已随 #153 搬 runtime-governance ——
|
|
456
|
+
// settings 折叠不再触碰 TaskSpec.shellGate,base 上 governance 层施加的值原样透传。)
|
|
466
457
|
export function applyTaskSettings(base, settings, gate, workflowGate) {
|
|
467
|
-
const { toolPolicy, handsReadOnly, enablePlanMode
|
|
468
|
-
const shellGateSafe = shellGate !== undefined && SHELL_GATE_RANK[shellGate] >= SHELL_GATE_RANK[base.shellGate ?? "off"] ? shellGate : undefined;
|
|
458
|
+
const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
|
|
469
459
|
let next = base;
|
|
470
|
-
if (toolPolicy !== undefined || handsReadOnly !== undefined
|
|
460
|
+
if (toolPolicy !== undefined || handsReadOnly !== undefined) {
|
|
471
461
|
const overrides = {};
|
|
472
462
|
if (toolPolicy !== undefined)
|
|
473
463
|
overrides.toolPolicy = toolPolicy;
|
|
474
464
|
if (handsReadOnly !== undefined)
|
|
475
465
|
overrides.handsReadOnly = handsReadOnly;
|
|
476
|
-
if (shellGateSafe !== undefined)
|
|
477
|
-
overrides.shellGate = shellGateSafe; // [1557]§四 opt-in; rank-guarded above
|
|
478
466
|
next = tightenTaskSpec(next, overrides); // deny-wins compose + THROWS on loosen (fail-loud)
|
|
479
467
|
}
|
|
480
468
|
// R4: enablePlanMode (mount present_plan) is ADDITIVE — it widens NOTHING (a read-only plan workflow), so it rides
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.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",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"sharp": "^0.35.3"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
|
-
"@sema-agent/sdk": "^6.
|
|
71
|
+
"@sema-agent/sdk": "^6.6.0",
|
|
72
72
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
73
73
|
"@types/node": "22.10.2",
|
|
74
74
|
"@types/pg": "^8.20.0",
|