@sema-agent/server 6.7.0 → 7.0.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.
@@ -15,6 +15,14 @@ import { parseNumOrFailNonNegative } from "../config.js";
15
15
  import { shellQuote as shellSafe } from "../plugins/remote-shell.js";
16
16
  import { BakeRunner, } from "./runner.js";
17
17
  const exec = promisify(execCb);
18
+ // 鲁棒性批5 A3(2026-08-05):claim/ingest/heartbeat 三个 fetch 此前零超时——bake-runner 的整条 loop()
19
+ // 是这台构建宿主机唯一的主循环,一次悬挂的 image-api 连接(网络分区/黑洞 TCP)会让 claim/heartbeat 永久
20
+ // 挂起,直到底层 socket 超时(可能几十分钟),期间既不上报心跳也不能认领新 bake——整机静默停摆。
21
+ // 上界两档:claim/ingest 不在心跳热路径,给 30s 吃满慢网 RTT + 大 body;heartbeat 必须显著小于
22
+ // BAKE_HEARTBEAT_MS 的默认量级(30s)——否则一次超时本身就吃掉一整个心跳周期,取 10s(同
23
+ // fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS` 判据的普适形)。
24
+ const BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS = 30_000;
25
+ const BAKE_HEARTBEAT_FETCH_TIMEOUT_MS = 10_000;
18
26
  function loadEnv() {
19
27
  const need = (k) => {
20
28
  const v = process.env[k];
@@ -55,6 +63,7 @@ function makeApiClient(env, log) {
55
63
  method: "POST",
56
64
  headers: auth,
57
65
  body: JSON.stringify({ runnerId: env.runnerId }),
66
+ signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
58
67
  });
59
68
  if (res.status === 204)
60
69
  return null; // empty queue — the routine "nothing to do" case, no log line
@@ -93,6 +102,7 @@ function makeApiClient(env, log) {
93
102
  method: "POST",
94
103
  headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
95
104
  body: JSON.stringify(frame),
105
+ signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
96
106
  });
97
107
  const body = res.ok ? (await res.json().catch(() => ({}))) : {};
98
108
  return {
@@ -107,6 +117,7 @@ function makeApiClient(env, log) {
107
117
  method: "POST",
108
118
  headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
109
119
  body: JSON.stringify({ event: "heartbeat" }),
120
+ signal: AbortSignal.timeout(BAKE_HEARTBEAT_FETCH_TIMEOUT_MS),
110
121
  });
111
122
  const body = res.ok ? (await res.json().catch(() => ({}))) : {};
112
123
  return {
@@ -23,5 +23,16 @@ export interface LeaderCtx {
23
23
  sessionStore: ReturnType<StoreBackend["session"]>;
24
24
  checkpointStore: CheckpointStoreFull | undefined;
25
25
  }
26
+ /**
27
+ * 鲁棒性批5 A6(2026-08-05):k8s 腿要求 MinIO 三件套(ENDPOINT/ACCESS_KEY/SECRET_KEY)全在——半开(一件缺)
28
+ * 就让 `leaderEndpoint` 的三元式落到 undefined,此前**零披露**:运维显式开了 LEADER_ENABLED +
29
+ * REMOTE_EXEC=k8s,却在日志里既看不到 `leader_endpoint_enabled` 也看不到任何"为什么没启用"的信号——只能
30
+ * 靠读源码才知道要去检查 MinIO 三件套。
31
+ *
32
+ * 纯谓词,抽出独立函数:只吃 leaderEnabled/leaderProvider/hasS3 三个原语 + 直接读 process.env 取缺失的
33
+ * 具体变量名,不依赖完整 `LeaderCtx`(构造一整套 brain/pricing/sessionStore 只为测一个 warn 分支不值当)。
34
+ * 返回 null = 不适用该警告(未开启/非 k8s 腿/三件套齐全);非 null = 该报警,携带具体缺的变量名列表。
35
+ */
36
+ export declare function leaderK8sMinioGap(leaderEnabled: boolean, leaderProvider: string | undefined, hasS3: boolean): string[] | null;
26
37
  export declare function createLeaderFace(ctx: LeaderCtx): ReturnType<typeof createLeaderEndpoint> | undefined;
27
38
  //# sourceMappingURL=leader.d.ts.map
@@ -1,5 +1,24 @@
1
1
  import { createLeaderEndpoint } from "../leader/endpoint.js";
2
2
  import { createLeaderRunner } from "../leader/wire.js";
3
+ /**
4
+ * 鲁棒性批5 A6(2026-08-05):k8s 腿要求 MinIO 三件套(ENDPOINT/ACCESS_KEY/SECRET_KEY)全在——半开(一件缺)
5
+ * 就让 `leaderEndpoint` 的三元式落到 undefined,此前**零披露**:运维显式开了 LEADER_ENABLED +
6
+ * REMOTE_EXEC=k8s,却在日志里既看不到 `leader_endpoint_enabled` 也看不到任何"为什么没启用"的信号——只能
7
+ * 靠读源码才知道要去检查 MinIO 三件套。
8
+ *
9
+ * 纯谓词,抽出独立函数:只吃 leaderEnabled/leaderProvider/hasS3 三个原语 + 直接读 process.env 取缺失的
10
+ * 具体变量名,不依赖完整 `LeaderCtx`(构造一整套 brain/pricing/sessionStore 只为测一个 warn 分支不值当)。
11
+ * 返回 null = 不适用该警告(未开启/非 k8s 腿/三件套齐全);非 null = 该报警,携带具体缺的变量名列表。
12
+ */
13
+ export function leaderK8sMinioGap(leaderEnabled, leaderProvider, hasS3) {
14
+ if (!(leaderEnabled && leaderProvider === "k8s" && !hasS3))
15
+ return null;
16
+ return [
17
+ !process.env.MINIO_ENDPOINT && "MINIO_ENDPOINT",
18
+ !process.env.MINIO_ACCESS_KEY && "MINIO_ACCESS_KEY",
19
+ !process.env.MINIO_SECRET_KEY && "MINIO_SECRET_KEY",
20
+ ].filter((v) => typeof v === "string");
21
+ }
3
22
  export function createLeaderFace(ctx) {
4
23
  const { config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore } = ctx;
5
24
  // v2 leader endpoint (design/50 + design/68): wire when LEADER_ENABLED + an isolated remote-exec backend
@@ -19,6 +38,9 @@ export function createLeaderFace(ctx) {
19
38
  }
20
39
  : {};
21
40
  const leaderProvider = config.remoteExec?.provider;
41
+ const minioGap = leaderK8sMinioGap(config.leaderEnabled, leaderProvider, "s3" in leaderMinio);
42
+ if (minioGap)
43
+ logger.warn("leader_k8s_minio_incomplete", { missing: minioGap });
22
44
  // DUAL-MODE §5: orchestration is an ENGINE capability, not fleet-only — the TOC `host` lane runs leader fan-out
23
45
  // bounded by ONE box (isolation=none, NON-durable: no snapshot, so the durable sub-worker suspend block below is
24
46
  // skipped — host workers run to completion in-process-adjacent). e2b/k8s keep their isolated/suspendable posture.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * design/170 件A(#148 件3③)——org 记忆准入的装配点:把 org-memory-admission 模块接上 core 5.13.0
3
+ * 的两个 seam(`RunnerDeps.memoryScopeAdmission` / `RunnerDeps.deploymentMemoryScopes`)。
4
+ *
5
+ * 目录源三态(单选,授权面不做双源合并):
6
+ * · center 在场(configCenter 且非 dryRun)⇒ 远程腿:per-principal caps 响应的 `orgMemory` 段
7
+ * (fetchPrincipalCaps 已 schema 读+透传;段键缺席=旧 center 能力握手 ⇒ 目录记 section_absent 瞬时)。
8
+ * **独立 fetch,不复用 caps 缓存条目**(C3:org 授权段自带判别联合缓存,两张表的失败域互不干扰
9
+ * ——caps 腿的 deny 缓存/304 复用机制一旦共享,org 面的 gen 拒退与退避语义就会被 caps 语义污染)。
10
+ * 流量记账:每 principal 每 grant TTL(60s)至多多一拍 caps 拉取,有界。
11
+ * · 无 center 而 `MEMORY_ORG_DIRECTORY_JSON` 在场 ⇒ 单机腿:operator 自证静态表(此处装载=启动期,
12
+ * 坏表 parseOrgDirectoryStatic 直接 throw 拒启动——config 层因 A4 分层不校验,见 config.ts 注)。
13
+ * 两者都在 ⇒ center 胜,env 表忽略 + warn(授权面双源合并=语义地雷,显式拒)。
14
+ * · 都缺 ⇒ resolver 缺席:core 对 request-origin org scope 铸 `memory.admission_required`(fail-closed),
15
+ * deployment-origin 走 `deploymentMemoryScopes` 自证通道不受影响(单用户零迁移,v4 §0 伤害①)。
16
+ *
17
+ * 能力探测(C12,fail-loud):多租户(requirePrincipal)部署的 projects 登记簿里挂了 org 键 ——这些
18
+ * 键在多租户下按 request-origin 盖章(N2 部署形态维)——而目录源缺席(含 configCenter dryRun 形)
19
+ * ⇒ 启动报错:该部署的每个 projectId 请求都会在 prepare 期被整拒,「audit 在跑但恒拒」不是可运行
20
+ * 状态,响亮拒启动比静默全拒服务诚实(clay 三裁 [2687]:直接 BREAKING,不留兼容臂)。
21
+ */
22
+ import type { RunnerDeps } from "@sema-agent/core";
23
+ import type { ServiceConfig } from "../config.js";
24
+ import type { Logger } from "../observability/logger.js";
25
+ import type { Metrics } from "../observability/metrics.js";
26
+ export interface OrgMemoryAdmissionWiring {
27
+ memoryScopeAdmission: RunnerDeps["memoryScopeAdmission"];
28
+ deploymentMemoryScopes: RunnerDeps["deploymentMemoryScopes"];
29
+ }
30
+ /** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
31
+ export declare function createOrgMemoryAdmissionWiring(opts: {
32
+ config: ServiceConfig;
33
+ logger: Pick<Logger, "warn">;
34
+ metrics: Pick<Metrics, "inc">;
35
+ }): OrgMemoryAdmissionWiring;
36
+ //# sourceMappingURL=org-memory.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { fetchPrincipalCaps } from "../config-center/http-client.js";
2
+ import { createMemoryScopeAdmission, createOrgMemoryDirectory, parseOrgDirectoryStatic } from "../org-memory-admission.js";
3
+ const ORG_PREFIX = "org:";
4
+ /** 部署自证 org scope 集(core 准入门的 deployment origin 词表):env 钉的 MEMORY_SCOPE(org 形时)
5
+ * + **单用户部署**的 projects 登记簿 org 键(N2:requirePrincipal !== true 的部署无租户边界,operator
6
+ * 登记簿条目归 deployment)。多租户下登记簿 org 键**不入**此集——它们由 resolve-spec 按 request 盖章,
7
+ * 走目录判决。 */
8
+ function collectDeploymentOrgScopes(config) {
9
+ const scopes = new Set();
10
+ if (config.memoryScope !== undefined && config.memoryScope.startsWith(ORG_PREFIX))
11
+ scopes.add(config.memoryScope);
12
+ if (config.requirePrincipal !== true) {
13
+ for (const project of Object.values(config.projects)) {
14
+ for (const s of project.defaultScopes ?? [])
15
+ if (s.startsWith(ORG_PREFIX))
16
+ scopes.add(s);
17
+ }
18
+ }
19
+ return [...scopes];
20
+ }
21
+ /** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
22
+ export function createOrgMemoryAdmissionWiring(opts) {
23
+ const { config, logger, metrics } = opts;
24
+ const deploymentMemoryScopes = collectDeploymentOrgScopes(config);
25
+ const centerLeg = config.configCenter !== undefined && !config.configCenter.dryRun ? config.configCenter : undefined;
26
+ const staticJson = config.memoryOrgDirectoryJson;
27
+ if (centerLeg !== undefined && staticJson !== undefined) {
28
+ logger.warn("org_memory_directory_env_ignored", {
29
+ reason: "MEMORY_ORG_DIRECTORY_JSON is set but a config-center is wired — the center directory wins; the env table is IGNORED (authorization faces never merge two sources)",
30
+ });
31
+ }
32
+ const directory = centerLeg !== undefined
33
+ ? createOrgMemoryDirectory({
34
+ fetchSection: async (principal) => {
35
+ // etag 恒缺席 ⇒ fetchPrincipalCaps 永不走 304 臂(返回 null 的唯一途径是条件请求),
36
+ // 这里的 null 防御臂只为类型收口;C8 回声核与 schema 校验都在 fetchPrincipalCaps 内。
37
+ const r = await fetchPrincipalCaps(centerLeg.baseUrl, centerLeg.token, principal, undefined, fetch, centerLeg.worker);
38
+ if (r === null)
39
+ throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
40
+ return r.orgMemory;
41
+ },
42
+ grantTtlMs: config.memoryOrgGrantTtlMs,
43
+ unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
44
+ })
45
+ : staticJson !== undefined
46
+ ? createOrgMemoryDirectory({
47
+ staticTable: parseOrgDirectoryStatic(staticJson), // 启动期 fail-loud(坏表拒启动)
48
+ grantTtlMs: config.memoryOrgGrantTtlMs,
49
+ unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
50
+ })
51
+ : undefined;
52
+ // C12 能力探测:多租户 + 登记簿有 org 用面 + 目录源缺席 ⇒ 拒启动(见文件头注)。
53
+ if (directory === undefined && config.requirePrincipal === true) {
54
+ const orgProjects = Object.entries(config.projects)
55
+ .filter(([, p]) => (p.defaultScopes ?? []).some((s) => s.startsWith(ORG_PREFIX)))
56
+ .map(([id]) => id);
57
+ if (orgProjects.length > 0) {
58
+ throw new Error(`multi-tenant deployment has org: keys in projects[${orgProjects.join(", ")}].defaultScopes but NO org-memory directory source ` +
59
+ `(config-center absent/dry-run and MEMORY_ORG_DIRECTORY_JSON unset) — every request selecting these projects would be refused at prepare ` +
60
+ `(memory.admission_required, fail-closed). Wire a config-center or set MEMORY_ORG_DIRECTORY_JSON, or remove the org keys.`);
61
+ }
62
+ }
63
+ const memoryScopeAdmission = directory !== undefined
64
+ ? createMemoryScopeAdmission(directory, {
65
+ mode: config.memoryOrgAdmissionMode,
66
+ // §5 观测:outcome 闭集 metric + 审计日志线(per-principal 归因走日志,principal 不入 metric label)。
67
+ onOutcome: (outcome, details) => {
68
+ metrics.inc("memory_admission_total", { outcome });
69
+ if (outcome !== "ok" || details !== undefined)
70
+ logger.warn("memory_admission_outcome", { outcome, ...details });
71
+ },
72
+ })
73
+ : undefined;
74
+ return { memoryScopeAdmission, deploymentMemoryScopes };
75
+ }
76
+ //# sourceMappingURL=org-memory.js.map
@@ -55,6 +55,12 @@ export interface ResolveSpecCtx {
55
55
  root: string;
56
56
  } | undefined;
57
57
  durableEnabled: boolean;
58
+ /** #152 ([2703] 案二):活体 AskUserQuestion 面(QuestionCoordinator 的判决探针切面)。在场(=
59
+ * ASK_QUESTION_ENABLED)时 durable question 门按活流上下文分腿、spec 不再 stamp QUESTION_AWAITS_RESUME
60
+ * (否则 spec.onQuestion 恒遮蔽 RunnerDeps.onQuestion 的活人腿);缺席时行为与旧形逐字一致。 */
61
+ liveQuestionFace: {
62
+ hasLiveContext(): boolean;
63
+ } | undefined;
58
64
  approvalExemptionStore: ReturnType<StoreBackend["approvalExemption"]> | undefined;
59
65
  singleUserAutoAcceptBaseline: boolean;
60
66
  checkpointStore: ReturnType<NonNullable<StoreBackend["checkpoint"]>> | undefined;
@@ -48,8 +48,27 @@ import { resolveRequestMcp } from "../task-mcp.js";
48
48
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, coercePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, withPermissionMode } from "../task-settings.js";
49
49
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
50
50
  import { redactSecrets } from "../trace/redact.js";
51
+ /** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
52
+ * `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
53
+ * 活流腿(bg/SSE,coordinator.runWithContext 包裹,ALS 判)allow——工具执行落到 RunnerDeps.onQuestion
54
+ * 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade)ask——durable park
55
+ * 原语义逐字保留(此时执行只会拿到 headless 空答,park 才是把问题送到人面前的那条腿)。
56
+ * 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。 */
57
+ function durableQuestionPolicy(live) {
58
+ if (live === undefined)
59
+ return createDurableQuestionPolicy();
60
+ return {
61
+ check(req) {
62
+ if (req.toolName !== "AskUserQuestion")
63
+ return { action: "allow" };
64
+ return live.hasLiveContext()
65
+ ? { action: "allow" }
66
+ : { action: "ask", message: "AskUserQuestion: awaiting a human answer (durable)" };
67
+ },
68
+ };
69
+ }
51
70
  export function createResolveSpec(ctx) {
52
- 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, } = ctx;
71
+ 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;
53
72
  // Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
54
73
  // bound per request); identity/session/policy stay server-side. Never taken from the request body.
55
74
  // design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
@@ -677,7 +696,9 @@ export function createResolveSpec(ctx) {
677
696
  // (`writeScope:null` — the engine materializes/reads but harvest commits nothing). Per-request (not a
678
697
  // stored flag) → no new per-session state; the shell carries the toggle. RESUME re-runs resolveSpec
679
698
  // from the persisted body, so a paused run stays paused across resume legs.
680
- memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
699
+ // 件A origin 盖章(#148 件3④):部署形态维随 requirePrincipal——多租户下登记簿种子(projectId
700
+ // 选定)按 request 盖章走 core 准入门,单用户一律 deployment(v4 §1 N2;零 org 键=整键缺席零迁移)。
701
+ memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }) : undefined,
681
702
  // Scenario-provided capabilities (e.g. code-review = repo tools + reviewer subagents + prompt).
682
703
  // RFC A2: the SelectEnvironment tool rides after the scenario's tools (spec.tools is ADDITIVE to core's
683
704
  // built-in roster — prepare-task mounts first-party tools separately). Only when the image chain is live.
@@ -711,10 +732,11 @@ export function createResolveSpec(ctx) {
711
732
  // operator with approval intent but no checkpointStore falls to `undefined` (core's UNGATED warning).
712
733
  // Durable ask (TC-5.4, core 1.95): in durable mode an AskUserQuestion call suspends like an F4 gate —
713
734
  // the question policy adjudicates it `ask`, the operator answers out-of-band, and the resume carries
714
- // the QuestionAnswer (server.ts `body.answer` → onQuestion closure). Suspend side mounts the tool with
715
- // QUESTION_AWAITS_RESUME (it must never run pre-suspend; reaching it = wiring bug, typed throw).
735
+ // the QuestionAnswer (server.ts `body.answer` → onQuestion closure). #152([2703] 案二): the question
736
+ // policy is LIVE-AWARE when the QuestionCoordinator is wired a live-stream leg adjudicates `allow`
737
+ // (the live human answers over the stream) instead of unconditionally parking; see durableQuestionPolicy.
716
738
  toolPolicy: durableEnabled
717
- ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
739
+ ? combinePolicies(durableQuestionPolicy(liveQuestionFace), createDurableAskPolicy({
718
740
  requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
719
741
  // The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
720
742
  // durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
@@ -761,7 +783,11 @@ export function createResolveSpec(ctx) {
761
783
  isCascade: body.cascade === true,
762
784
  scope: encodeCheckpointScope(auth?.principal),
763
785
  })),
764
- onQuestion: QUESTION_AWAITS_RESUME,
786
+ // #152([2703] 案二):sentinel 只在活体面缺席时 stamp。core 取 `spec.onQuestion ?? deps.onQuestion`
787
+ // (spec 位恒赢),无条件 stamp 会把 ASK_QUESTION_ENABLED 部署的活人腿永久遮蔽成 durable park。
788
+ // 活体面在场 ⇒ 键缺席,RunnerDeps.onQuestion 的 coordinator 生效;park 与否由上面的判决门分腿。
789
+ // (suspend 侧的「未答 approve」拒绝不靠 sentinel:server.ts decide 路由 pre-CAS 400 守着。)
790
+ ...(liveQuestionFace === undefined ? { onQuestion: QUESTION_AWAITS_RESUME } : {}),
765
791
  }
766
792
  : {}),
767
793
  };
@@ -49,6 +49,10 @@ export interface RunnerDepsCtx {
49
49
  root: string;
50
50
  } | undefined;
51
51
  memorySyncRunner: MemorySyncRunner | undefined;
52
+ /** design/170 件A(#148 件3③):org 记忆准入两 seam 的装配产物(boot/org-memory.ts)。进**共享
53
+ * 基座**——主/sub 两 Runner 必须同源:委托子代的 prepare 同样跑准入(委托冻结的重判腿),漏挂
54
+ * subRunner=子代平面绕过目录判决。 */
55
+ orgMemoryAdmission: import("./org-memory.js").OrgMemoryAdmissionWiring;
52
56
  toolResultStore: ToolResultStoreFull | undefined;
53
57
  sessionPolicyStore: ReturnType<StoreBackend["sessionPolicy"]> | undefined;
54
58
  runtimeCapsResolver: RunnerDeps["runtimeCapsResolver"];
@@ -68,7 +72,7 @@ export interface RunnerDepsCtx {
68
72
  }
69
73
  /** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
70
74
  * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
71
- export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore">;
75
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes">;
72
76
  /**
73
77
  * design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
74
78
  * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
@@ -97,7 +101,7 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
97
101
  * 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
98
102
  */
99
103
  /** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
100
- export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore">;
104
+ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore" | "orgMemoryAdmission">;
101
105
  export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
102
106
  export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
103
107
  //# sourceMappingURL=runner-deps.d.ts.map
@@ -49,6 +49,12 @@ export function createSharedRunnerDeps(ctx) {
49
49
  // E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
50
50
  // carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
51
51
  sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
52
+ // design/170 件A:org 记忆准入两 seam(core 5.13.0)。共享基座=主/sub 同源(委托子代 prepare 的
53
+ // 委托冻结重判腿同样过目录);resolver 缺席=core 对 request-origin org 铸 admission_required
54
+ // fail-closed,deployment 自证集空=键缺席(诚实缺席>空数组——core 按 Set 语义消费,空集与缺席
55
+ // 同义,但缺席让 deps 字面量不携带死键)。
56
+ memoryScopeAdmission: ctx.orgMemoryAdmission.memoryScopeAdmission ? ctx.orgMemoryAdmission.memoryScopeAdmission : undefined,
57
+ deploymentMemoryScopes: ctx.orgMemoryAdmission.deploymentMemoryScopes && ctx.orgMemoryAdmission.deploymentMemoryScopes.length > 0 ? ctx.orgMemoryAdmission.deploymentMemoryScopes : undefined,
52
58
  // design/166-T1:治理窗两键**成对**且进共享基座——主/sub/hook 三 runner 全覆盖(漏挂 subRunner=
53
59
  // 子代用量不进窗,治理可被 subagent 刷穿;双记不存在:core stats.nested 是旁路侧栏不折入
54
60
  // stats.tokens,governance.commit 是 per-任务 delta 制)。store 只在 config.usageWindows 配置时
@@ -203,8 +209,11 @@ export function createRunnerDeps(ctx) {
203
209
  // E23: the live-only inbound-elicitation seam. core invokes it only for servers that opted in via
204
210
  // McpServerSpec.elicitation (default OFF) AND only when this is wired — both must hold (doubly fail-closed).
205
211
  onElicit: elicitation ? elicitation.elicit : undefined,
206
- // §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here; the
207
- // DURABLE leg's spec.onQuestion (QUESTION_AWAITS_RESUME) OVERRIDES this per-task so a disconnected-human ask suspends.
212
+ // §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here.
213
+ // #152([2703] 案二): with the coordinator wired the durable leg NO LONGER stamps spec.onQuestion
214
+ // (QUESTION_AWAITS_RESUME) — park-vs-live is decided per-leg by resolve-spec's live-aware question policy
215
+ // (live-stream leg → this coordinator; no-live-context leg → durable park). The sentinel is stamped only
216
+ // when the coordinator is absent (ASK_QUESTION_ENABLED off).
208
217
  onQuestion: question ? question.question : undefined,
209
218
  // [816]/[820]②: the live tool-approval seam (core `resolveAsk` — `spec.onAsk ?? deps.onAsk`). ALS-routed like
210
219
  // onQuestion: a leg wrapped by the coordinator's runWithContext reaches the human.
@@ -38,6 +38,7 @@ export declare function fetchPrincipalCaps(baseUrl: string, token: string, princ
38
38
  scenario?: ScenarioRuling;
39
39
  execution?: ExecutionRuling;
40
40
  executionDrift?: string;
41
+ orgMemory?: unknown;
41
42
  etag?: string;
42
43
  } | null>;
43
44
  /** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
@@ -5,7 +5,7 @@
5
5
  * facade re-exports every symbol below unchanged).
6
6
  */
7
7
  import { createHash } from "node:crypto";
8
- import { readEffectiveWire } from "@sema-agent/registry-core";
8
+ import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/registry-core";
9
9
  import { centerPromptsFromEffective } from "../prompts-domain-validate.js"; // #100 worker 腿 prompts 边界归一(中立叶)
10
10
  /** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
11
11
  * payload that is not an effective config at all(非对象 / version 非有限数 / gate 域坏形——[2281] 裁B)。
@@ -94,7 +94,22 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
94
94
  }
95
95
  if (!res.ok)
96
96
  throw new Error(`config-center principal-caps HTTP ${res.status}`);
97
- const body = (await res.json());
97
+ // 边界读=schema(clay 宪法 [2704] §2,同源锚=registry-core PrincipalCapsWire):治理面
98
+ // runtimeCaps/budget 严格+passthrough(值漂移整体拒 ⇒ 本函数 throw ⇒ resolver fail-close,
99
+ // 「outage 绝不静默 GRANT」同向);[565]/[580] fail-open 面与 orgMemory(C3 失败域隔离)在
100
+ // schema 里保持宽读,归一/校验仍在下方与目录腿(极性不因 schema 化翻转)。
101
+ const parsedBody = PrincipalCapsWire.safeParse(await res.json());
102
+ if (!parsedBody.success) {
103
+ 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
+ }
105
+ const body = parsedBody.data;
106
+ // design/170 件A C8(响应 principal 回声核,串租户防线一行):center 两条 ?principal= 腿的 body 都
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
+ }
98
113
  return {
99
114
  runtimeCaps: body.runtimeCaps ?? null,
100
115
  configured: Boolean(body.configured),
@@ -120,28 +135,37 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
120
135
  // and a drifted `required` into silent non-enforcement. Now: drift ⇒ DROP the whole ruling (fail-open,
121
136
  // gate passes) + `executionDrift` so the caller's audit line fires (the resolver routes it to onError).
122
137
  ...((() => {
123
- const ex = body.execution;
124
- if (ex === undefined || ex === null)
138
+ // [2704] §1:unknown 面的窄化走运行时类型谓词(真检查),不走断言。
139
+ const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
140
+ const exRaw = body.execution;
141
+ if (exRaw === undefined || exRaw === null)
125
142
  return {};
126
- if (typeof ex.required !== "boolean")
127
- return { executionDrift: `execution.required is ${typeof ex.required}, expected boolean — ruling dropped (fail-open)` };
128
- if (!Array.isArray(ex.allowedLanes))
129
- return { executionDrift: `execution.allowedLanes is ${typeof ex.allowedLanes}, expected string[] — ruling dropped (fail-open)` };
130
- const execution = { required: ex.required, allowedLanes: ex.allowedLanes.filter((s) => typeof s === "string") };
143
+ if (!isRecord(exRaw))
144
+ return { executionDrift: `execution is ${Array.isArray(exRaw) ? "array" : typeof exRaw}, expected object — ruling dropped (fail-open)` };
145
+ if (typeof exRaw.required !== "boolean")
146
+ return { executionDrift: `execution.required is ${typeof exRaw.required}, expected boolean — ruling dropped (fail-open)` };
147
+ if (!Array.isArray(exRaw.allowedLanes))
148
+ return { executionDrift: `execution.allowedLanes is ${typeof exRaw.allowedLanes}, expected string[] — ruling dropped (fail-open)` };
149
+ const execution = { required: exRaw.required, allowedLanes: exRaw.allowedLanes.filter((s) => typeof s === "string") };
131
150
  // registry-core 0.10.1-0.10.2:execution.sessionMirror 宽读。observation-only 子面
132
151
  // (server 非执法端,见 ExecutionRuling.sessionMirror 注),所以形状漂移只丢**本子面**并走 executionDrift
133
152
  // 审计线(resolver 路由到 onError),**绝不**连带丢 lane ruling(那是真执法面,不能被观测子面拖垮)。
134
153
  // required 缺省 false = registry-core zod 默认(便利态);engineUrl 必须非空 string。
135
- if (ex.sessionMirror !== undefined && ex.sessionMirror !== null) {
136
- const sm = ex.sessionMirror;
137
- const smOk = typeof sm === "object" && typeof sm.engineUrl === "string" && sm.engineUrl.length > 0 && (sm.required === undefined || typeof sm.required === "boolean");
138
- if (smOk)
154
+ if (exRaw.sessionMirror !== undefined && exRaw.sessionMirror !== null) {
155
+ const sm = exRaw.sessionMirror;
156
+ if (isRecord(sm) && typeof sm.engineUrl === "string" && sm.engineUrl.length > 0 && (sm.required === undefined || typeof sm.required === "boolean")) {
139
157
  execution.sessionMirror = { engineUrl: sm.engineUrl, required: sm.required === true };
140
- else
158
+ }
159
+ else {
141
160
  return { execution, executionDrift: "execution.sessionMirror malformed — mirror observation dropped (fail-open; lane ruling kept)" };
161
+ }
142
162
  }
143
163
  return { execution };
144
164
  })()),
165
+ // design/170 件A:org 记忆解析段**原值透传**(键在场保在场、缺席保缺席——缺席=旧 center 能力握手,
166
+ // org 目录腿据此判 section_absent 瞬时;形状校验是 org-memory-admission 模块用 PrincipalOrgMemoryWire
167
+ // 的事,本腿保持薄透传不预判)。
168
+ ...("orgMemory" in body ? { orgMemory: body.orgMemory } : {}),
145
169
  etag: res.headers.get("etag") ?? undefined,
146
170
  };
147
171
  }
@@ -205,6 +205,21 @@ export interface ServiceConfigFlat {
205
205
  maxPushEntries?: number;
206
206
  maxPullEntries?: number;
207
207
  };
208
+ /** design/170 件A(clay 三裁 [2687]):org 记忆准入模式。`enforce`(默认)=判决即结果;`audit`=判决
209
+ * 照算但纯观察零行为变化(不整拒、不窄化 writeScope,只记 would-deny/would-narrow)——运维诊断位,
210
+ * 非发布步骤(audit-first 灰度步按裁2 免除)。env `MEMORY_ORG_ADMISSION_MODE`(enumEnv 二值)。 */
211
+ memoryOrgAdmissionMode: "audit" | "enforce";
212
+ /** 件A 单机形目录:env `MEMORY_ORG_DIRECTORY_JSON` 原文(`Record<principal, Record<org:scope, {write?}>>`)。
213
+ * config 解析期即整段校验(parseOrgDirectoryStatic,fail-loud);此处存**原文**(config=纯数据,
214
+ * Map 在装配点重建)。与 config-center 目录腿互斥的裁决在装配点(两者都在=center 腿胜,env 表忽略
215
+ * 并 warn——授权面不做双源合并)。 */
216
+ memoryOrgDirectoryJson?: string;
217
+ /** 件A 目录 granted(含负结果)缓存 TTL(ms)。新任务/新 resume 腿的准入判决滞后上限=此值(「TTL 即
218
+ * 有界 LKG」);与同仓保护面 grant TTL 对齐默认 60s。env `MEMORY_ORG_GRANT_TTL_MS`(numEnvBounded)。 */
219
+ memoryOrgGrantTtlMs: number;
220
+ /** 件A 目录 unavailable 退避窗(ms)——取数失败的退避语义(bound center hammering),不是策略负结果。
221
+ * 默认 10s。env `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS`(numEnvBounded)。 */
222
+ memoryOrgUnavailableBackoffMs: number;
208
223
  /** design/48 v1b + design/61: opt-in remote execution backend (PEER adapters, chosen by REMOTE_EXEC). Unset
209
224
  * (OA/review deployments) → in-process stub env, zero behavior change. Secrets (apiKey/privateKey) ONLY from env.
210
225
  * - `e2b` → per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
@@ -893,7 +908,7 @@ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" |
893
908
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
894
909
  export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
895
910
  /** 组:memory(记忆面 + TOC 同步腿)。 */
896
- export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
911
+ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
897
912
  /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
898
913
  * 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
899
914
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
package/dist/config.js CHANGED
@@ -491,9 +491,11 @@ function parseStoreDomain(ctx) {
491
491
  user: env("MYSQL_USER"),
492
492
  password: env("MYSQL_PASSWORD", ""),
493
493
  database: env("MYSQL_DATABASE"),
494
- connectionLimit: process.env.MYSQL_POOL_SIZE
495
- ? Number(process.env.MYSQL_POOL_SIZE)
496
- : undefined,
494
+ // 鲁棒性批5 A2(2026-08-05):此前裸 `Number(process.env.MYSQL_POOL_SIZE)` ——一个手滑值(如
495
+ // "20;drop") → NaN,driver 的 `connectionLimit` 直接拿到 NaN 而不是走池大小默认,行为因驱动
496
+ // 而异(未必 fail-loud)。optFinitePositiveEnv 与 dbQueryTimeoutMs 同款:非法值回默认(undefined
497
+ // = driver 默认池大小)+ S20 boot 警告,合法值照常生效。
498
+ connectionLimit: optFinitePositiveEnv("MYSQL_POOL_SIZE"),
497
499
  }
498
500
  : undefined,
499
501
  pg: needsDb && dbBackend === "pg"
@@ -503,7 +505,8 @@ function parseStoreDomain(ctx) {
503
505
  user: env("PG_USER"),
504
506
  password: env("PG_PASSWORD", ""),
505
507
  database: env("PG_DATABASE"),
506
- connectionLimit: process.env.PG_POOL_SIZE ? Number(process.env.PG_POOL_SIZE) : undefined,
508
+ // MYSQL_POOL_SIZE (鲁棒性批5 A2)
509
+ connectionLimit: optFinitePositiveEnv("PG_POOL_SIZE"),
507
510
  }
508
511
  : undefined,
509
512
  // S9 deeper fix: per-query DB timeout (see the interface doc). Soft knob — a typo degrades to the default
@@ -984,6 +987,18 @@ function parseMemoryDomain(ctx) {
984
987
  // design/158 B4 ② 的 D 家族四枚里,唯一落在 ServiceConfig 上的就是这枚(其余三枚只在使用点懒读,
985
988
  // 预登记留在 orchestration 域 —— 顺序与改前一致:本枚的弃用通知仍先于那三枚)。
986
989
  const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
990
+ // design/170 件A(clay 三裁 [2687]):org 记忆准入四旋钮。模式=enumEnv 二值非布尔(N9),默认
991
+ // enforce(裁2:audit-first 灰度步免除,audit 降级为运维诊断位);TTL/退避窗=numEnvBounded 启动期
992
+ // 报错(env-numeric-fail-loud 门,F6:optFinitePositiveEnv 的静默回默认恰是要消灭的形)。单机目录
993
+ // env 表在这里就整段校验(fail-loud 越早越好——一张半坏的授权表比拒绝启动更危险),config 上只存
994
+ // 原文(纯数据;Map 在装配点重建)。
995
+ const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
996
+ // 原文存 config(纯数据);整段 fail-loud 校验(parseOrgDirectoryStatic)在装配点——它传递值引
997
+ // security.js(assertPrincipalShape),而 base config 层禁值引 auth 模块(design/158 A4 分层),
998
+ // 校验不能放这里。装配点仍在启动期,坏表照样拒启动。
999
+ const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
1000
+ const memoryOrgGrantTtlMs = numEnvBounded("MEMORY_ORG_GRANT_TTL_MS", "60000", 1000, 3_600_000);
1001
+ const memoryOrgUnavailableBackoffMs = numEnvBounded("MEMORY_ORG_UNAVAILABLE_BACKOFF_MS", "10000", 500, 600_000);
987
1002
  return {
988
1003
  memoryEngineEnabled,
989
1004
  memoryEngineDir: process.env.MEMORY_ENGINE_DIR || undefined,
@@ -997,6 +1012,10 @@ function parseMemoryDomain(ctx) {
997
1012
  memoryEngineBackend,
998
1013
  memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
999
1014
  ...(memorySync ? { memorySync } : {}),
1015
+ memoryOrgAdmissionMode,
1016
+ ...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
1017
+ memoryOrgGrantTtlMs,
1018
+ memoryOrgUnavailableBackoffMs,
1000
1019
  projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (3.0.0 起 legacy PROJECT_MEMORY_DISABLED = fail-loud 墓碑)
1001
1020
  syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
1002
1021
  };
@@ -1177,7 +1196,12 @@ function parseOrchestrationDomain(ctx) {
1177
1196
  ...(process.env.DOCKER_HOST ? { dockerHost: process.env.DOCKER_HOST } : {}),
1178
1197
  ...(process.env.DOCKER_MEMORY ? { memory: process.env.DOCKER_MEMORY } : {}),
1179
1198
  ...(process.env.DOCKER_CPUS ? { cpus: Number(process.env.DOCKER_CPUS) } : {}),
1180
- ...(process.env.DOCKER_PIDS_LIMIT ? { pidsLimit: Number(process.env.DOCKER_PIDS_LIMIT) } : {}),
1199
+ // 鲁棒性批5 A1(2026-08-05):此前裸 `Number(process.env.DOCKER_PIDS_LIMIT)`——非数字值(如
1200
+ // 手滑的 "512;") → NaN,而消费端(local-docker 执行环境)对 `pidsLimit` 的守卫是
1201
+ // `?? 512`(只挡 null/undefined),NaN 穿透守卫;随后 `NaN > 0` 恒假,`--pids-limit` 整个
1202
+ // 从 docker run 参数里省略——运维以为设了 fork-bomb 背栓,实际背栓被静默卸掉。
1203
+ // optFinitePositiveEnv:非法值回 undefined(消费端默认 512 生效)+ S20 boot 警告。
1204
+ ...((n) => (n !== undefined ? { pidsLimit: n } : {}))(optFinitePositiveEnv("DOCKER_PIDS_LIMIT")),
1181
1205
  ...(boolEnv("DOCKER_DROP_CAPS", false) ? { dropAllCaps: true } : {}),
1182
1206
  ...(process.env.DOCKER_NETWORK
1183
1207
  ? { network: enumEnv("DOCKER_NETWORK", "bridge", ["none", "bridge", "host"]) }
@@ -1354,7 +1378,8 @@ const APPROVAL_GROUP_KEYS = [
1354
1378
  ];
1355
1379
  const MEMORY_GROUP_KEYS = [
1356
1380
  "memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
1357
- "memorySync", "projectMemoryEnabled", "syncImportLeaseStaleSec",
1381
+ "memorySync", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
1382
+ "projectMemoryEnabled", "syncImportLeaseStaleSec",
1358
1383
  ];
1359
1384
  const AUTH_GROUP_KEYS = [
1360
1385
  "authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
@@ -1,3 +1,12 @@
1
+ import type { TaskNotificationPayload } from "@sema-agent/core";
2
+ /** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
3
+ * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
4
+ * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
5
+ * agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
6
+ * 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
7
+ * 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
8
+ * 换装 import 删本镜像(提货件已上板)。 */
9
+ export declare function isFleetAgentTerminalNotification(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
1
10
  /** The fleet vocabulary the shell renders (MF-Fleet `FleetTask.status`). The service maps its run/workflow status
2
11
  * onto this neutral set; `awaiting approval` = a needs-review/plan-approval park, `waiting` = a durable suspend.
3
12
  *
@@ -22,6 +22,16 @@
22
22
  */
23
23
  import { EventEmitter } from "node:events";
24
24
  import { redactSecrets } from "../trace/redact.js";
25
+ /** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
26
+ * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
27
+ * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
28
+ * agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
29
+ * 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
30
+ * 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
31
+ * 换装 import 删本镜像(提货件已上板)。 */
32
+ export function isFleetAgentTerminalNotification(n) {
33
+ return n.task_type === "background_agent" && (n.status === "completed" || n.status === "failed" || n.status === "killed" || n.status === "cancelled");
34
+ }
25
35
  /**
26
36
  * The process-local fleet aggregation bus. Holds the current active set + fans out deltas to SSE subscribers.
27
37
  * Upserts MERGE (a partial delta patches the existing row), so a publisher can emit just the field that changed
@@ -420,6 +420,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
420
420
  }
421
421
  // §4 active-run guard (don't drop a live append) + per-session import lease (don't let two staged imports race
422
422
  // to commit the same session). Both 409.
423
+ // `runStore?.` 的可选链不是守卫豁免面(鲁棒性批5 #7 亲验定性,2026-08-06):runStore 仅在
424
+ // **backend 整体缺席**时才 undefined(tidb/pg/local 三形态的 backend.run() 全都在,main.ts:378),
425
+ // 而 backend 缺席时本路由的 Phase A 已在 `beginImportStaging` 探测处硬 501(capability.session_store_
426
+ // required)——「守卫被跳过而 sync 仍可达」的组合按构造不存在(能力面与路由解析同真值)。
423
427
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
424
428
  if (activeTaskId) {
425
429
  sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });