@sema-agent/server 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— `RunnerDeps` 装配(含 self-orchestration 成组段)。
3
+ *
4
+ * 纯搬运:两个字面量逐字来自 `main.ts`(原 1579-1881 行),缩进不变(顶层键仍在 4 空格,
5
+ * `deps-literal-shape-gate` 的顶层条件展开门覆盖面不变);新增的只有 import 与包壳。
6
+ *
7
+ * ⚠️ **位置即契约**:必须晚于 stores/exec-env/coordinators/workflow 段(所有键都是它们的产物),
8
+ * 且早于 `new Runner(runnerDeps)` —— Runner 构造会冻结 tier 展开后的私有目录副本
9
+ * (`runnerTierFrozen` 判据就取自构造那一刻的 `config.tiers`)。
10
+ * `getRunStore()` 是唯一的晚绑取值(runStore 在本段之后才构造),原文靠同作用域前向引用。
11
+ */
12
+ import { type RunnerDeps, type WorkflowRunStore } from "@sema-agent/core";
13
+ import type { createBrain } from "../brain.js";
14
+ import type { buildPricing, createTracer } from "../budget.js";
15
+ import type { ServiceConfig } from "../config.js";
16
+ import type { ElicitationCoordinator } from "../elicitation.js";
17
+ import { FleetEventBus } from "../fleet/fleet-bus.js";
18
+ import type { Logger } from "../observability/logger.js";
19
+ import type { Metrics } from "../observability/metrics.js";
20
+ import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
21
+ import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
22
+ import { WorkflowNotifyGate, type WorkflowCompletionPayload } from "../orchestration/workflow-notify-journal.js";
23
+ import type { ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
24
+ import type { QuestionCoordinator } from "../question.js";
25
+ import type { ToolApprovalCoordinator } from "../tool-approval.js";
26
+ import type { MemoryBackend } from "@sema-agent/core";
27
+ import type { MemorySyncRunner } from "../memory-sync-client.js";
28
+ export interface RunnerDepsCtx {
29
+ config: ServiceConfig;
30
+ logger: Logger;
31
+ metrics: Metrics;
32
+ localRoot: string;
33
+ promptSource: RunnerDeps["promptSource"];
34
+ rosterStore: RunnerDeps["rosterStore"];
35
+ backgroundAgentStore: RunnerDeps["backgroundAgentStore"];
36
+ mailboxStore: RunnerDeps["mailboxStore"];
37
+ brain: ReturnType<typeof createBrain>;
38
+ pricing: ReturnType<typeof buildPricing>;
39
+ tracer: ReturnType<typeof createTracer>;
40
+ outcomeSink: ReturnType<StoreBackend["outcomeSink"]> | undefined;
41
+ elicitation: ElicitationCoordinator | undefined;
42
+ question: QuestionCoordinator | undefined;
43
+ toolApproval: ToolApprovalCoordinator | undefined;
44
+ sessionStore: ReturnType<StoreBackend["session"]>;
45
+ memoryEngine: {
46
+ backend: MemoryBackend;
47
+ root: string;
48
+ } | undefined;
49
+ memorySyncRunner: MemorySyncRunner | undefined;
50
+ toolResultStore: ToolResultStoreFull | undefined;
51
+ sessionPolicyStore: ReturnType<StoreBackend["sessionPolicy"]> | undefined;
52
+ runtimeCapsResolver: RunnerDeps["runtimeCapsResolver"];
53
+ fileSnapshotStore: ReturnType<StoreBackend["fileSnapshot"]> | undefined;
54
+ executionEnvFactory: RunnerDeps["executionEnvFactory"];
55
+ lspManager: RunnerDeps["lspManager"];
56
+ fleetBus: FleetEventBus;
57
+ deploymentHooks: NonNullable<RunnerDeps["hooks"]>;
58
+ workflowRunStore: WorkflowRunStore | undefined;
59
+ workflowJournalStore: ServiceWorkflowJournalStore | undefined;
60
+ workflowAgentRegistry: WorkflowAgentRegistry | undefined;
61
+ workflowNotifyGate: WorkflowNotifyGate | undefined;
62
+ workflowCompletionInbox: WorkflowCompletionInbox | undefined;
63
+ deliverWorkflowCompletion: (p: WorkflowCompletionPayload) => Promise<void>;
64
+ /** 晚绑(runStore 在本段之后构造)——见文件头「位置即契约」。 */
65
+ getRunStore: () => ReturnType<StoreBackend["run"]> | undefined;
66
+ }
67
+ /** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
68
+ * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
69
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore">;
70
+ /**
71
+ * design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
72
+ * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
73
+ * 字面量之间,以下 ~15 个键此前是**各自手写、独立展开**的同源值(同一 ctx 变量,两处分别拼字面量,
74
+ * 不是同一引用)——正是 [1543]§三族A 那次「主 runner 挂了、subRunner 漏挂」漏配事故的键族(当时的
75
+ * 修法是把 onBackgroundChildEvent/loadProjectMemory/probeInstructionSources/onError 四键改成直引
76
+ * `runnerDeps.X`「同一实例」;这里补的是**剩下未被那次改法覆盖**的 15 个键——两处字面量各自独立
77
+ * 取值,漏配一个不会有任何编译期或运行期信号)。
78
+ *
79
+ * 抽成本函数 + `Pick<RunnerDeps, …>` 类型标注(`SharedRunnerDeps`),把「新增/改名一个键、两处忘
80
+ * 同步」从「肉眼比对两份手写字面量」变成**编译期护栏**:调用方漏写一个必需键 ⇒ 返回值类型当场
81
+ * 红;`RunnerDeps` 改名/删键 ⇒ 这里的联合类型当场红。
82
+ *
83
+ * 消费点(复审 2026-07-30 F1 更新,过期「尚未接上」段已删):**两处都已展开**——下方
84
+ * `createRunnerDeps()`(全量 ctx)与 main.ts 的 subRunner `new Runner({...createSharedRunnerDeps(...)})`
85
+ * (窄面 {@link SharedRunnerDepsCtx})。新增共享键只需加进本函数返回体,两 Runner 自动同源;
86
+ * **不要**再在任一调用点手写同名键(那会 override 展开,回到 [1543] 漏配病)。
87
+ *
88
+ * 差异键(为何不在本共享基座、逐一注明):
89
+ * - `sessionStore`:main runner 用宿主 durable 会话店;subRunner 用私有短 TTL 的
90
+ * `ForkRoutingSessionStore`(main.ts 现场构造,子代转录生命周期与主会话不同)。
91
+ * - `checkpointStore`:main runner 走 `spec.checkpointStore` 分支(不进 `RunnerDeps`,本 ctx 根本
92
+ * 没有这个字段);subRunner 单独携带(子代执行面的 park 设施,[1584])。
93
+ * - `onBackgroundChildEvent` / `loadProjectMemory` / `probeInstructionSources` / `onError`:
94
+ * [1543]§三族A 已用「直引 `runnerDeps.X`」修过(main.ts 两处引用同一个 `runnerDeps` 实例的同一
95
+ * 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
96
+ */
97
+ /** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
98
+ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore">;
99
+ export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
100
+ export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
101
+ //# sourceMappingURL=runner-deps.d.ts.map
@@ -0,0 +1,343 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— `RunnerDeps` 装配(含 self-orchestration 成组段)。
3
+ *
4
+ * 纯搬运:两个字面量逐字来自 `main.ts`(原 1579-1881 行),缩进不变(顶层键仍在 4 空格,
5
+ * `deps-literal-shape-gate` 的顶层条件展开门覆盖面不变);新增的只有 import 与包壳。
6
+ *
7
+ * ⚠️ **位置即契约**:必须晚于 stores/exec-env/coordinators/workflow 段(所有键都是它们的产物),
8
+ * 且早于 `new Runner(runnerDeps)` —— Runner 构造会冻结 tier 展开后的私有目录副本
9
+ * (`runnerTierFrozen` 判据就取自构造那一刻的 `config.tiers`)。
10
+ * `getRunStore()` 是唯一的晚绑取值(runStore 在本段之后才构造),原文靠同作用域前向引用。
11
+ */
12
+ import { createFileWorkflowScriptStore } from "@sema-agent/core";
13
+ import { join } from "node:path";
14
+ import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
15
+ import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
16
+ import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
17
+ import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
18
+ import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
19
+ import { resolveServedSession } from "../orchestration/workflow-completion-inbox.js";
20
+ import { WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
21
+ import { makeLoadProjectMemory, makeProbeInstructionSources } from "../project-memory.js";
22
+ import { workflowModelAllowlistFor } from "../task-workflow.js";
23
+ export function createSharedRunnerDeps(ctx) {
24
+ // 具名带类型的字面量(非裸 return)——deps-literal-shape-gate 的 POINTS 按 `const X: T = {` 咬装配
25
+ // 字面量,裸 return 形在它的覆盖外(复审 2026-07-30 F5):conditional-spread 病在这里就会失检。
26
+ const shared = {
27
+ brain: ctx.brain,
28
+ // core 1.265: the active tier table (tier words + CC aliases → catalog keys, expandTiers at Runner
29
+ // construction; empty = INERT by core contract). Same mutateInPlace reference applyEffective fills — a
30
+ // refresh-time tier change is restart-to-apply, same tier as models.
31
+ tiers: ctx.config.tiers,
32
+ models: ctx.config.models,
33
+ roles: ctx.config.roles,
34
+ pricing: ctx.pricing,
35
+ tracer: ctx.tracer,
36
+ // e/b([985]):center catalog 轴的 core 消费面——candidate/pinned-digest 解析走这里(prepare-task
37
+ // dist:session_start 优先 candidate、resume 按 digest 解析、miss=fail-loud prompt_snapshot_unavailable)。
38
+ promptSource: ctx.promptSource,
39
+ executionEnvFactory: ctx.executionEnvFactory ? ctx.executionEnvFactory : undefined,
40
+ lspManager: ctx.lspManager ? ctx.lspManager : undefined,
41
+ // core 1.364 durable bg agents 读半场(写半场=scenarioDeps.backgroundAgentStore 同实例,组装区注释)。
42
+ backgroundAgentStore: ctx.backgroundAgentStore ? ctx.backgroundAgentStore : undefined,
43
+ // S3c tier-3 懒复活(core 1.374):SendMessage 链内消费,与 backgroundAgentStore 同实例配套挂载。
44
+ mailboxStore: ctx.mailboxStore ? ctx.mailboxStore : undefined,
45
+ // [1070]① agent-team S1:持久名册 seam(具名 spawn advisory 写入;SendMessage 活注册表 miss 后咨询)。
46
+ rosterStore: ctx.rosterStore ? ctx.rosterStore : undefined,
47
+ hooks: ctx.deploymentHooks,
48
+ toolResultStore: ctx.toolResultStore,
49
+ // E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
50
+ // carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
51
+ sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
52
+ };
53
+ return shared;
54
+ }
55
+ export function createRunnerDeps(ctx) {
56
+ const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, memorySyncRunner, runtimeCapsResolver, fileSnapshotStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, } = ctx;
57
+ // design/158 A10 留档发现②:共享基座——见 createSharedRunnerDeps 头注(main.ts 的第二处展开是
58
+ // 并行车道待办,未在本次改动内接线)。
59
+ const sharedRunnerDeps = createSharedRunnerDeps(ctx);
60
+ // S8 (design/98) + SVC-1: mount core's `run_workflow` under the hardened-vm sandbox when
61
+ // enabled (default OFF → core fail-closed, tool not mounted, zero impact). The tool STARTS a workflow and
62
+ // returns its runId IMMEDIATELY (the background-run half — the caller never blocks); the workflow runs to
63
+ // terminal in the background and fires core's in-process completion notify. Workflow-spawned children inherit
64
+ // a CONSERVATIVE baseline (handsReadOnly — read-only by default; loosen deliberately); the script may pick
65
+ // ONLY allow-listed model names (empty ⇒ it cannot pick → the workflow's default role).
66
+ //
67
+ // design/158 S4:三处装配点里**唯一**一组多键条件段 —— 提成带类型标注的中间 const 再整体展开进
68
+ // runnerDeps。标注位 `Pick<RunnerDeps, …>` 本身触发 TS 多余属性检查(键名写错 / 键根本不属于
69
+ // RunnerDeps ⇒ 编译期红),所以这不是把旁路挪个地方;组内可选键一律写成显式
70
+ // `k: cond ? expr : undefined`,不再用 `...(cond ? { k } : {})`。
71
+ const workflowModelAllowlist = workflowModelAllowlistFor(config);
72
+ const selfOrchestrationDeps = config.selfOrchestrationEnabled
73
+ ? {
74
+ // worker_thread isolation (heap cap + terminate) when opted in (multi-tenant DoS hardening), else
75
+ // the faster in-process runner. Both run the same conformance-validated + probe-hardened membrane.
76
+ workflowScriptRunner: config.selfOrchestrationWorkerIsolation
77
+ ? createWorkerHardenedVmRunner()
78
+ : createHardenedVmRunner(),
79
+ workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
80
+ // SVC-2 (CORE-9 Part A): durable resume journal — core auto-wires it into the tool's startWorkflow, the
81
+ // `resumeFromRunId` tool input then replays the unchanged prefix (cross-replica when tidb/pg-backed). The
82
+ // store enforces scope in its WHERE (CORE-9 audit BLOCKER: a cross-tenant resumeFromRunId → empty → live).
83
+ workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
84
+ // 切片 1.5 / design/140 §6: the named-workflow registry seam — a COMPOSITE store.
85
+ // persist/load = core's file store under `<localDataRoot>/workflow-scripts` (the CC scriptPath-iterate
86
+ // face: every invocation's script lands on disk, the model edits + re-invokes with {scriptPath};
87
+ // containment is the file store's contract). resolveName consults the collab projection FIRST (center
88
+ // collab templates as `{TEAM_DISCUSSION_SCRIPT, defaultArgs}` entries; a collab id equal to a built-in
89
+ // name SHADOWS it — §6 1c) and falls back to the file store's `<name>.js` face (an operator-saved
90
+ // script is a named workflow too). list() = collab entries only (the file store deliberately doesn't
91
+ // enumerate — its dir mixes per-run scripts with saved names, core contract note); sync, so the
92
+ // entries reach the statically-built Workflow tool card.
93
+ workflowScriptStore: (() => {
94
+ const fileStore = createFileWorkflowScriptStore(join(config.localDataRoot ?? localRoot, "workflow-scripts"));
95
+ return {
96
+ // core 1.366 (B-4) scope 分区一致性标:persist/load 直转 core file 实现(per-scope 子目录,
97
+ // 真分区)——标是诚实声明;缺标=core 工具读侧拒/写侧跳(编译期必填字面量即为此设计)。
98
+ scopePartitioned: true,
99
+ persist: fileStore.persist.bind(fileStore),
100
+ load: fileStore.load.bind(fileStore),
101
+ resolveName: (name) => resolveCollabWorkflow(name) ?? fileStore.resolveName?.(name),
102
+ list: () => listCollabWorkflows(),
103
+ };
104
+ })(),
105
+ // SVC-5 (CORE-9 Part B): the opt-in steer handle-sink. When set, the tool's `agent()` runs STEERABLE and
106
+ // emits the live handle HERE (never into the script/runner — the host-context membrane stays closed). We
107
+ // register it by runId+label so POST /v1/workflows/:id/agents/:label/steer can route a steer to it, and
108
+ // unregister when the agent settles (result() settles on stream completion; a stale handle would only
109
+ // 409 `steering.not_running` anyway — this just bounds the map). A steered agent is NOT journaled (CORE-9.1
110
+ // limitation: its resume re-runs live), which is correct + documented.
111
+ onWorkflowAgentSpawn: workflowAgentRegistry
112
+ ? (handle) => {
113
+ const unregister = workflowAgentRegistry.register(handle);
114
+ void Promise.resolve(handle.result()).then(unregister, unregister);
115
+ }
116
+ : undefined,
117
+ // workflowSizeGuideline: advisory size guidance injected into the
118
+ // Workflow tool card (core Wvs, CC 206 verbatim). Absent/unrestricted = byte-compat card.
119
+ workflowLimits: config.workflowSizeGuideline ? { sizeGuideline: config.workflowSizeGuideline } : undefined,
120
+ workflowGovernanceBaseline: {
121
+ // [824]① clay 拍 A 案(workflow 权限全面 CC parity):默认基线撤 handsReadOnly 钳 —— 主 LLM 与
122
+ // 子 agent 同 root 同信任域,主 LLM 本就能写这棵树([816] ask 门照管),单独钳子 agent 的安全增益≈0
123
+ // (只防绕路不防直路的门不是边界);实测产品代价=[814]A 死锁(写型 workflow agent 永远只读)。并发
124
+ // 写互踩按 CC 同姿用建议解决(worktree 契约话术+design/111 advisory),不用强制钳。要保守的 TOB
125
+ // 部署自己开 WORKFLOW_AGENTS_READONLY=true(config.workflowAgentsReadOnly)加回旧钳。
126
+ base: config.workflowAgentsReadOnly ? { handsReadOnly: true } : {},
127
+ // [824]②/[826]-core 1.291 worktree seam(TOB 旋钮回归腿):旋钮开启时 worktreeBase 配成
128
+ // overlay 形状 —— core 对 `agent({isolation:"worktree"})` 做 base 浅合并(非整体替换,
129
+ // workflow-primitives effectiveBaseline:{...base,...worktreeBase}),{handsReadOnly:false} 语义=
130
+ // 「只放写,其余基线全保留」。效果:保守部署里非隔离子 agent 仍只读,进了自有 worktree 的
131
+ // agent 恢复可写([814]A 写型 workflow 在 TOB 形态下不死锁)。A 案默认(旋钮 off)不配 worktreeBase。
132
+ worktreeBase: config.workflowAgentsReadOnly ? { handsReadOnly: false } : undefined,
133
+ // Model allowlist for the script's `agent({model})` picks: explicit
134
+ // SELF_ORCHESTRATION_MODELS wins; single-user defaults to the deployment's own catalog; multi-tenant
135
+ // keeps core's fail-closed empty default. Decision + rationale in workflowModelAllowlistFor.
136
+ workflowModelAllowlist: workflowModelAllowlist ? workflowModelAllowlist : undefined,
137
+ },
138
+ // SVC-1 at-least-once notify: route core's terminal notify THROUGH the gate (deliver-then-ack +
139
+ // journal dedup) instead of straight to log/meter, so a crash mid-delivery re-delivers on the next boot
140
+ // recovery sweep. When the gate is absent (memory backend / no journal) fall back to the bare
141
+ // log+meter delivery (at-most-once, the prior behavior — honest about the lost-on-crash window).
142
+ workflowCompletionNotifier: {
143
+ // ⚠️ design/158 S4 有意保留的展开:两臂**都非空**(要么 gate 造的 notifier,要么裸 notify 兜底),
144
+ // 不是「条件键」形——它合并的是同一必填键 `notify` 的两个来源,两来源都有类型,不构成
145
+ // 多余属性检查旁路(缺 notify 反而当场编译红)。
146
+ ...(workflowNotifyGate
147
+ ? workflowNotifyGate.buildNotifier()
148
+ : { notify: (input) => deliverWorkflowCompletion(input) }),
149
+ // core 1.232 (the CORE half of the poll-then-also-notify dedup): pollWorkflow fires this
150
+ // ONCE when the ORIGINATING session polls the run to a real terminal (timeout/cross-session polls
151
+ // don't) — drop that (session, runId)'s pending inbox entry + arm the served fence, so the push
152
+ // that would duplicate what the model just read is dead even across a shell restart. Session
153
+ // attribution mirrors resolveCompletionRoute's session half (payload id, else the run row).
154
+ ackServed: async (input) => {
155
+ if (!workflowCompletionInbox)
156
+ return;
157
+ try {
158
+ const sid = await resolveServedSession(input, getRunStore() ? (id) => getRunStore().getRun(id) : undefined); // A10 搬运改写:晚绑取值
159
+ if (sid) {
160
+ await workflowCompletionInbox.markTerminalServed(sid, input.runId);
161
+ // The THIRD way an inbox entry disappears (besides the three stream-open drains
162
+ // and the owner-mismatch drop) — the model itself polled the run to terminal, so the push is
163
+ // suppressed. An "enqueue → ack within ms, but the shell showed nothing" report with THIS line
164
+ // means the completion went to the MODEL in-process (core injected it), not onto any stream.
165
+ logger.info("workflow_complete_ack_served", { route: "poll-served", sessionId: sid, runId: input.runId });
166
+ }
167
+ }
168
+ catch (err) {
169
+ logger.warn("workflow_ack_served_failed", { runId: input.runId, err: String(err) });
170
+ }
171
+ },
172
+ },
173
+ }
174
+ : {};
175
+ const runnerDeps = {
176
+ // design/158 A10 留档发现②:brain/models/roles/tiers/pricing/tracer/promptSource/
177
+ // executionEnvFactory/lspManager/backgroundAgentStore/mailboxStore/rosterStore/hooks/
178
+ // toolResultStore/sessionPolicyStore —— 共享基座展开,见 createSharedRunnerDeps 头注
179
+ // (含每键各自的历史 rationale 注释,搬到了那个函数里,不在此重复)。
180
+ ...sharedRunnerDeps,
181
+ // [931]① clay 拍(core 1.300 BREAKING:缺省不署 Co-Authored-By,署名=产品身份资产归部署):
182
+ // branded 形态(local provider = Sema 产品线,scenarios brandIdentity 同判据)commit 尾注接 Sema 署名;
183
+ // 非 brand 部署维持 core 新缺省(不署)。seam=RunnerDeps.hands.commitCoAuthor。
184
+ hands: config.configProvider === "local" ? { commitCoAuthor: "Sema <noreply@vivi-ai.com>" } : undefined,
185
+ // design/73 §1 (core 1.226 seam, clay 拍 2026-07-04 接): consume mechanical TaskOutcome facts into the
186
+ // outcome ledger — tidb/pg = SQL rows (coreOutcomeToLedgerRow mapping + verbatim `core_outcome` JSON so
187
+ // red-line ② oracleHadRedRun survives lossless), local = owner-only JSONL. Read-only v1: records facts,
188
+ // drives NO policy (§7.4 backtest gate stands). Fire-and-forget — a sink failure logs and never touches
189
+ // the run (core's emitTaskOutcome swallow-guards too). Emitters today: core runGoal terminal + any
190
+ // harness with a REAL mechanical oracle calling runner.emitTaskOutcome; plain runTask never auto-emits.
191
+ onTaskOutcome: outcomeSink
192
+ ? (o) => {
193
+ metrics.inc("task_outcomes_total", { status: o.status, green: String(o.oracle?.green ?? "unknown") });
194
+ void outcomeSink.recordCore(o).catch((err) => logger.warn("task_outcome_record_failed", { runId: o.runId, err: String(err) }));
195
+ }
196
+ : undefined,
197
+ // E23: the live-only inbound-elicitation seam. core invokes it only for servers that opted in via
198
+ // McpServerSpec.elicitation (default OFF) AND only when this is wired — both must hold (doubly fail-closed).
199
+ onElicit: elicitation ? elicitation.elicit : undefined,
200
+ // §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here; the
201
+ // DURABLE leg's spec.onQuestion (QUESTION_AWAITS_RESUME) OVERRIDES this per-task so a disconnected-human ask suspends.
202
+ onQuestion: question ? question.question : undefined,
203
+ // [816]/[820]②: the live tool-approval seam (core `resolveAsk` — `spec.onAsk ?? deps.onAsk`). ALS-routed like
204
+ // onQuestion: a leg wrapped by the coordinator's runWithContext reaches the human.
205
+ //
206
+ // [879] G1 终态(core 1.295 OnAsk 三值化):恒 wire。回调逐 ask 时刻判活人——ALS 附着腿 ⇒ 同步三选卡;
207
+ // 无附着/卡送达失败 ⇒ 返 "unavailable",core 以 approverUnavailable 回路把该 ask 交回 suspendAsk 走
208
+ // durable park(prepare-task 分路带 `approverUnavailable !== true` 豁免位,dist 亲读)——park 与 live 卡
209
+ // 两全,1.199 的「durable 部署不 wire deps.onAsk」止血撤除。无 park 设施的部署 core 自己 fail-closed
210
+ // deny(resolveAsk 的 unavailable 文案),与旧姿势同向。
211
+ onAsk: toolApproval
212
+ ? (req, signal) => toolApproval.ask(req, signal)
213
+ : undefined,
214
+ // [822]② auto permission mode, deployment half: RunnerDeps.autoMode is the operator TRUST face of core's
215
+ // auto-mode classifier (1.276/1.277) — wiring it alone arms NOTHING (core requires the per-principal entitlement
216
+ // runtimeCaps.autoMode === true from center, fail-closed dark by default; see runtime-caps-resolver.ts). With
217
+ // both present, core screens every policy `ask` through the classifier (allow safe / deny hostile / leave the
218
+ // rest on the ask path → this bridge or durable park). Defaults-only config (core's rules/window); breaker-open
219
+ // is surfaced for observability.
220
+ autoMode: {
221
+ onBreakerOpen: (info) => {
222
+ metrics.inc("auto_mode_breaker_open_total");
223
+ logger.warn("auto_mode_breaker_open", { consecutiveFailures: info.consecutiveFailures, lastCause: info.lastCause });
224
+ },
225
+ },
226
+ sessionStore,
227
+ // design/138 S1: the memory-engine switch — when present (single-user + engine on), core replaces the
228
+ // legacy memory path WHOLESALE per task (materialize → session file ops → harvest; no remember/recall
229
+ // tools). `memoryEngineDir` = the resolved config root (core derives the B3 control plane beside the
230
+ // memory dir). Absent (multi-tenant / MEMORY_ENGINE=off) ⇒ no deps.memoryBackend ⇒ memory dark.
231
+ memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
232
+ memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
233
+ // S3-TOB 复审 F-9(operator 可观测底座):harvest 报告 → metrics(拒收/incident/patch 计数从此可见;
234
+ // core swallow-guard 保证 throwing consumer 不伤边界)。
235
+ onMemoryHarvestReport: memoryEngine
236
+ ? (report, info) => {
237
+ metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
238
+ if (report.patches)
239
+ metrics.inc("memory_harvest_patches_total", { phase: info.phase }, (report.patches.add ?? 0) + (report.patches.update ?? 0));
240
+ if (report.incident)
241
+ logger.warn("memory_harvest_incident", { kind: report.incident.kind, phase: info.phase });
242
+ // 142-S2.5-W1: 成功 harvest 真有 patch 落地 = 本地记忆变了 ⇒ fire-and-forget 一轮同步
243
+ // (trigger 自带 inflight 节流:上一轮在飞则跳过,漏掉的变更下一轮全量补上)。
244
+ if (memorySyncRunner && report.ok && (report.patches?.add ?? 0) + (report.patches?.update ?? 0) > 0)
245
+ memorySyncRunner.trigger("harvest");
246
+ }
247
+ : undefined,
248
+ // (toolResultStore/sessionPolicyStore already carried by the shared-base spread above.)
249
+ // design/99 §K: per-principal runtime entitlements (allowWorkflows / forceDurableGate) resolved from center.
250
+ // Opt-in: undefined ⇒ NO per-principal restriction (tighten-only; deployment default governs). See above.
251
+ runtimeCapsResolver: runtimeCapsResolver ? runtimeCapsResolver : undefined,
252
+ // E19: working-tree snapshot/restore for rewind (+ the 2c artifact store). core snapshots each completed turn +
253
+ // restores on resumeAt when spec.rewindFiles is set, for ANY env when this store is wired (gate-split 1.134.0).
254
+ fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
255
+ // (executionEnvFactory/lspManager already carried by the shared-base spread above.)
256
+ // design/129-B (core 1.240.0): the PROCESS-LEVEL background-child observer — spawn/tick/terminal
257
+ // for every bg delegation child, never dying with a leg. Feeds the fleet rows (launch 即有行, session-scoped
258
+ // children stay visible past the turn — the 缺口① fix) + the bg_notification frame on the
259
+ // always-open fleet stream (缺口②: idle completion delivery is immediate). Wired once per process.
260
+ onBackgroundChildEvent: fleetBackgroundChildPublisher(fleetBus, (msg, fields) => logger.info(msg, fields)),
261
+ // design/113 C4: inject CLAUDE.md + git narrative so a chat opens project-aware (like CC). The helper reads cwd via
262
+ // local fs/execFile — only meaningful when cwd is LOCAL (host/file lane); it stats the dir and returns null on a
263
+ // remote-sandbox cwd, so this is safe to wire generally, but we gate it to single-user (cwdHonored posture — the
264
+ // same lane where reading the local project for the caller isn't a confused-deputy hole) + the opt-out kill-switch.
265
+ loadProjectMemory: config.requirePrincipal !== true && config.projectMemoryEnabled ? makeLoadProjectMemory({ logger }) : undefined,
266
+ // core 1.302 instructions-change lane (板 [952]②): re-fingerprint the declared instruction file each turn so a
267
+ // mid-run edit surfaces as a tail attachment. STRICTLY host lane — on a remote-sandbox lane the instruction file
268
+ // lives in the container; a local re-read would fingerprint the wrong tree, so the probe is not wired there.
269
+ probeInstructionSources: config.remoteExec?.provider === "host" && config.requirePrincipal !== true && config.projectMemoryEnabled
270
+ ? makeProbeInstructionSources()
271
+ : undefined,
272
+ // (hooks already carried by the shared-base spread above.)
273
+ ...selfOrchestrationDeps, // S8 self-orchestration 全家桶(成组条件段;类型标注在上方 const)
274
+ // F8: surface best-effort compaction failures + the 1.22 prompt-cache low-hit warning (an
275
+ // unstable/poisoned prefix tanking cost) instead of silently swallowing them.
276
+ onError: (err, ctx) => {
277
+ // "degraded" (1.40) is an operational event, NOT a failure — the task still completes (on a cheaper
278
+ // model). Log it as a warning so it doesn't pollute error rates/alerts. The degraded_total metric
279
+ // comes from the tracer's task.degraded event.
280
+ if (ctx.phase === "degraded") {
281
+ logger.warn("task_degraded", { sessionId: ctx.sessionId, info: String(err) });
282
+ return;
283
+ }
284
+ // "prompt-constitution" (core 1.243): a stableSystem provider returned an ALREADY-
285
+ // assembled prompt (constitution anchor found); core passed it through un-doubled. The task is safe
286
+ // (guard un-doubles), but the provider is pre-1.243-shaped and should be upgraded to return only the
287
+ // role base. Warn — paired with the tracer's prompt_constitution_total{mode="provider-assembled"}.
288
+ if (ctx.phase === "prompt-constitution") {
289
+ logger.warn("prompt_provider_needs_upgrade", { sessionId: ctx.sessionId, info: String(err) });
290
+ return;
291
+ }
292
+ // "rewind" (E19 per-turn file snapshot): best-effort — a failed snapshot only makes that one turn
293
+ // non-rewindable; the task itself is unaffected. `too_large` is the EXPECTED shape on a big working
294
+ // tree (TOC user opening a 20G folder: enumerate hits the 256MB bound EVERY turn → an error-level
295
+ // line per turn reads like the run is broken). Downgrade to warn + count by code so a real store
296
+ // fault (enumerate_failed on a readable tree, blob-write errors) still stands out in the metric.
297
+ if (ctx.phase === "rewind") {
298
+ const code = /\((\w+)\)/.exec(String(err))?.[1] ?? "unknown";
299
+ logger.warn("rewind_snapshot_failed", { sessionId: ctx.sessionId, code, info: String(err) });
300
+ metrics.inc("rewind_snapshot_failed_total", { code });
301
+ return;
302
+ }
303
+ // "memory" (1.62, design/41): a best-effort post-task consolidation pass failed. The task already
304
+ // completed and its notes are safely appended (just not reconciled this round) — warn, fail-open,
305
+ // don't pollute error rates. (Consolidation cost still lands in model_cost_micro_usd via the tracer.)
306
+ if (ctx.phase === "memory") {
307
+ logger.warn("memory_consolidation_failed", { sessionId: ctx.sessionId, info: String(err) });
308
+ return;
309
+ }
310
+ // "mcp" (1.68): a broken MCP server was SKIPPED (fail-open) — the task still ran without
311
+ // that server's tools. Surface it as a warning + metric so a misconfigured server is observable, not silent.
312
+ if (ctx.phase === "mcp") {
313
+ logger.warn("mcp_server_unavailable", { sessionId: ctx.sessionId, info: String(err) });
314
+ metrics.inc("mcp_server_unavailable_total");
315
+ return;
316
+ }
317
+ // "prompt-cache" (1.22 low-hit + design/31 break detector): since core 1.89 the detector passes its
318
+ // root cause as ctx.classification — "server-or-ttl" is usually benign in agentic tasks (slow tools ⇒
319
+ // 5min+ request gaps expire provider caches) → warn; the prefix-bug causes
320
+ // (model-switch/tool-schema/tool-set/system-prefix) stay errors that warrant attention.
321
+ if (ctx.phase === "prompt-cache") {
322
+ metrics.inc("prompt_cache_low_hit_total");
323
+ const fields = { sessionId: ctx.sessionId, ...(ctx.classification ? { classification: ctx.classification } : {}), err: String(err) };
324
+ if (ctx.classification === "server-or-ttl")
325
+ logger.warn("prompt_cache_break", fields);
326
+ else
327
+ logger.error("prompt_cache_break", fields);
328
+ return;
329
+ }
330
+ // "config" (core 1.300 [931]②): assembly-time advisories — e.g. toolPolicy 名单池审计
331
+ // (config.toolpolicy.unmatched_names:名单里写了实挂宇宙不存在的工具名=typo 探测,enforcement
332
+ // 不变)。观测性提示,warn+metric,不进 error 告警面。
333
+ if (ctx.phase === "config") {
334
+ logger.warn("runner_config_advisory", { sessionId: ctx.sessionId, info: String(err) });
335
+ metrics.inc("runner_config_advisory_total");
336
+ return;
337
+ }
338
+ logger.error("runner_error", { phase: ctx.phase, sessionId: ctx.sessionId, err: String(err) });
339
+ },
340
+ };
341
+ return runnerDeps;
342
+ }
343
+ //# sourceMappingURL=runner-deps.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— per-principal 运行期准入(design/99 §K caps 客户端)。
3
+ *
4
+ * 纯搬运:函数体逐字来自 `main.ts`(原 1520-1577 行),缩进不变;新增的只有 import 与包壳。
5
+ *
6
+ * ⚠️ **位置即契约**:必须早于 `RunnerDeps` 装配(core 的 `runtimeCapsResolver` seam 从这里取)与
7
+ * `resolveSpec`(scenario/lane 两道 ruling 同一 caps 车)。boot 期那条 scoped-token 误配 warn 也必须
8
+ * 留在这里 —— 它是"一次性 boot 诊断",挪到别处会变成每任务噪声或干脆不打。
9
+ */
10
+ import type { ServiceConfig } from "../config.js";
11
+ import type { Logger } from "../observability/logger.js";
12
+ export interface RuntimeCapsCtx {
13
+ config: ServiceConfig;
14
+ logger: Logger;
15
+ }
16
+ export declare function createRuntimeCaps(ctx: RuntimeCapsCtx): {
17
+ principalCaps: import("../runtime-caps-resolver.js").PrincipalEntitlementsClient | undefined;
18
+ centerRuntimeCapsResolver: ((principal: string | undefined) => Promise<import("@sema-agent/core").RuntimeCaps | undefined>) | undefined;
19
+ runtimeCapsResolver: import("../runtime-caps-resolver.js").EntitlementsResolverFn | undefined;
20
+ };
21
+ //# sourceMappingURL=runtime-caps.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { applyObserverEnvOptIn, createPrincipalEntitlementsClient, scopedTokenNeedsWorker } from "../runtime-caps-resolver.js";
2
+ export function createRuntimeCaps(ctx) {
3
+ const { config, logger } = ctx;
4
+ // design/99 §K (core 1.157 `RunnerDeps.runtimeCapsResolver`) — the ENFORCE last-link of the
5
+ // three-stage workflows gate. core calls it once per task at prepare time with `spec.principal`; we resolve the
6
+ // per-principal entitlement from center's `GET /api/config/effective?principal=` (cached + fail-closed). Wired
7
+ // ONLY when a center is configured AND not in dry-run: in dry-run the service pulls+logs but does NOT APPLY
8
+ // center config, so it must NOT ENFORCE center's per-principal caps either (enforcing would deny workflows = a
9
+ // real behavior change, contradicting observe-only). Absent ⇒ core sees `undefined` ⇒ no per-principal
10
+ // restriction (the deployment-level capability + toolPolicy gates still govern).
11
+ //
12
+ // 🔐 TRUST DEPENDENCY: the gate keys off `spec.principal` (= resolveSpec's `auth.principal`). F-fix (2026-07-01,
13
+ // core-steered; contract-compliance per core 1.187's hardened `TaskSpec.principal` doc — it MUST be the
14
+ // cryptographically VERIFIED identity, never a spoofable header): `auth.principal` now comes from
15
+ // `verifiedPrincipal` (security.ts). On a GATED door that's `principalFrom` (the BFF already verified the header;
16
+ // `gatedPrincipal === principalFrom` there); on a DIRECT door (`directDoorActive`) it's the verified
17
+ // `x-approval-principal-token` JWT `sub`, NEVER the spoofable header. So `spec.principal` is ALWAYS the verified
18
+ // identity — governance (this gate), cost, and isolation key off it safely on BOTH postures. The same verified
19
+ // identity also fans out to the non-authorizer owner/cost sites that don't run createAuthorizer
20
+ // (runOwnerOk / quotaExceeded / leader / idemKey — they call `gatedPrincipal` directly). The invariant:
21
+ // spec.principal AND every principal-keyed security decision are VERIFIED. (Was a real direct-door spoof hole
22
+ // before this fix; latent until a multi-tenant direct door ships, but it is the only wall against it.)
23
+ // wpt_ scoped-token: RESOLVED (center ruling, sema-registry `aeb4979`). The earlier 1.14.0 guard
24
+ // (skip the resolver on a `wpt_` token, because center 403'd it for `?principal=`) is GONE — center now lets a
25
+ // worker-scoped `wpt_<self>` resolve its OWN worker's principal caps when we pass `?worker=<self>` (caps-only,
26
+ // never others' config). So we ALWAYS wire the resolver (configCenter + !dryRun) and pass `worker` =
27
+ // `SEMA_REGISTRY_WORKER` — it authorizes the wpt_ path and is harmless on a full token (caps are worker-
28
+ // independent). Per-principal workflow gating now truly works on the scoped-token topology (no more silent
29
+ // fleet-wide deny). (A wpt_ with no worker still 403s → fail-closed deny, the correct degrade.)
30
+ // MISCONFIG diagnostic (review MEDIUM): a wpt_<self> token with NO SEMA_REGISTRY_WORKER will 403 every caps
31
+ // fetch → fail-closed DENY ALL workflows (correct degrade, but otherwise only per-task warns). One-shot BOOT
32
+ // warning so it's diagnosable. (The orchestrator normally injects the worker alongside a wpt_ — hand-misconfig.)
33
+ if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
34
+ logger.warn("runtime_caps_scoped_token_no_worker", {
35
+ reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
36
+ });
37
+ }
38
+ // ONE per-principal caps client serves two faces off the same fetch/cache — `resolveRuntimeCaps`
39
+ // (core's seam, fail-closed) and `scenarioRuling` (resolveSpec's scenario gate, fail-open; center attaches the
40
+ // resolved {scenario, allowlist} to the caps body, single semantic source in center resolve-scenario.ts).
41
+ const principalCaps = config.configCenter && !config.configCenter.dryRun
42
+ ? createPrincipalEntitlementsClient({
43
+ baseUrl: config.configCenter.baseUrl,
44
+ token: config.configCenter.token,
45
+ ...(config.configCenter.worker ? { worker: config.configCenter.worker } : {}),
46
+ onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
47
+ })
48
+ : undefined;
49
+ // 「entitlement resolver wired」语义源(enableForkFromBody / selfOrchestrationFromBody 的多租户 fail-close
50
+ // 判别):只有 center 背书的 caps client 算数 —— 下面的 env observer 基线不是 entitlement 源,不得改变它。
51
+ const centerRuntimeCapsResolver = principalCaps?.resolveRuntimeCaps;
52
+ // observer 开闸线 env 半场:EXPERIMENTAL_OBSERVER_AGENTS=true → 单用户部署把
53
+ // allowObservers:true 作部署基线合成(center caps 带键则 center 赢);多租户不认 env(boot warn 一次,
54
+ // 行为零变)。center caps 的 allowObservers 键本身在 toCoreRuntimeCaps 宽读透传(缺键=core 默认 OFF)。
55
+ const runtimeCapsResolver = applyObserverEnvOptIn(centerRuntimeCapsResolver, {
56
+ experimentalObserverAgents: config.experimentalObserverAgents,
57
+ requirePrincipal: config.requirePrincipal,
58
+ warn: (msg, fields) => logger.warn(msg, fields),
59
+ });
60
+ return { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver };
61
+ }
62
+ //# sourceMappingURL=runtime-caps.js.map
@@ -0,0 +1,57 @@
1
+ import type { ServiceConfig } from "../config.js";
2
+ import type { Logger } from "../observability/logger.js";
3
+ import type { Metrics } from "../observability/metrics.js";
4
+ import type { WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
5
+ import { PlanCacheProbe } from "../plan-cache-probe.js";
6
+ import type { CheckpointStoreFull, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
7
+ import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js";
8
+ import type { OwnerAwareSessionStore } from "../security.js";
9
+ import { SessionWatchRegistry } from "../session-watch.js";
10
+ export interface SessionFacesCtx {
11
+ config: ServiceConfig;
12
+ logger: Logger;
13
+ metrics: Metrics;
14
+ localRoot: string;
15
+ backend: StoreBackend | undefined;
16
+ sessionStore: ReturnType<StoreBackend["session"]>;
17
+ runStore: ReturnType<StoreBackend["run"]> | undefined;
18
+ checkpointStore: CheckpointStoreFull | undefined;
19
+ toolResultStore: ToolResultStoreFull | undefined;
20
+ resumeAnchorStore: ReturnType<StoreBackend["resumeAnchor"]> | undefined;
21
+ approvalExemptionStore: ReturnType<StoreBackend["approvalExemption"]> | undefined;
22
+ sessionPolicyStore: ReturnType<StoreBackend["sessionPolicy"]> | undefined;
23
+ taskAttachmentStore: TaskAttachmentStore | undefined;
24
+ fileSnapshotStore: ReturnType<StoreBackend["fileSnapshot"]> | undefined;
25
+ workflowCompletionInbox: WorkflowCompletionInbox | undefined;
26
+ }
27
+ export declare function createSessionFaces(ctx: SessionFacesCtx): {
28
+ ownerAware: OwnerAwareSessionStore;
29
+ sessionAudit: ((sessionId: string) => Promise<{
30
+ owner: string | null;
31
+ sessionId: string;
32
+ createdAt: string;
33
+ floorEntryId: string | null;
34
+ thinkingLevel: string;
35
+ model: {
36
+ provider: string;
37
+ modelId: string;
38
+ } | null;
39
+ messages: unknown[];
40
+ promptEpoch?: {
41
+ epoch: number;
42
+ artifactDigest: string;
43
+ packId: string;
44
+ assemblyApi: number;
45
+ activatedBy: "session_start" | "compaction" | "legacy_migration";
46
+ };
47
+ } | undefined>) | undefined;
48
+ sessionWatchRegistry: SessionWatchRegistry | undefined;
49
+ purgeSession: ((sessionId: string, owner: string | null) => Promise<{
50
+ deleted: boolean;
51
+ } | {
52
+ active: string;
53
+ }>) | undefined;
54
+ instrumentDegenerate: ((result: import("@sema-agent/core").TaskResult) => void) | undefined;
55
+ planCacheProbe: PlanCacheProbe;
56
+ };
57
+ //# sourceMappingURL=session-faces.d.ts.map