@sema-agent/server 7.4.0 → 7.5.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 +43 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +108 -11
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +132 -32
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +24 -8
- package/dist/config.d.ts +6 -1
- package/dist/config.js +56 -11
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +23 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +17 -5
- package/dist/observability/fail-open.d.ts +13 -2
- package/dist/observability/fail-open.js +15 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +57 -4
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +17 -3
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -43,7 +43,11 @@ export function createLiveCoordinators(ctx) {
|
|
|
43
43
|
parkFacility: durableEnabled,
|
|
44
44
|
});
|
|
45
45
|
// §8.4:协议**开着**却上不了场 = 合法部署形(不是配置错误)⇒ 一行 **info**(不是 warn),把原因说明白。
|
|
46
|
-
//
|
|
46
|
+
// 反过来「开关本来就没开」不打日志:显式关掉的部署自己知道为什么关,一行噪音没有信息量。
|
|
47
|
+
// ⚠️ 读数已反转:总开关默认在树上已是 ON(版本坐标见 config.ts 的 `streamApprovalConfig()` 头注),
|
|
48
|
+
// 所以这一行在**默认部署**上是常态 —— 任何没有 durable
|
|
49
|
+
// backend / park 设施的默认部署每次启动都会打一行 `stream_approval_disabled`(每次启动一行,不是每
|
|
50
|
+
// tick)。写这条判据时的前提「默认形绕开本路径」已不成立;要收紧,收紧点在本 if 而不是在开关处。
|
|
47
51
|
if (config.streamApproval.enabled && !streamApprovalGate.active) {
|
|
48
52
|
logger.info("stream_approval_disabled", {
|
|
49
53
|
reason: streamApprovalGate.reason,
|
|
@@ -55,8 +59,12 @@ export function createLiveCoordinators(ctx) {
|
|
|
55
59
|
}
|
|
56
60
|
const toolApproval = config.toolApprovalEnabled
|
|
57
61
|
? new ToolApprovalCoordinator({
|
|
58
|
-
// 协议上场 ⇒ 持久层 +
|
|
62
|
+
// 协议上场 ⇒ 持久层 + 配置窗(§7:`STREAM_ASK_WINDOW_MS` **只在开关内**生效,
|
|
59
63
|
// `DEFAULT_APPROVAL_TTL_MS` 一个字不动);不上场 ⇒ 两者都不传,窗保持既有 5min(D1 逐字不变)。
|
|
64
|
+
// ⚠️ 两臂的窗**默认已同长**:窗默认自 7.5.0 起 = 300000 = `DEFAULT_APPROVAL_TTL_MS`(clay 同裁把
|
|
65
|
+
// 「窗变短」从翻真里摘出去)。原注画的「上场 = 60s / 不上场 = 5min」这个对比在默认配置下不存在
|
|
66
|
+
// 了,别据它得出「开了协议窗会缩短」的运维结论:上场与否改变的是**持久层与协议帧**,窗只有在
|
|
67
|
+
// 部署显式设了 `STREAM_ASK_WINDOW_MS` 时才分叉。
|
|
60
68
|
...(streamApprovalGate.active ? { askStore: streamApprovalGate.askStore, ttlMs: config.streamApproval.windowMs } : {}),
|
|
61
69
|
// 余量旋钮恒传(既有键,窗长三元公式的第二项;legDeadlineMonotonic 缺席时它不参与计算)。
|
|
62
70
|
windowMarginMs: config.streamAskWindowMarginMs,
|
|
@@ -26,9 +26,15 @@ import type { RunnerDeps } from "@sema-agent/core";
|
|
|
26
26
|
import type { ServiceConfig } from "../config.js";
|
|
27
27
|
import type { Logger } from "../observability/logger.js";
|
|
28
28
|
import type { Metrics } from "../observability/metrics.js";
|
|
29
|
+
import { type OrgMemoryDirectory } from "../org-memory-admission.js";
|
|
29
30
|
export interface OrgMemoryAdmissionWiring {
|
|
30
31
|
memoryScopeAdmission: RunnerDeps["memoryScopeAdmission"];
|
|
31
32
|
deploymentMemoryScopes: RunnerDeps["deploymentMemoryScopes"];
|
|
33
|
+
/** §7 第二消费者收编:同一个目录实例也喂 memory-policy 面的 `org:` 属主门(`ServiceDeps.orgMemoryDirectory`)。
|
|
34
|
+
* **同实例**是这条收编的全部意义——「谁属于 org:acme」在一个进程里必须只有一个答案,两面共享同一份
|
|
35
|
+
* TTL 缓存/退避窗/gen 高水位;各建各的目录会让准入门与策略面在吊销窗内公开分歧。缺席=无目录源
|
|
36
|
+
* (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。 */
|
|
37
|
+
directory: OrgMemoryDirectory | undefined;
|
|
32
38
|
}
|
|
33
39
|
/** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
|
|
34
40
|
export declare function createOrgMemoryAdmissionWiring(opts: {
|
package/dist/boot/org-memory.js
CHANGED
package/dist/boot/reapers.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface ReapersCtx {
|
|
|
68
68
|
imageBakes: ImageBake | undefined;
|
|
69
69
|
worktreeReap: (() => Promise<void>) | undefined;
|
|
70
70
|
workflowNotifyGate: WorkflowNotifyGate | undefined;
|
|
71
|
+
/** [2995]/[2999] recover 的两半 opts(boot 与周期扫描共用,单点合成在 workflow-orchestration)。 */
|
|
72
|
+
workflowRecoverOpts: Parameters<WorkflowNotifyGate["recover"]>[0];
|
|
71
73
|
workflowJournalStore: ServiceWorkflowJournalStore | undefined;
|
|
72
74
|
sqlWorkflowRunStore: WorkflowRunStore | undefined;
|
|
73
75
|
workflowNotifyJournal: WorkflowNotifyJournalStore | undefined;
|
package/dist/boot/reapers.js
CHANGED
|
@@ -74,7 +74,7 @@ export function createThrottledReaperCatch(name, logger, threshold = REAPER_FAIL
|
|
|
74
74
|
}
|
|
75
75
|
/** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */
|
|
76
76
|
export function startReapers(ctx) {
|
|
77
|
-
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
|
|
77
|
+
const { config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore, rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap, workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore, backgroundAgentStore, mailboxStore, toolApproval, getRunDenySweep, } = ctx;
|
|
78
78
|
// S7 (SILENT-FALLBACK P0-d): the sweeps' return counts were discarded — an instance death that batch-fails
|
|
79
79
|
// N orphans was indistinguishable from organic failures. Count + log ONLY when a sweep flipped rows (>0),
|
|
80
80
|
// so healthy ticks stay silent. Tolerant of void-returning stores (typeof guard).
|
|
@@ -112,8 +112,15 @@ export function startReapers(ctx) {
|
|
|
112
112
|
const scratchpadSweepStaleGuard = createThrottledReaperCatch("scratchpad_sweep_stale", logger);
|
|
113
113
|
const approvalReconcileGuard = createThrottledReaperCatch("approval_reconcile", logger);
|
|
114
114
|
let approvalReconcileInFlight = false; // 车5 §8 C-8:重入守卫(邻居四先例同形——本腿时长随 PARKING 行数增长)
|
|
115
|
-
// #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描)
|
|
116
|
-
// (
|
|
115
|
+
// #151 车5(design/172 §3.0 对账收敛器 + 崩溃恢复扫描):建腿条件 = 开关开 ∧ **有 `StoreBackend`**
|
|
116
|
+
// (见下方判据行 —— 是**任意** backend,不限 SQL twins:`local` 车道也算,它的 `approvalAsk()` 是进程内
|
|
117
|
+
// `InMemoryApprovalAskStore`,扫的是内存不是库)。无 backend 或显式关 ⇒ 本腿根本不注册,零扫描。
|
|
118
|
+
// ⚠️ 运维读数已反转:总开关默认**在树上已是 ON**(发布线上落在 7.4.0 之后的下一发布;版本坐标与
|
|
119
|
+
// 「别按 /health 版本号推默认」的告诫见属主 config.ts 的 `streamApprovalConfig()` 头注),
|
|
120
|
+
// 所以「零新增扫描」从**默认态**变成了 **opt-out 态** —— 任何配了 backend 的部署(含单用户 local)
|
|
121
|
+
// 默认都会注册本腿并每 tick 扫两段(PARKING + 孤儿 STREAM_PENDING,每段
|
|
122
|
+
// `STREAM_APPROVAL_RECONCILE_BATCH` 行,默认 200);SQL twins 上这就是**真新增的周期性查询**,local 上
|
|
123
|
+
// 只是内存遍历。存量部署升级后要回到零扫描,需显式把开关关掉。
|
|
117
124
|
// 判据 1/2 的两个读口按能力面接:checkpoint 店的窄读口只有 SQL twins 有(local 车道的店没有这个面),
|
|
118
125
|
// 缺席 ⇒ 判据 1 恒不命中(诚实降级,行落 ②③④⑤),不是「假装没 checkpoint 就该 DENY」。
|
|
119
126
|
const approvalReconciler = (() => {
|
|
@@ -354,7 +361,7 @@ export function startReapers(ctx) {
|
|
|
354
361
|
// run whose in-process notify was lost, AND finalizes-as-abandoned a `running` run orphaned past the grace
|
|
355
362
|
// window (core never resumes/reaps a prior `running` row, so nothing else would). Idempotent; best-effort.
|
|
356
363
|
void workflowNotifyGate
|
|
357
|
-
?.recover(
|
|
364
|
+
?.recover(workflowRecoverOpts)
|
|
358
365
|
.then(() => workflowNotifyRecoverGuard.onSuccess(), workflowNotifyRecoverGuard.onError);
|
|
359
366
|
// SVC-2 (adversarial-review HIGH): time-based GC of the workflow_journal table (the heaviest, TaskResult-bearing
|
|
360
367
|
// one) — the per-run deleteByRun has no run-store reap hook, so this bounded sweep is what stops unbounded
|
|
@@ -73,8 +73,9 @@ export interface ResolveSpecCtx {
|
|
|
73
73
|
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
74
74
|
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
75
75
|
* RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
|
|
76
|
-
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(
|
|
77
|
-
*
|
|
76
|
+
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(#166 后无活流腿放行执行
|
|
77
|
+
* 也不会产出空答:coordinator 无 ALS ctx ⇒ 冻结 `{kind:"unavailable"}`(src/question.ts),永不悬挂;
|
|
78
|
+
* park 仍是把问题送到人面前的唯一那条腿,正当性不变、只是反事实前提换了)。
|
|
78
79
|
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
|
|
79
80
|
* ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
|
|
80
81
|
* 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* 描述的代码下移进阶段②。
|
|
19
19
|
*/
|
|
20
20
|
import { realpathSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, expandTiers, tightenTaskSpec } from "@sema-agent/core";
|
|
21
|
+
import { join, posix } from "node:path";
|
|
22
|
+
import { FileError, NodeExecutionEnv, QUESTION_AWAITS_RESUME, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, err, expandTiers, ok, tightenTaskSpec } from "@sema-agent/core";
|
|
23
23
|
import { createDurableAskPolicy } from "../approval.js";
|
|
24
24
|
import { cappedCeiling } from "../budget.js";
|
|
25
25
|
import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
|
|
@@ -53,8 +53,9 @@ import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSl
|
|
|
53
53
|
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
54
54
|
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
55
55
|
* RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
|
|
56
|
-
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(
|
|
57
|
-
*
|
|
56
|
+
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(#166 后无活流腿放行执行
|
|
57
|
+
* 也不会产出空答:coordinator 无 ALS ctx ⇒ 冻结 `{kind:"unavailable"}`(src/question.ts),永不悬挂;
|
|
58
|
+
* park 仍是把问题送到人面前的唯一那条腿,正当性不变、只是反事实前提换了)。
|
|
58
59
|
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
|
|
59
60
|
* ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
|
|
60
61
|
* 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
|
|
@@ -74,8 +75,79 @@ export function createDurableQuestionGate(live) {
|
|
|
74
75
|
},
|
|
75
76
|
};
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* 沙箱 lane 上**相对形**写目标的守卫补层用 env(codex 对抗复审 round1 finding 1,红先复现)。
|
|
80
|
+
*
|
|
81
|
+
* 缺口:沙箱 lane 的守卫策略拿不到 `rootPath`(沙箱 cwd 不是 server 能猜的,#165 裁定 1),而
|
|
82
|
+
* `DeferredSandboxPathEnv.absolutePath` 对相对形一律报错。core 的 `canonicalizeTarget` 在
|
|
83
|
+
* **absolutePath 失败**这一支不置 `unresolvedSymlink`,于是 `createSensitivePathPolicy` 走的是
|
|
84
|
+
* 「判不了就弃权」的 `allow`(dist 亲读)。写门在场时这条腿被门的 `ask` 兜住;而
|
|
85
|
+
* `bypassPermissions` / settings 缺席这几形**根本没有门**,于是 `Write(file_path: ".env")` 一路放行——
|
|
86
|
+
* 而 core 的结构化写工具会把相对形按 engine 跟踪的 cwd 解析后真写下去(fs-write.js `resolveKey`)。
|
|
87
|
+
*
|
|
88
|
+
* 补法:**同一只** `createSensitivePathPolicy` 再铸一个实例,只把「路径→canonical key」这一步换成
|
|
89
|
+
* 纯词法基准(本 env)。判定与提取(哪个参数是写目标、NotebookEdit 的 notebook_path 优先、段匹配)
|
|
90
|
+
* 全部仍是 core 的,server 侧零复刻——复刻 core 的裁决逻辑正是「同源谎」那一类错误。
|
|
91
|
+
*
|
|
92
|
+
* 三条不可动的边界:
|
|
93
|
+
* · **绝对形一律弃权**(absolutePath 报错 ⇒ canon 失败且非 unresolvedSymlink ⇒ core 判 allow):
|
|
94
|
+
* 绝对形归真身裁决那一层,#165「真身胜过名字」的裁定(域内良性软链名叫 `.ssh` 只 ask)不受影响。
|
|
95
|
+
* · **只会 deny,不会放行**:相对形自身拼写里出现的段,解析成绝对路径后仍在,所以词法命中即真命中;
|
|
96
|
+
* 反过来一条名叫 `.env` 而真身良性的相对软链会被误 deny —— 方向是 fail-closed,与守卫集语义同向。
|
|
97
|
+
* · 🔴 **覆盖面到此为止:cwd 里的守卫段看不见**(codex 对抗复审 round2,验真)。本层把相对形挂在 `/`
|
|
98
|
+
* 上,而工具是挂在 engine **活** cwd 上的:`cd .git` 后 `Write("config")` 真写 `<root>/.git/config`
|
|
99
|
+
* (命中缺省模式 `.git/config`),本层只看得到 `/config` ⇒ 不命中 ⇒ 弃权。同形的还有 cwd 落在
|
|
100
|
+
* `.ssh`/`.aws` 里再写叶名。**这一格今天无人裁决**(沙箱 lane ∧ 守卫集开 ∧ 写门不在场的
|
|
101
|
+
* bypass/settings 缺席两形)。收口只有两条路,都超出本件自裁范围,已上交:①(正解)一条「按当前
|
|
102
|
+
* 工具调用的活 cwd/env 裁决」的引擎缝——与子代继承那条残余面同一条缝;②(代价形)守卫集开启时把
|
|
103
|
+
* 沙箱 lane 的相对写目标一律 deny——那是拿「相对写从 ask 变 deny」换覆盖,有真受损方且无对应旋钮,
|
|
104
|
+
* 须部署方拍板。特征化钉见 test/task-settings.test.ts(「cwd 里的守卫段」那条),别把它读成已保护。
|
|
105
|
+
*/
|
|
106
|
+
class RelativeTargetLexicalEnv extends StubExecutionEnv {
|
|
107
|
+
/** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(= 弃权,见类注)。 */
|
|
108
|
+
absolutePath(path) {
|
|
109
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\u0000")) {
|
|
110
|
+
return Promise.resolve(err(new FileError("not_supported", "this guard layer only adjudicates RELATIVE write targets (absolute forms are adjudicated against the real sandbox filesystem)", path)));
|
|
111
|
+
}
|
|
112
|
+
return Promise.resolve(ok(posix.normalize(`/${path}`)));
|
|
113
|
+
}
|
|
114
|
+
/** 恒「不存在」⇒ core 的 `canonicalizeNewPath` 逐级回退,最终把词法归一形当 canonical key 交给段匹配。
|
|
115
|
+
* 这里绝不能报错:报错会被 core 读成 `unresolvedSymlink` 而对**每一个**相对目标 deny(含普通文件)。 */
|
|
116
|
+
exists(_path, _abortSignal) {
|
|
117
|
+
return Promise.resolve(ok(false));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
77
120
|
export function createResolveSpec(ctx) {
|
|
78
121
|
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, } = ctx;
|
|
122
|
+
// ── #177 的两件 boot 期收口(把守卫集搬到 governance 拍之后才成立的两条)────────────────────────
|
|
123
|
+
//
|
|
124
|
+
// ① 守卫集的**编译**从此每个请求都发生(旧家只在 default/auto/acceptEdits 三个模式臂里发生)。
|
|
125
|
+
// core 的 `compilePatterns` 对「一个路径段都没有」的模式(`"/"`、`"//"`)THROW,那条 throw 在旧家
|
|
126
|
+
// 还会被 `applyTaskSettings` 的 catch 误标成「settings 不能放松部署策略」的 422;搬家后它会变成
|
|
127
|
+
// **每一个任务一条 500**,且运维从错误里看不出是自己的 env 写错了。这里在 boot 期先编译一次:
|
|
128
|
+
// 非法旋钮值当场炸在启动上(与 config.ts 的 env fail-loud 同族),指名键与 core 的原因。
|
|
129
|
+
// env 只是编译期的占位(compilePatterns 不碰它),真裁决用的是每请求按 lane 铸的那一个。
|
|
130
|
+
if (config.sensitiveWritePatterns.length > 0) {
|
|
131
|
+
try {
|
|
132
|
+
createSensitivePathPolicy({ env: new StubExecutionEnv(), patterns: config.sensitiveWritePatterns });
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
throw new Error(`SENSITIVE_WRITE_PATTERNS is not a usable guard set: ${e instanceof Error ? e.message : String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ② governance 拍的 DENY 基线让 `spec.toolPolicy` 在**任何**客户端表态下都在场,于是 core 的
|
|
139
|
+
// 「write-capable hand tools are present but UNGATED」那条 onError 不再触发(core 判据是
|
|
140
|
+
// `policyLayers.length > 0`,prepare-task dist 亲读)。它此前是「部署一个门都没接」这个 misconfig
|
|
141
|
+
// 的唯一信号,而守卫集只挡那二十来个路径段、其余写目标照旧无裁决 —— 信号不能因为本件静默消失。
|
|
142
|
+
// 条件 = 两条产 toolPolicy 的腿都不在场(durable 门关 ∧ 单用户 auto-accept 基线不适用),即修前
|
|
143
|
+
// `spec.toolPolicy` 恒缺席的那一形;三个量都是 boot 常量,所以这里说一次而不是每任务说一次。
|
|
144
|
+
if (config.sensitiveWritePatterns.length > 0 && !durableEnabled && !singleUserAutoAcceptBaseline) {
|
|
145
|
+
logger.warn("tool_policy_only_sensitive_baseline", {
|
|
146
|
+
detail: "the sensitive-path DENY baseline (SENSITIVE_WRITE_PATTERNS) is this deployment's ONLY tool policy — writes outside the guarded segments are unadjudicated, and core's own UNGATED warning no longer fires because a policy is now always present",
|
|
147
|
+
remedy: "wire an approval face (DURABLE_APPROVAL / APPROVAL_REQUIRE with a store) or accept the single-user auto-accept baseline",
|
|
148
|
+
guardedPatterns: config.sensitiveWritePatterns.length,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
79
151
|
// Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
|
|
80
152
|
// bound per request); identity/session/policy stay server-side. Never taken from the request body.
|
|
81
153
|
// design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
|
|
@@ -564,8 +636,9 @@ export function createResolveSpec(ctx) {
|
|
|
564
636
|
// [1144]/[1146] (core 1.328 R2): promptProfile 双形轴直通——纯呈现轴无租户门(deferTools 姿势);
|
|
565
637
|
// 缺省不挂键(引擎缺省 simple);非法值由 HTTP 门 400,resume 重放 defensive DROP。
|
|
566
638
|
...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
|
|
567
|
-
// [2856]②(core 5.14.0):deferred 工具激活后的 schema
|
|
568
|
-
// `spec.toolMaterializeStrategy ?? env ?? "
|
|
639
|
+
// [2856]②(core 5.14.0 引入;5.15.0 BREAKING 末项默认翻 swap):deferred 工具激活后的 schema 供给
|
|
640
|
+
// 策略,per-task 位。core 侧读法是 `spec.toolMaterializeStrategy ?? env ?? "swap"`(5.16.0 dist
|
|
641
|
+
// prepare-task 亲读)⇒ **缺省不挂键**才能让进程级 env 继续说话;
|
|
569
642
|
// 挂一个「缺省值」上去会把 env 车道悄悄夺权。非法值由 HTTP 门 400,resume 重放 defensive DROP。
|
|
570
643
|
...(() => { const v = toolMaterializeStrategyFromBody(body.toolMaterializeStrategy); return v ? { toolMaterializeStrategy: v } : {}; })(),
|
|
571
644
|
// [922]①/[915]② (core 1.296): interactiveTools 三态旋钮直通——boolean 上 spec(壳 -p 恒 stamp false=
|
|
@@ -829,10 +902,49 @@ export function createResolveSpec(ctx) {
|
|
|
829
902
|
// be an inconsistent false-safety: approval would still be live).
|
|
830
903
|
// #153([2713] 分单②):MANUAL_MODE_SHELL_GATE 在此无条件施加(与 autonomy 同拍)——部署旋钮不再
|
|
831
904
|
// 依赖客户端 settings 在场性/模式臂/host lane(三重条件曾让 print/headless 腿与沙箱 lane 全静默失效)。
|
|
905
|
+
//
|
|
906
|
+
// #177([2951] issue #29):SENSITIVE_WRITE_PATTERNS 是 #153 漏下的兄弟旋钮,同病同修——operator
|
|
907
|
+
// 守卫集预铸成纯 DENY policy 在 governance 拍无条件施加(bypassPermissions/键缺席/settings 整缺席
|
|
908
|
+
// 都掀不掉;deny-wins 折叠,普通目标弃权 ⇒ 不给 bypass 加 ask 门,[820] 表第四行逐字保持)。
|
|
909
|
+
// 裁决 env/cwd 在此**单点**铸,settings 折叠的 fsWriteGate wiring 复用同一份(两处各铸一份正是
|
|
910
|
+
// 漂移的成因)。lane 分形与 cwd 三形/哨兵的完整理由成文在下方 settings 块(修5/W4/#165),此处不复述。
|
|
911
|
+
const hostSemanticsLane = !isSandboxPathAdjudicationLane(config.remoteExec?.provider);
|
|
912
|
+
const pathAdjudication = hostSemanticsLane
|
|
913
|
+
? (() => {
|
|
914
|
+
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
915
|
+
const gateCwd = sessionCwd ??
|
|
916
|
+
(config.remoteExec === undefined
|
|
917
|
+
? process.cwd()
|
|
918
|
+
: join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted")); // never created — see the settings block
|
|
919
|
+
return { env: new NodeExecutionEnv({ cwd: gateCwd }), cwd: gateCwd };
|
|
920
|
+
})()
|
|
921
|
+
: {
|
|
922
|
+
env: new DeferredSandboxPathEnv({
|
|
923
|
+
sessionId: auth?.sessionId,
|
|
924
|
+
slots: sandboxPathEnvSlots,
|
|
925
|
+
logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider }) },
|
|
926
|
+
}),
|
|
927
|
+
};
|
|
928
|
+
const sensitivePathPolicy = config.sensitiveWritePatterns.length > 0
|
|
929
|
+
? (() => {
|
|
930
|
+
const realTarget = createSensitivePathPolicy({
|
|
931
|
+
env: pathAdjudication.env,
|
|
932
|
+
patterns: config.sensitiveWritePatterns,
|
|
933
|
+
...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
|
|
934
|
+
});
|
|
935
|
+
// cwd 在场(host 形)⇒ 相对形已被 core 按 rootPath 解析进真身裁决,一层就够。
|
|
936
|
+
// cwd 缺席(沙箱形)⇒ 相对形在真身那一层是「判不了 ⇒ 弃权 allow」,而写门恰恰在
|
|
937
|
+
// bypass/settings 缺席这几形不在场 ⇒ 补一层纯词法的相对形守卫(见 RelativeTargetLexicalEnv)。
|
|
938
|
+
if (pathAdjudication.cwd !== undefined)
|
|
939
|
+
return realTarget;
|
|
940
|
+
return combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns }));
|
|
941
|
+
})()
|
|
942
|
+
: undefined;
|
|
832
943
|
const governedBase = applyRuntimeGovernance(spec, {
|
|
833
944
|
autonomy: config.autonomy,
|
|
834
945
|
commandPolicy: config.commandPolicy,
|
|
835
946
|
...(config.manualModeShellGate ? { manualModeShellGate: config.manualModeShellGate } : {}),
|
|
947
|
+
...(sensitivePathPolicy ? { sensitivePathPolicy } : {}),
|
|
836
948
|
});
|
|
837
949
|
// Fold the client's per-request settings stamp onto the governed spec, TIGHTEN-ONLY (deny-wins) — AFTER
|
|
838
950
|
// the approval baseline + operator governance, so the order is deployment ⊇ operator ⊇ client (each can only
|
|
@@ -866,8 +978,7 @@ export function createResolveSpec(ctx) {
|
|
|
866
978
|
// [820]③ scratchpadDir, hoisted from the envFacts block below so the SAME per-session dir feeds BOTH the
|
|
867
979
|
// `# Environment` fact and the fs-write gate's exemptDirs (single-source path via scratchpadDirFor inside
|
|
868
980
|
// ensureScratchpadDir). HOST-semantics lanes only + session required — rationale at the envFacts consumer.
|
|
869
|
-
//
|
|
870
|
-
const hostSemanticsLane = !isSandboxPathAdjudicationLane(config.remoteExec?.provider);
|
|
981
|
+
// (hostSemanticsLane 判别式已随 #177 上移到 governance 拍前——单一真源不变,只是搬了家。)
|
|
871
982
|
// G3([816]③/[820]③,[1840]§四):壳供 body.scratchpadDir 优先——壳(CC 形)有自己的 per-session
|
|
872
983
|
// scratchpad 约定路径,接受后事实/豁免/根围栏三处同源指向壳的目录(壳侧提示词与 server 写门不再
|
|
873
984
|
// 两个 scratchpad)。验收门 fail-closed(多租户/远程 lane/浅路径全拒收,warn 后回落自算)——
|
|
@@ -905,13 +1016,11 @@ export function createResolveSpec(ctx) {
|
|
|
905
1016
|
// (hand 工具的书面契约形)与 scratchpad exempt 均不受影响。
|
|
906
1017
|
// · provider 未设(in-process host / run-local)⇒ hands 就在本进程 cwd 跑(无 factory),process.cwd()
|
|
907
1018
|
// 正是真工作目录,保留。
|
|
908
|
-
//
|
|
1019
|
+
// 两形**共有**的一件(与 env/cwd 无关,故不随 lane 分叉——单一属主,两臂各抄一份正是漂移的成因):
|
|
909
1020
|
const gateCommon = {
|
|
910
|
-
// ③
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
// 沙箱 lane 上它按**词法** key 的路径段匹配(#156)——glob 本就只看段名,不需要真 fs。
|
|
914
|
-
...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
|
|
1021
|
+
// ③ 的 sensitive-path DENY 腿已随 #177([2951] issue #29)搬到 governance 拍(见上方
|
|
1022
|
+
// sensitivePathPolicy)——wiring 不再承运守卫集,FsWriteGateWiring.sensitivePatterns 字段一并拆除
|
|
1023
|
+
// (#153 shellGate 同形搬家:部署级旋钮不得挂在客户端表态派生腿上)。
|
|
915
1024
|
// [1557]§四 的 shellGate 旧家已随 #153 搬到 applyRuntimeGovernance(governance 层无条件施加,
|
|
916
1025
|
// 见上方 governedBase)——wiring 不再承运该旋钮,FsWriteGateWiring.shellGate 字段一并拆除。
|
|
917
1026
|
// [841]① / core 1.294 exemption seam:gate 产 ask 前查同一只 approval_exemption 店(与 decide
|
|
@@ -932,19 +1041,14 @@ export function createResolveSpec(ctx) {
|
|
|
932
1041
|
: {}),
|
|
933
1042
|
};
|
|
934
1043
|
const fsWriteGate = hostSemanticsLane
|
|
935
|
-
?
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
cwd: gateCwd,
|
|
944
|
-
...(scratchpadDir ? { scratchpadDir } : {}),
|
|
945
|
-
...gateCommon,
|
|
946
|
-
};
|
|
947
|
-
})()
|
|
1044
|
+
? {
|
|
1045
|
+
// env/cwd 构造已随 #177 上移(governance 拍前的 pathAdjudication 单点),此处复用同一份——
|
|
1046
|
+
// 守卫集 DENY 腿与写门 ask 腿在同一 env/同一 cwd 基准上裁决,两腿不可能分家漂移。
|
|
1047
|
+
env: pathAdjudication.env,
|
|
1048
|
+
...(pathAdjudication.cwd !== undefined ? { cwd: pathAdjudication.cwd } : {}),
|
|
1049
|
+
...(scratchpadDir ? { scratchpadDir } : {}),
|
|
1050
|
+
...gateCommon,
|
|
1051
|
+
}
|
|
948
1052
|
: // 沙箱 lane(#165):裁决 env = `DeferredSandboxPathEnv` 代理 —— 裁决时刻(必在 prepare 之后)
|
|
949
1053
|
// 去 per-session slot 里取本 task 的**真沙箱 env**,转发 exists/canonicalPath/fileInfo/readLink。
|
|
950
1054
|
// 于是绝对路径的 symlink 真身、存在性都是真沙箱 fs 的事实,域内软链指向守卫段照样 deny;
|
|
@@ -965,11 +1069,7 @@ export function createResolveSpec(ctx) {
|
|
|
965
1069
|
(() => {
|
|
966
1070
|
const sandboxScratchpadDir = isRemoteScratchpadLane(config.remoteExec?.provider) ? remoteScratchpadDirFor(auth?.sessionId) : undefined;
|
|
967
1071
|
return {
|
|
968
|
-
env:
|
|
969
|
-
sessionId: auth?.sessionId,
|
|
970
|
-
slots: sandboxPathEnvSlots,
|
|
971
|
-
logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider }) },
|
|
972
|
-
}),
|
|
1072
|
+
env: pathAdjudication.env, // #177 单点构造(DeferredSandboxPathEnv 代理),与守卫集 DENY 腿同一份
|
|
973
1073
|
...gateCommon,
|
|
974
1074
|
...(sandboxScratchpadDir ? { scratchpadDir: sandboxScratchpadDir } : {}),
|
|
975
1075
|
// codex 复审 B(2026-08-05,dist 亲读+实测):core 的 canonicalizeTarget 把反斜杠 UNC 形在**入口**
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (`runnerTierFrozen` 判据就取自构造那一刻的 `config.tiers`)。
|
|
10
10
|
* `getRunStore()` 是唯一的晚绑取值(runStore 在本段之后才构造),原文靠同作用域前向引用。
|
|
11
11
|
*/
|
|
12
|
-
import { type RunnerDeps, type WorkflowRunStore } from "@sema-agent/core";
|
|
12
|
+
import { type RunnerDeps, type WorkflowRunStore, type AskOutcome } from "@sema-agent/core";
|
|
13
13
|
import type { createBrain } from "../brain.js";
|
|
14
14
|
import type { buildPricing, createTracer } from "../budget.js";
|
|
15
15
|
import type { ServiceConfig } from "../config.js";
|
|
@@ -25,6 +25,28 @@ import type { QuestionCoordinator } from "../question.js";
|
|
|
25
25
|
import type { ToolApprovalCoordinator } from "../tool-approval.js";
|
|
26
26
|
import type { MemoryBackend } from "@sema-agent/core";
|
|
27
27
|
import type { MemorySyncRunner } from "../memory-sync-client.js";
|
|
28
|
+
/**
|
|
29
|
+
* `RunnerDeps.onAsk` 的装配(#168 件4:从内联闭包提为具名导出,**纯 testability 重构、零行为变化**)。
|
|
30
|
+
*
|
|
31
|
+
* 为什么值得有名字:bg / resume 两条腿**没有** per-task `spec.onAsk` 装配点(routes/runs.ts 只递 ALS 与
|
|
32
|
+
* 两个窗旋钮),它们唯一的审批接缝就是这一格。端到端测试此前只能把这一行**照抄**一份,再靠一条读源码
|
|
33
|
+
* 的正则当漂移告警 —— 那种锚只抓得到「这一行没了/改名了」,抓不到任何语义变化,而且抄件与真件从此是两
|
|
34
|
+
* 份实现。提成导出之后,测试直接取本函数,接缝与 production 是**同一个符号**。
|
|
35
|
+
*
|
|
36
|
+
* 命名(CLAUDE.md 工厂命名律):返回的是一个闭包(活对象)⇒ `create*`。
|
|
37
|
+
*
|
|
38
|
+
* 返回型**刻意窄于** `RunnerDeps["onAsk"]`(它还允许 `"allow"`/`"deny"` 两个字符串常量):本仓的接缝
|
|
39
|
+
* 恒是回调形,而 core 的 sync-first 判据只看 `onAsk !== undefined` ⇒ 字符串模式会顶掉 durable park
|
|
40
|
+
* (core backlog #73,黑板 [2897]④ / [2899] 已知悉)。窄返回型把「本仓零受迫于那一形」变成编译期事实。
|
|
41
|
+
*
|
|
42
|
+
* 语义逐字不变:[816]/[820]② 的 live tool-approval 接缝(core `resolveAsk` 读 `spec.onAsk ?? deps.onAsk`),
|
|
43
|
+
* ALS 路由同 `onQuestion` —— 被协调器 `runWithContext` 包住的腿才够得着人。[879] G1 终态(core 1.295
|
|
44
|
+
* OnAsk 三值化)下**恒**接线:回调逐 ask 时刻判活人,无附着/卡送达失败 ⇒ 返 `"unavailable"`,core 以
|
|
45
|
+
* `approverUnavailable` 回路把该 ask 交回 `suspendAsk` 走 durable park;无 park 设施的部署 core 自己
|
|
46
|
+
* fail-closed deny。协调器缺席(协议未上场)⇒ `undefined`,即「这条部署没有活人接缝」。
|
|
47
|
+
*/
|
|
48
|
+
export type RunnerDepsOnAsk = (req: Parameters<ToolApprovalCoordinator["ask"]>[0], signal?: AbortSignal) => Promise<AskOutcome>;
|
|
49
|
+
export declare function createRunnerDepsOnAsk(toolApproval: ToolApprovalCoordinator | undefined): RunnerDepsOnAsk | undefined;
|
|
28
50
|
export interface RunnerDepsCtx {
|
|
29
51
|
config: ServiceConfig;
|
|
30
52
|
logger: Logger;
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -20,6 +20,11 @@ import { resolveServedSession } from "../orchestration/workflow-completion-inbox
|
|
|
20
20
|
import { WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
|
|
21
21
|
import { makeLoadProjectMemory, makeProbeInstructionSources } from "../project-memory.js";
|
|
22
22
|
import { workflowModelAllowlistFor } from "../task-workflow.js";
|
|
23
|
+
export function createRunnerDepsOnAsk(toolApproval) {
|
|
24
|
+
if (!toolApproval)
|
|
25
|
+
return undefined;
|
|
26
|
+
return (req, signal) => toolApproval.ask(req, signal);
|
|
27
|
+
}
|
|
23
28
|
export function createSharedRunnerDeps(ctx) {
|
|
24
29
|
// 具名带类型的字面量(非裸 return)——deps-literal-shape-gate 的 POINTS 按 `const X: T = {` 咬装配
|
|
25
30
|
// 字面量,裸 return 形在它的覆盖外(复审 2026-07-30 F5):conditional-spread 病在这里就会失检。
|
|
@@ -215,17 +220,9 @@ export function createRunnerDeps(ctx) {
|
|
|
215
220
|
// (live-stream leg → this coordinator; no-live-context leg → durable park). The sentinel is stamped only
|
|
216
221
|
// when the coordinator is absent (ASK_QUESTION_ENABLED off).
|
|
217
222
|
onQuestion: question ? question.question : undefined,
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
// [879] G1 终态(core 1.295 OnAsk 三值化):恒 wire。回调逐 ask 时刻判活人——ALS 附着腿 ⇒ 同步三选卡;
|
|
222
|
-
// 无附着/卡送达失败 ⇒ 返 "unavailable",core 以 approverUnavailable 回路把该 ask 交回 suspendAsk 走
|
|
223
|
-
// durable park(prepare-task 分路带 `approverUnavailable !== true` 豁免位,dist 亲读)——park 与 live 卡
|
|
224
|
-
// 两全,1.199 的「durable 部署不 wire deps.onAsk」止血撤除。无 park 设施的部署 core 自己 fail-closed
|
|
225
|
-
// deny(resolveAsk 的 unavailable 文案),与旧姿势同向。
|
|
226
|
-
onAsk: toolApproval
|
|
227
|
-
? (req, signal) => toolApproval.ask(req, signal)
|
|
228
|
-
: undefined,
|
|
223
|
+
// live tool-approval 接缝 —— 语义与历史全部搬进 `createRunnerDepsOnAsk` 的头注(#168 件4:同一份
|
|
224
|
+
// 说明只留一处,不在装配点复述)。
|
|
225
|
+
onAsk: createRunnerDepsOnAsk(toolApproval),
|
|
229
226
|
// [822]② auto permission mode, deployment half: RunnerDeps.autoMode is the operator TRUST face of core's
|
|
230
227
|
// auto-mode classifier (1.276/1.277) — wiring it alone arms NOTHING (core requires the per-principal entitlement
|
|
231
228
|
// runtimeCaps.autoMode === true from center, fail-closed dark by default; see runtime-caps-resolver.ts). With
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* → `workflowRunStore`(包装体)的顺序是数据依赖链,不可重排。
|
|
12
12
|
* 3. `getRunStore()` 是晚绑取值(runStore 在本段之后构造),原文靠同作用域前向引用。
|
|
13
13
|
*/
|
|
14
|
-
import { type WorkflowRunStore } from "@sema-agent/core";
|
|
14
|
+
import { type WorkflowRun, type WorkflowRunStore } from "@sema-agent/core";
|
|
15
15
|
import type { ServiceConfig } from "../config.js";
|
|
16
16
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
17
17
|
import type { Logger } from "../observability/logger.js";
|
|
18
18
|
import type { Metrics } from "../observability/metrics.js";
|
|
19
19
|
import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
|
|
20
|
-
import { WorkflowNotifyGate, type WorkflowCompletionPayload } from "../orchestration/workflow-notify-journal.js";
|
|
20
|
+
import { JournalingWorkflowRunStore, WorkflowNotifyGate, type WorkflowCompletionPayload } from "../orchestration/workflow-notify-journal.js";
|
|
21
21
|
import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
|
|
22
22
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
23
23
|
import type { StoreBackend } from "../plugins/store-backend.js";
|
|
@@ -37,10 +37,15 @@ export declare function createWorkflowOrchestration(ctx: WorkflowOrchestrationCt
|
|
|
37
37
|
deliverWorkflowCompletion: (p: WorkflowCompletionPayload) => Promise<void>;
|
|
38
38
|
workflowNotifyGate: WorkflowNotifyGate | undefined;
|
|
39
39
|
fleetBus: FleetEventBus;
|
|
40
|
-
workflowRunStore:
|
|
40
|
+
workflowRunStore: JournalingWorkflowRunStore | undefined;
|
|
41
41
|
workflowJournalStore: import("../plugins/store-backend.js").ServiceWorkflowJournalStore | undefined;
|
|
42
42
|
outcomeSink: import("../plugins/file-outcome-sink.js").OutcomeSink | undefined;
|
|
43
43
|
workflowAgentRegistry: WorkflowAgentRegistry | undefined;
|
|
44
44
|
subagentSteerRegistry: SubagentSteerRegistry;
|
|
45
|
+
workflowRecoverOpts: {
|
|
46
|
+
orphanGraceMs?: number;
|
|
47
|
+
finalizeStartedBeforeMs?: number;
|
|
48
|
+
publishTerminalFleetRow?: (id: string, run: WorkflowRun) => void;
|
|
49
|
+
};
|
|
45
50
|
};
|
|
46
51
|
//# sourceMappingURL=workflow-orchestration.d.ts.map
|
|
@@ -20,6 +20,11 @@ import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
|
|
|
20
20
|
import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
|
|
21
21
|
export function createWorkflowOrchestration(ctx) {
|
|
22
22
|
const { config, logger, metrics, localRoot, backend, getRunStore } = ctx;
|
|
23
|
+
// [2995] boot-orphan 判死界:此刻之前记入 journal 的 pending 条目属于**前世进程**(workflow 执行器是
|
|
24
|
+
// in-process 的,随前世死)。取在 journal/gate 构造之前 ⇒ 本进程随后 record 的每条 createdAt 常态下都严格
|
|
25
|
+
// 晚于它。⚠️ 它是**必要不充分**条件:时钟回拨会让本世条目落到界下,判死的最终锚是 gate 内的
|
|
26
|
+
// incarnation 名册(见 WorkflowNotifyGate.recordedThisIncarnation),两者取交集。
|
|
27
|
+
const workflowBootAtMs = Date.now();
|
|
23
28
|
// S8 / SVC-1 workflow run store (design/97 S1b): records LLM-authored BACKGROUND workflow
|
|
24
29
|
// runs for the /v1/workflows list+detail view (live subscribe is in-process via subscribeWorkflow,
|
|
25
30
|
// store-independent). Only present when self-orchestration is enabled (else no runs to record).
|
|
@@ -120,6 +125,23 @@ export function createWorkflowOrchestration(ctx) {
|
|
|
120
125
|
const workflowRunStore = baseWorkflowRunStore
|
|
121
126
|
? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
|
|
122
127
|
: undefined;
|
|
128
|
+
// [2995]/[2999] recover 两半的**单点合成**(boot 扫描与周期扫描共用同一对象,防两处漂移——#177 同族教训):
|
|
129
|
+
// · finalizeStartedBeforeMs = 诚实性半场,**只在 replica-local(File)journal 形**:SQL journal 是跨副本
|
|
130
|
+
// 的(上面 1.108 注),pending running run 可能活在别的 replica,那里年龄(grace)是唯一健全的孤儿代理。
|
|
131
|
+
// (进程内是否本世记录由 gate 自己的 incarnation 锚判,时钟回拨不参与——见 recover 的键注。)
|
|
132
|
+
// · publishTerminalFleetRow = 判死臂的**撤行**口:sweep 翻完 durable 行后经 wrapper 的同一投影发终帧+移除,
|
|
133
|
+
// store/面板/notify 三面一个答案。⚠️ 复审第 1 轮两条 high 的收口:sweep **绝不**发 running 行(读时快照
|
|
134
|
+
// 铸出来的行可能永远无人可撤——跨副本形属主在别的副本、同副本形 live 终态可能已把行撤了)。fleet 活行的
|
|
135
|
+
// 唯一写者是本副本 wrapper 的 put/update 观察点。
|
|
136
|
+
// 授权条件直接问「这本 journal 是不是 replica-local」,**不拿 sqlWorkflowRunStore 当代理**:两处各写一遍
|
|
137
|
+
// 必漂移,而漂移方向是把判死授权交给共享 journal = 杀别副本正在跑的 run。未知/新 journal 实现 ⇒ 恒 false
|
|
138
|
+
// (fail-closed:最坏只是孤儿多等一个 grace 窗)。
|
|
139
|
+
const replicaLocalNotifyJournal = workflowNotifyJournal instanceof FileWorkflowNotifyJournalStore;
|
|
140
|
+
const workflowRecoverOpts = {
|
|
141
|
+
orphanGraceMs: config.workflowOrphanGraceMs,
|
|
142
|
+
...(replicaLocalNotifyJournal ? { finalizeStartedBeforeMs: workflowBootAtMs } : {}),
|
|
143
|
+
...(workflowRunStore ? { publishTerminalFleetRow: (id, run) => workflowRunStore.republishFleet(id, run) } : {}),
|
|
144
|
+
};
|
|
123
145
|
// SVC-2 (core CORE-7/CORE-9 Part A): the durable resume journal. core 1.145.0 added `RunnerDeps.workflowJournalStore`
|
|
124
146
|
// (twin of workflowRunStore) + auto-wires it into the run_workflow tool's startWorkflow(RunWorkflowOptions.journalStore);
|
|
125
147
|
// the tool's `resumeFromRunId` input then replays the longest unchanged prefix. tidb/pg = durable cross-replica resume;
|
|
@@ -144,7 +166,7 @@ export function createWorkflowOrchestration(ctx) {
|
|
|
144
166
|
return {
|
|
145
167
|
sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion,
|
|
146
168
|
workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink,
|
|
147
|
-
workflowAgentRegistry, subagentSteerRegistry,
|
|
169
|
+
workflowAgentRegistry, subagentSteerRegistry, workflowRecoverOpts,
|
|
148
170
|
};
|
|
149
171
|
}
|
|
150
172
|
//# sourceMappingURL=workflow-orchestration.js.map
|
|
@@ -434,20 +434,43 @@ export function runtimeGatePresent(rt, key) {
|
|
|
434
434
|
*
|
|
435
435
|
* 顺带 C1-2:数组项 trim + 去空串,与 env csv 归一(`csv()` 的 `.map(trim).filter(Boolean)`)一致——
|
|
436
436
|
* 否则 `" Bash "` 从中心腿进来会是个永不匹配的名字,正是本条要消灭的那类静默失效。
|
|
437
|
+
*
|
|
438
|
+
* ⚠️ codex R1(#186 同批复审):**形错必须拒域,不能折成 `[]`**。旧实现先按 `typeof === "string"` 过滤再验
|
|
439
|
+
* 判据,于是 `[123]`(非串项)与 `"Bash"`(整个不是数组)双双归约成空名单、判据自然干净 ⇒ 被当作「运维
|
|
440
|
+
* 显式把闸关掉」落配,把前值换成空门且**零告警**。方向与 A1 相反(A1 是门空转,这条是门蒸发),但同属本域
|
|
441
|
+
* 要堵的失败,且在安全轴上是静默 fail-open。判据分家:**空串/纯空白仍丢弃**(C1-2 的 env csv 归一,env 腿
|
|
442
|
+
* 产不出非串项),**非串项 / 非数组=形错 ⇒ 拒整域保前值**;显式 `[]` 依旧是合法的「闸关掉」。
|
|
437
443
|
*/
|
|
438
444
|
function stageApprovalRequire(raw, logger) {
|
|
439
|
-
//
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
445
|
+
// `kept` 逐臂说真话:内容臂的后果是「门空转」,形错臂的后果是「门蒸发」——两句不能混用。
|
|
446
|
+
const refuse = (detail, names, kept) => {
|
|
447
|
+
logger?.warn("sema_registry_approval_require_invalid", { detail, names: [...names], kept });
|
|
448
|
+
return {};
|
|
449
|
+
};
|
|
450
|
+
const KEPT_WOULD_MATCH_NOTHING = "the prior approvalRequire is unchanged — the published list would have silently matched nothing";
|
|
451
|
+
const KEPT_WOULD_REMOVE_GATE = "the prior approvalRequire is unchanged — coercing this malformed value would have silently REMOVED the approval gate";
|
|
452
|
+
// 形错门(codex R1),先于内容判据:此处**不能**静默归一,归一的结果恰好是「空门」。
|
|
453
|
+
// 非数组一支是**纵深防御**:唯一调用点先过 `runtimeGatePresent`(判 `Array.isArray`),非数组在那里就按
|
|
454
|
+
// 「缺席」跳过、保前值——已是 fail-safe。本函数签名收 `unknown`,所以自己也把这一形收口,免得 present
|
|
455
|
+
// 判据日后放宽时非数组悄悄掉进下面的归一腿(测试两支都钉了)。
|
|
456
|
+
if (!Array.isArray(raw)) {
|
|
457
|
+
return refuse(`published runtime.approvalRequire must be an ARRAY of whole tool names — got ${raw === null ? "null" : typeof raw}. ` +
|
|
458
|
+
`Refusing the domain instead of coercing it: an empty list means "no approval gate", so coercing a malformed value would SILENTLY remove the gate.`, [], KEPT_WOULD_REMOVE_GATE);
|
|
459
|
+
}
|
|
460
|
+
// 逐项报**下标 + 实际类型**(运维要能在发布件里定位到那一项;只报个数无法行动)。
|
|
461
|
+
const nonString = raw
|
|
462
|
+
.map((n, i) => (typeof n === "string" ? undefined : { i, t: n === null ? "null" : typeof n }))
|
|
463
|
+
.filter((e) => e !== undefined);
|
|
464
|
+
if (nonString.length > 0) {
|
|
465
|
+
return refuse(`published runtime.approvalRequire contains ${nonString.length} non-string entr${nonString.length === 1 ? "y" : "ies"} ` +
|
|
466
|
+
`(${nonString.map((e) => `[${e.i}]:${e.t}`).join(", ")}) — every entry must be a whole tool name string. ` +
|
|
467
|
+
`Refusing the domain instead of dropping them: dropping every entry would SILENTLY remove the approval gate.`, nonString.map((e) => `[${e.i}]`), KEPT_WOULD_REMOVE_GATE);
|
|
468
|
+
}
|
|
469
|
+
// wire verbatim ⇒ 空串/纯空白按 env csv 归一丢弃(C1-2);元素形状已在上面的形错门收口。
|
|
470
|
+
const names = raw.filter((n) => typeof n === "string").map((n) => n.trim()).filter((n) => n.length > 0);
|
|
443
471
|
const bad = findUnmatchableToolNames(names);
|
|
444
472
|
if (bad.length > 0) {
|
|
445
|
-
|
|
446
|
-
detail: formatUnmatchableToolNames("published runtime.approvalRequire", bad),
|
|
447
|
-
names: bad.map((b) => b.name),
|
|
448
|
-
kept: "the prior approvalRequire is unchanged — the published list would have silently matched nothing",
|
|
449
|
-
});
|
|
450
|
-
return {};
|
|
473
|
+
return refuse(formatUnmatchableToolNames("published runtime.approvalRequire", bad), bad.map((b) => b.name), KEPT_WOULD_MATCH_NOTHING);
|
|
451
474
|
}
|
|
452
475
|
return { value: names };
|
|
453
476
|
}
|