/** [ref] A1: the ServiceConfig TYPE face, split out of the 1.8k-line loader module (config.ts) * so pure-type consumers (http/server, plugins/*, hooks/hook-llm, security, brain, sema-registry) * depend on a LEAF with zero runtime edges — this is the substantive step that dissolves the * type-closed edges into config.ts (lens2 §A). TYPES ONLY: loadConfig and every runtime value stay * in config.ts, which re-exports these names so existing `from "./config.js"` importers keep * compiling unchanged. All imports below are top-level `import type` (erased — never a load edge, * see test/module-cycle-gate.test.ts header on verbatimModuleSyntax). */ import type { ReadFaceSource } from "./read-face-posture.js"; import type { PostureSource } from "./posture-source.js"; import type { SealedKeyPoison } from "./sealed-key.js"; import type { A2aServerSpec, CompliancePosture, CrossSessionDialogExpiry, CrossSessionInboundSetting, FileHistoryRetentionPolicy, LockedKey, McpServerSpec, Model, ModelRoles, RetentionPolicy, WriteProtectedEntry } from "@sema-agent/core"; import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js"; import type { ElicitationThrottle } from "./elicitation.js"; import type { QuestionThrottle } from "./question.js"; import type { UnattendedApprovalPolicy } from "./tool-approval.js"; import type { InfraCostRates } from "./observability/cost-taxonomy.js"; import type { Autonomy, CommandRule } from "./runtime-governance.js"; import type { GitApiKind } from "./git-api-kind.js"; /** [ref]:记忆向量面的 embedder 坐标(env `MEMORY_EMBEDDER_*` 解析结果;单一属主 = 本类型, * `plugins/memory-embedder.ts` 的工厂直接消费它,不复制形状)。语义与三问见 * {@link ServiceConfigFlat.memoryEmbedder}。纯数据 —— 端点原文照存(拼 `/embeddings` 是 transport * 的活,与 `memorySync.url` 的尾斜杠口径一致)。 */ export interface MemoryEmbedderConfig { /** OpenAI 兼容服务的基址或完整 `/embeddings` 端点(两种写法都收,拼接在工厂里归一)。 */ endpoint: string; /** 模型名,原样进请求体 `{ model, input }`。 */ model: string; /** 声明维度(正整数)。响应向量长度与它不等 = 抛,绝不截断/补零。 */ dimensions: number; /** 单次 embed 的活性上限(ms)。`MEMORY_EMBEDDER_TIMEOUT_MS`,默认 30s。 */ timeoutMs: number; /** 可选 Bearer(自托管 TEI/vllm/ollama 通常不需要;公有云端点需要)。 */ apiKey?: string; } /** * [ref] §4 —— 托管留存的**灰度档**闭集。`as const` 数组是词表的唯一真源(config 解析、能力位、审计行 * 的 `mode` 列共读它);两词之外的值在启动期响亮拒 —— 一个拼错的档位静默落回 `audit-only` 会让运维 * 以为在删、其实没删,而拼成别的方向就是反过来那件更坏的事。 */ export declare const RETENTION_MODES: readonly ["audit-only", "enforce"]; export type RetentionMode = (typeof RETENTION_MODES)[number]; /** [ref] §3/§4 —— sweep lane 的节律与灰度档(语义与三问见 {@link ServiceConfigFlat.retentionSweep})。 */ export interface RetentionSweepConfig { /** `audit-only` = 照跑照判、只记 `audit_only` 行,**不调任何破坏性方法**;`enforce` = 真删。默认前者。 */ mode: RetentionMode; /** sweep 节律(秒)。`0` = **关**(默认);租约 TTL = 2×本值。 */ intervalSec: number; } /** * [ref] D2 —— 周期腿节律的**下界**(秒)。一轮 consolidation 读全库(~1e5 prompt tokens)且 * core 自带 24h 的时间闸,比这更密的节律只可能是把分钟/毫秒当成秒写的手滑。夹取(不是拒启)方向安全: * 夹上去只会**少**跑,而这条腿花的是钱。 * ⚠️ 今天这条夹取**还够不着**:本 build 没有周期腿,任何正值在解析期就拒启(见 * {@link MemoryConsolidationDriverConfig.intervalSec})。常量在拒启文案里被单点引用,腿落地那批撤掉 * 那道拒之后它即刻生效 —— 留着是为了「那天不必有人再猜一个数」。 */ export declare const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300; /** * S-220 —— **`resilience` 块缺省的唯一属主**(S-202 的单源从一根扩到整族)。 * * 这是一张**表**,不是六个常量:缺省长在三条腿上(解析器腿 `config.ts` 的 `numEnv`/`boolEnv` 缺省、 * 目录腿 `config-catalog.ts` 的 `staticDefault`、`brain.ts` 的 `DEFAULT_RESILIENCE` —— config 对象 * 没带 resilience 块时的同姿态兜底),而三条腿此前**各写一份字面量**。S-202 只把 `firstTokenTimeoutMs` * 一根收成单源,于是剩下五根还在「改一处漏一处 = 目录对外宣告的缺省与真跑的不是同一代」这条病里 —— * 按类修的形是把**整族**收进一张表,让三条腿都只许引用它,而不是再加五个同形常量(六个常量 = 六份 * 要各自记得引用的纪律;一张表 = 一条)。 * * 每行带自己的 **env 键名** —— 它是这一行的**身份**,给消费门当锚(目录行查名、三腿对账)。 * ⚠️ 但 `config.ts` / `config-catalog.ts` 的读点仍写**字面** env 名,不写 `RESILIENCE_DEFAULTS.x.env`: * D1 双向对账门(`test/config-catalog.test.ts` ⑦)是 AST 扫 `numEnv("NAME", …)` 的字面名来数「src/ 在读 * 哪些键」,改成间接引用会让这六个键在那道门眼里整体消失(实测:6 个幽灵键 + 动态读棘轮 27→33)—— * 拿一条真在岗的对账门去换一处写法上的统一是亏的。键名的同源由那道门自己咬:src/ 字面名 ⟺ 目录行名 * ⟺ 本表的 `env`(S-220 那一格按 `env` 查目录行,查不到当场红),三方互钉,没有静默漂移的空间。 * `satisfies Readonly>` 双向咬:块上加字段而表里没行 * ⇒ tsc 红(加字段的人被迫当场回答「它的出厂缺省是多少、归哪个 env」);表里多一行块上没有的键同样红。 * * ⚠️ 语义面**一个数字都没动**:`0 = 关`(三个看门狗)与「熔断默认关」照旧,env 恒赢。 * 首 token 那一行的 600_000 由来(S-202 clay 裁 A):自托管后端在长 prefill(大上下文 / 冷缓存 / * 排队)下首字合理地要等几分钟,120s 会把一次**正常**的慢启动判成 `[network]` 失败去重试,而重试 * 再交同一份 prompt 给同一个排队中的后端只会更慢更贵;依赖旧姿态的部署显式写回 * `MODEL_FIRST_TOKEN_TIMEOUT_MS=120000` 即可。 * * 🔴 **常量在此、别处只许引用**:`test/config-catalog.test.ts` 的 S-220 两格咬 —— 一格按表逐行钉 * 三腿同值(目录 staticDefault / 解析器实算值 / {@link buildDefaultResilience}),一格扫全 `src/` * 判「字面量」这一类写法为红(下一份镜像出生即红)。 */ export declare const RESILIENCE_DEFAULTS: { readonly connectTimeoutMs: { readonly env: "MODEL_CONNECT_TIMEOUT_MS"; readonly value: 30000; }; readonly firstTokenTimeoutMs: { readonly env: "MODEL_FIRST_TOKEN_TIMEOUT_MS"; readonly value: 600000; }; readonly idleTimeoutMs: { readonly env: "MODEL_IDLE_TIMEOUT_MS"; readonly value: 300000; }; readonly circuitBreaker: { readonly env: "MODEL_CIRCUIT_BREAKER"; readonly value: false; }; readonly failureThreshold: { readonly env: "MODEL_CB_FAILURE_THRESHOLD"; readonly value: 5; }; readonly cooldownMs: { readonly env: "MODEL_CB_COOLDOWN_MS"; readonly value: 30000; }; }; /** * S-220 —— 「config 对象没带 resilience 块」时的整块兜底,**逐字段派生自 {@link RESILIENCE_DEFAULTS}**。 * * 返回类型把键集钉死(缺键 / 多键都是 tsc 红),所以这里剩下的唯一手写物是键名本身,值一个字面量都没有。 * 每次调用铸一只新对象(共享一只导出常量会被消费方就地改掉,那是另一条病)。 */ export declare function buildDefaultResilience(): ServiceConfigFlat["resilience"]; /** * [ref] D1/D2 —— consolidation driver 阀门的三根旋钮(语义与三问见 * {@link ServiceConfigFlat.memoryConsolidationDriver})。纯数据。 */ export interface MemoryConsolidationDriverConfig { /** 阀门(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,缺省 `off`)。`false` = 零装配零探针: * 座不解析、引擎不构造、两个 admin 口整个不挂(404 族,**不是** 501)、能力位翻假。 */ enabled: boolean; /** * 周期腿节律(秒;env `MEMORY_CONSOLIDATION_INTERVAL_SEC`,缺省 `0` = 不周期跑)。 * 🔴 **本 build 里任何正值都拒启**:结构上没有周期腿(没有可用的真互斥基建 —— 全文与偏离登记见 * `boot/memory-consolidation.ts` 顶注与 designs/378 §遗留 遗-1)。所以本字段今天**恒为 0**;它留在 * 类型上是因为拒启文案与周期腿落地那一批共用同一个字段名与同一条下界语义 * ({@link MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC})。 */ intervalSec: number; /** 阀门的 scope 表(env `MEMORY_CONSOLIDATION_SCOPES`,逗号表;缺省空表)。v1 的两个用途: * ①恰好一条时 = 两个 admin 口省略 `scope` 的**唯一**缺省(两条及以上恒不猜——在两个库之间猜一个 * 去花钱是本设计要消灭的形);②周期腿落地时的 scope 源(设计稿 D2)。 * 🔴 **不是授权边界**:本仓今天没有任何授权源能对一个 operator 收窄 scope(理由与亲读依据逐字见 * `src/memory-operator-faces.ts` 的 `erasureEnvelopeRefusal` 头注),在本层拿它当白名单就是**发明 * 第二套授权语义**。显式传的 scope 照跑,表只管缺省。 */ scopes: readonly string[]; /** * S-362 / core 7.21.0 [ref] —— **`RunnerDeps.autoRunOnRecommendation` 的部署席** * (env `MEMORY_AUTO_CONSOLIDATION=on|off`;**缺席 = 不写那个键**,由 core 的具名缺省 * `AUTO_RUN_ON_RECOMMENDATION_DEFAULT` 作答)。 * * 🔴 **三态,不是布尔**,而且三态各有各的意思 —— 折成布尔会当场丢掉本旋钮存在的**唯一**理由: * · `undefined`(env 未设)= 不写 `RunnerDeps` 那个键 ⇒ 读 core 的具名缺省。core 7.21.0 发的是 * `false`,而 clay [ref] 已裁**缺省开**、core 7.21.1 翻常量 —— 所以这一格的取值**会随提货漂**, * 这正是它该有的样子(缺省的属主在 core)。 * · `false`(`MEMORY_AUTO_CONSOLIDATION=off`)= **显式关,胜过缺省**。这是本旋钮的**关闭口**: * 一台不愿意让记忆内容自动外流的部署,靠它在 core 把缺省翻开之后仍然保持关闭。 * · `true`(`=on`)= 武装。**武装是一条出口决定,不是性能旋钮**:每个建议任务之后会有一次整库整理跑, * 记忆内容自动外流到配置的整理模型 —— 设它之前请读 `boot/memory-consolidation.ts` 的 EGRESS 段。 * * ⚠️ **前置 = `MEMORY_CONSOLIDATION_DRIVER=on`**(且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席 * (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)由**同一只已解析座**接上 * (S-399,`boot/runner-deps.ts` 的 `ctx.consolidationSeat` 那一段)⇒ 本旋钮真的生效;阀门关着 ⇒ 两席 * 一个键都不写,而 core 对「武装但没接整理席」的处置是**每一次 prepare 都抛** `config.auto_consolidation` * —— 那会买到一台**启动成功、每个任务都死**的机器,所以本仓把那一拒提前到启动期响亮拒 * (`assertAutoConsolidationWirable`,读的是刚铸出来的那只 deps 而不是一个手抄的布尔),与 * `MEMORY_CONSOLIDATION_DRIVER` 的 D1「安全/成本控件不得半开」同一条。 * 🔴 **本段自 S-399 改判**:S-362 那一批这里写的是「本 build 上 `on` 一定拒启(本仓没接整理席)」—— * 同一批的后半场把两席接上之后,那句话成了谎。旧世界断言留在树上比欠账更危险(7.81.0 整批的教训), * 所以真相变了就地改,不留「将来某一版」的口气。 */ autoRunOnRecommendation?: boolean; } /** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the * scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */ export interface ScopedMcpServer { scenarios: string[]; spec: McpServerSpec; } /** The deployment's MCP declaration, resolved (`resolveMcpServers`). `servers` = what actually MOUNTED (env-NAME refs * resolved, `enabled:false` / missing-ref / malformed-`toolFaces` entries skipped, each with its own loud warn); * `reservedNames` = EVERY name the deployment declared, mounted or not. A name the deployment declared is CLOSED to * request-side injection (`resolveRequestMcp`) whether or not it mounted — otherwise "skipped" would silently mean * "free for the caller to refill without the operator's `toolFaces` / `allowTools`" (车CF codex r2 [high]). */ export interface CenterMcp { servers: ScopedMcpServer[]; reservedNames: readonly string[]; } /** DESIGN-269 §2.1 —— an A2A PEER resolved to a core spec (header-NAME refs already → real values) plus * the scenarios it applies to (empty = all). Exact sibling of {@link ScopedMcpServer}: `resolveSpec` * filters by scenario (`a2aForScenario`) and passes `spec` to core as `TaskSpec.a2a`. * * 🔒 A peer is a remote AGENT, not a data source — core mounts every one of its skills with * `egress:true` + `effect:"write"` (ruled 2026-08-03), so a configured peer is an outbound WRITE * channel on every scenario it is scoped to. Scope deliberately, exactly as with an MCP server. */ export interface ScopedA2aPeer { scenarios: string[]; spec: A2aServerSpec; } /** * DESIGN-269 §3.1 车2 —— ONE declared skill on the agent card this deployment publishes when it acts as * an A2A **peer** (server-as-peer half). v1 is a **static declaration**: the operator writes the list, * the server never enumerates its own scenarios into it. * * 🔒 Why static (设计稿 §3.1 逐字):auto-enumeration would publish this deployment's internal scenario * names to an anonymous public discovery document. `scenario` is the ONE field that names an internal * key, and it is deliberately **not** projected onto the wire card (see `buildAgentCard` — the card * carries `id`/`name`/`description` only). It exists so a declared skill actually routes somewhere: * a `message/send` naming this skill runs under that scenario. */ export interface A2aServeSkill { /** Public, caller-facing skill id (what an A2A peer names to select it). Never an internal key. */ id: string; /** Human label; defaults to `id` on the card when absent. */ name?: string; description?: string; /** INTERNAL scenario key this skill runs under. NEVER projected onto the card. */ scenario: string; } /** * DESIGN-269 §3.3 车2 —— the server-as-peer block. **Absence IS the OFF state** (`A2A_SERVE_ENABLED` * default OFF): with this field undefined both `/.well-known/agent-card.json` and `POST /v1/a2a` 404, * i.e. "卡都不发=对外不存在" — not "publish a card but refuse the calls". Modelling OFF as absence * (rather than an `enabled:false` member) makes the half-state structurally unrepresentable. * * `url` is REQUIRED here because it is required when the knob is on: a public discovery document that * cannot say where to call is a broken card, and the reachable address is the operator's to declare — * [ref] `FLEET_ADVERTISE_ADDRESS` precedent, 不自猜可达地址(deriving it from the request `Host` header * would let an anonymous caller decide what our public card advertises). */ export interface A2aServeConfig { /** `A2A_SERVE_NAME` — the card's `name`. */ name: string; /** `A2A_SERVE_DESCRIPTION` — the card's `description`. */ description?: string; /** `A2A_SERVE_URL` — the PUBLIC JSON-RPC endpoint URL announced on the card (http/https, validated). */ url: string; /** `A2A_SERVE_SKILLS` — declared skills (may be empty: an honest "I take work, I advertise no skill"). */ skills: A2aServeSkill[]; /** `A2A_SERVE_BLOCKING_WAIT_MS` — how long a `blocking:true` `message/send` waits for a terminal state * before answering `working` and letting the caller poll `tasks/get`. Server-side budget only — it is * NOT core's client-leg `A2A_CALL_TOTAL_TIMEOUT_MS` (that is the CALLER's 10-minute budget, and holding * an inbound HTTP request open for anything like that long is how you exhaust a connection pool). */ blockingWaitMs: number; } /** Sandbox-image-pool BAKE config (IMAGE-API-DESIGN.md §P2). `enabled=false` ⇒ the /v1/images/bakes* block is * off (the control plane never mounts). The runner-facing facts (the runner principal, the build-host CACHE_BASE, * the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv — * operator free-text never reaches the runner. */ /** [ref] 车3([ref] 流内审批协议)的配置面。总开关默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后; * 树上已生效,发布线落在 7.4.0 之后的下一个发布);从属旋钮**除 `windowMs` 外**只在开关为真时生效 * ([ref]:`STREAM_ASK_WINDOW_MS` 是活卡窗长,与协议上不上场解耦 —— 逐条辖域见各字段域注) * (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */ export interface StreamApprovalConfig { /** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。 * 翻真要等回决端点(车4)与对账收敛器(车5)到位——在此之前是个不完整协议(有卡无正规回决口、 * 有 PARKING 无收敛器、跨副本回决无唤醒路径)。 */ enabled: boolean; /** `ToolApprovalCoordinator` 的 `ttlMs` —— **一只活卡等人多久**([ref] §3.3 的「配置窗默认」)。 * `STREAM_ASK_WINDOW_MS`,默认 **300000**(同裁与 sync 腿既有 5min 活卡窗对齐)。 * * 🔴 **辖域([ref] 起):无条件** —— 本值在**每一条**呈活卡的腿上生效,与流内协议上不上场无关 * (`DB_BACKEND=local` / 无 park 设施 / `STREAM_APPROVAL_ENABLED=false` 的部署同样按它计窗)。 * 旧文「开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)」是**已修的病**,不是 * 设计:那让一根部署级旋钮的生效与否挂在装配在场性上,运维配了它、没有任何一行说它没生效 * (clay 令:部署级旋钮禁挂在场性派生腿)。缺省值与那个常量同为 300000 ⇒ **没配过这根 env 的部署 * 字节不变**;协议上不上场改变的是**持久层与协议帧**(ask 行 / 重放 / 迟到赎回),不是窗长。 * * `0` = 运维显式关窗 ⇒ **不发任何审批卡**,每只受门 ask 在铸造时刻直接走 `unattendedAskOutcome()` * (§3.3 窗=0 语义;[ref] 起两条车道同义 —— 此前 `0` 在协议不上场的腿上被静默读成「5min 活卡」)。 * 该形有 park 设施 ⇒ 全落 durable park(经 `POST /v1/approvals/:sessionId/decide` 兑现);无 park 设施 * 或 `UNATTENDED_APPROVAL_POLICY=deny` ⇒ core fail-closed 拒 = **受门工具全关**,故 boot 期有一行 * `stream_ask_window_zero` 点名(`boot/coordinators.ts`)。 * **坏值拒启两侧**([ref]):负值(`-1` 此前静默落成「立即到期」并白发一张没人赢得了的卡)与 * **超过 Node 定时器上界 `2147483647`(2^31−1 ms ≈ 24.8 天)的值**(codex r1-[high]:`setTimeout` 把更大的 * delay 静默折成 1ms ⇒ 想调长反而立即到期;跨旋钮门只管 `窗 + adhoc宽限 < orphanTtl`,而 orphanTtl 域上界 * 90 天,所以 30 天的窗此前合法通过)—— 两侧都点名拒启,不夹取。 * * 窗到期的**去向**不由本键决定(`UNATTENDED_APPROVAL_POLICY` + park 设施在场性决定),本键只决定 * 「等多久」。悬挂 ask(零活流铸造,[ref] 案A)走的是 `unreachedTtlMs`,不是本键。 */ windowMs: number; /** 开流重放(§5)每次最多投几张未决卡——**读面**的帽,超出只投最新的并记一次 warn,开流不失败。 * `STREAM_APPROVAL_REPLAY_MAX`,默认 **50**。 */ replayMax: number; /** 写侧准入门(§0 X-2)的 per-task 未决 ask 上限。超限 ⇒ 在落 pending/建 timer/发帧**之前**返回 * `"unavailable"`(park 路由;**缺省 park 政策下永不 deny**——过载 bypass 到 park 是 §3.3 的硬条款。 * [ref] R-13 C:`UNATTENDED_APPROVAL_POLICY=deny` 的部署按自声明当场拒,理由见该键域注)。 * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK`,默认 **32**。消费点在刀 3b。 */ admitMaxPerTask: number; /** 同上,per-owner(租户)维。`STREAM_APPROVAL_ADMIT_MAX_PER_OWNER`,默认 **256**。 */ admitMaxPerOwner: number; /** [ref] 车5 §5 治理旋钮(全部 `numEnvBounded` 有界解析,§9 C6)—— 对账收敛器每 tick **每段** * (PARKING / 孤儿 STREAM_PENDING)最多处理的行数。超出的下轮接着扫(靠 `deferReconcile` 的队列 * 轮转保证「接着」是真的,不是永远同一批)。`STREAM_APPROVAL_RECONCILE_BATCH`,默认 **200**, * 有界 [1, 10000]。 */ reconcileBatch: number; /** 崩溃恢复扫描的**宽限**(§8 D-1.1):只有 `expiresAtMs + 本值 < now` 的孤儿 `STREAM_PENDING` 行才 * 被 reaper 代打 `expireAsk`。活属主进程里的窗到期竞争者在这个宽限内常态必胜 ⇒ reaper 退回纯崩溃 * 兜底,不与在场闭包抢(抢 = 制造挂死面)。`STREAM_APPROVAL_PENDING_GRACE_MS`,默认 **30000**, * 有界 [0, 3600000]。 */ pendingGraceMs: number; /** 判据 4(adhoc 腿)的窗后宽限。`STREAM_APPROVAL_ADHOC_GRACE_MS`,默认 **60000**,有界 [0, 86400000]。 * 🔴 跨旋钮不变量:必须 **< `orphanTtlMs`**(否则判据 5 的兜底会先于判据 4 触发,adhoc 腿拿到的 * 归因就成了 `orphan_ttl_exceeded`,归因诚实性失效)——boot 期校验,破坏形**拒启**。 */ adhocGraceMs: number; /** 判据 5(遗孤最终可判)的上界,量 immutable 的 `createdAtMs`。`STREAM_APPROVAL_ORPHAN_TTL_MS`, * 默认 **7d**,有界 [60000, 90d]。 */ orphanTtlMs: number; /** * [ref](案A,[ref] / core [ref])—— **零活流铸造**的 ask 的悬挂窗。`UNREACHED_ASK_TTL_MS`, * 默认 **3600000**(1h),有界 [60000, 86400000](60s ~ 24h),坏值/越界**拒启**([ref] A 档)。 * * 三问(behavior-facing 硬条款): * · **谁需要**:workflow 恒后台跑(CC 220 parity)⇒ 子代 ask 到达时 (owner, session) 键域常无活 SSE。 * 改形前那一刻即答 `"unavailable"`,合成整条命令死拒([ref] 车道2 实证)。悬挂让人稍后经 * `/v1/approvals` / 壳队列 / attach 补投三通道回来答它。1h 是「人不在场但当天会回来」的尺度。 * · **谁被伤**:悬挂 ask 占一格写侧准入配额(`STREAM_APPROVAL_ADMIT_MAX_PER_TASK`,默认 32)且占一条 * 持久行,窗越长积压越久;显式声明无人值守的部署(`UNATTENDED_APPROVAL_POLICY=deny`)**完全不受 * 本键影响**(那条腿在铸造时刻即拒,追记 F1)。 * · **补偿**:本键本身(调小即回到接近旧行为;60s 下界不给「等于零窗」的形——那等价于关掉案A, * 要那个语义请用 `deny` 政策或 `STREAM_ASK_WINDOW_MS=0` 的 windowZero 腿)。 * * 🔴 跨旋钮不变量(与 `WINDOW + ADHOC_GRACE < ORPHAN_TTL` 同族、同理由): * **`UNREACHED_ASK_TTL_MS + ADHOC_GRACE` 必须严格小于 `ORPHAN_TTL`** —— 悬挂行的 `expiresAtMs ≈ * createdAtMs + 本键`,判据 4 在 `expiresAtMs + 宽限` 触发而判据 5 在 `createdAtMs + orphanTtl` 触发; * 不等式不成立 ⇒ 遗孤兜底必先到,每条悬挂行的归因都被写成 `orphan_ttl_exceeded`。boot 期校验,拒启。 */ unreachedTtlMs: number; } export interface ImageBakeConfig { enabled: boolean; /** The dedicated operator principal the bake-runner authenticates as on the internal claim/ingest routes * (§P2.4e). A runner-only route requires the resolved principal to EXACTLY equal this. `BAKE_RUNNER_PRINCIPAL` * (default `system:bake-runner`). */ runnerPrincipal: string; /** The registry host prefix the auto-registered repo/ref is built under, so an `push:true` bake row's * `repo = ${registry}/${profile}` matches the P1 seed convention (seed-image-index.ts) and the index id * `sha256(repo@digest)` is stable across seed and bake. `BAKE_IMAGE_REGISTRY` env; **code default = null** * ⇒ the host-less `sema-images/` (node-local, documented non-pullable). Fleet posture (clay 拍 * 2026-07-13, SWR 弃用; 2026-08 内网 Gitea registry 容器已下线全删): `docker.io/claybobby`(唯一在跑 * 的公共源;无内建 CN 兜底,国内网络不通的部署需自行配置 dockerd `registry-mirrors` 或自建 registry)。 */ registry: string | null; /** The build-host promoted fetch-once cache base injected as `--cache-base` (NEVER caller-settable — a foreign * origin would defeat the domestic-only iron rule). `BAKE_CACHE_BASE` (e.g. `http://:`). */ cacheBase: string | null; /** The pinned current sandbox-base ref injected as `--base-ref` when the caller omits `baseRef` (§P2.4c default). * When NULL the server falls back to the index's latest published `sandbox-base` digest (design §P2.4c * "SANDBOX_BASE_REF / the index's latest published base"), so a deployment that publishes a sandbox-base needs * no separate pin. A caller-supplied baseRef must additionally be a known-published sandbox-base digest in the * index. `SANDBOX_BASE_REF` (sha256:…). */ defaultBaseRef: string | null; /** The lease duration / heartbeat window (ms) — the runner heartbeats `lease_until` forward within this and * the reaper steals an expired lease. `BAKE_LEASE_MS` (default 90000 ≈ 3× a 30s heartbeat). */ leaseMs: number; /** A `running` bake whose lease is stale by more than this (ms) is reaped → failed (§P2.7). Must exceed >2 * lease-heartbeat intervals. `BAKE_STALE_MS` (default 180000). */ staleMs: number; /** Per-principal submission rate guard window seconds + max (§P2.4c — fills the register DoS gap). 0 = off. * `BAKE_SUBMIT_RATE_MAX` / `BAKE_SUBMIT_RATE_WINDOW_SEC` (default 10 per 60s). */ submitRateMax: number; submitRateWindowSec: number; } /** Service configuration, read from environment — the **平铺面**(the ONE storage location for every * field). `ServiceConfig` (bottom of this file) = this flat face **plus** nine read-only group views; * fixtures that CONSTRUCT a config literal build this type and hand it to `attachConfigGroups`. */ export interface ServiceConfigFlat { port: number; /** OpenAI-compatible model gateway base URL (without /chat/completions). */ gatewayBaseUrl: string; gatewayApiKey?: string; /** Secondary openai-compatible gateways for cross-gateway FAILOVER (same protocol + same model id). * Tried in order after the primary when a gateway fails before streaming any content. */ gatewayFallbackUrls: string[]; /** `GATEWAY_MAX_RETRIES` —— openai 兼容腿的**每次调用**重试上限。 * * 🔴 **缺席 = 不传给引擎**(不是"传个默认值"):此前这里是硬编码 `maxRetries: 2`,而显式传参**压过** * 引擎默认 ⇒ core 抬默认对 server 部署一点效果都没有。缺席不传之后,引擎默认当家、core 抬默认自动继承。 * (五报清查 R1① 的 server 半场,黑板 [ref]/[ref];anthropic 腿早有 `ANTHROPIC_MAX_RETRIES` 出口, * 这条只是把同样的出口补给限流 provider 真正走的那条腿。) */ gatewayMaxRetries?: number; /** Optional Anthropic Messages API ROUTE (cloud). Selected for models whose `provider` is * "anthropic" (routing is by model.provider); the local gateway serves every other provider. */ anthropic?: { /** `x-api-key` auth. OPTIONAL since the Anthropic-compatible Bearer path: a Bearer-only * provider may auth by `authToken` alone. When BOTH are set, authToken wins. */ apiKey?: string; /** Anthropic-compatible-ecosystem `ANTHROPIC_AUTH_TOKEN` (many such providers use Bearer, not x-api-key): * sent as `Authorization: Bearer `; when set, `apiKey` is NOT sent (二选一, AUTH_TOKEN priority). */ authToken?: string; /** Default https://api.anthropic.com (overridden by model.baseUrl). */ baseUrl?: string; /** `anthropic-version` header; core default 2023-06-01. */ version?: string; /** Prompt-cache breakpoints on system + last tool (stable prefix). Default true. */ cacheBreakpoints: boolean; /** `ANTHROPIC_MAX_RETRIES` —— **缺席不铸键**,同 `gatewayMaxRetries` 的理由(见其顶注): * env 没配时以前会铸成 `2` 再显式传给引擎,把引擎默认写死在 server 这一层。 */ maxRetries?: number; }; /** Resilience brain stack (1.38, [ref]): tiered timeouts + an optional circuit breaker layered * UNDER the existing failover (gatewayFallbackUrls). Every field defaults to off/disabled, so the * brain composition stays byte-identical until an operator opts in. The breaker only changes * behaviour when paired with ≥2 gateway routes (it fast-fails a tripped primary so failover switches * immediately instead of waiting out timeouts). BreakerState is core's in-process Map for now — * cross-replica shared state (TiDB adapter) is a documented follow-up. */ resilience: { /** Abort fetch if response headers don't arrive within this many ms → retryable [network]. * Default 30s ([ref] fleet audit). 0 = off. */ connectTimeoutMs: number; /** Cancel an open SSE stream if no content/thinking delta arrives within this many ms. * Default 120s ([ref] fleet audit — core's idle timer only arms after the first token, * so this is the only guard for a stream that opens and never emits). 0 = off. */ firstTokenTimeoutMs: number; /** Mid-stream stall watchdog (1.40.1): after output starts, re-armed on every delta; silence for * this many ms → cancel + retryable [network]. Catches a gateway that hangs AFTER the first token * (firstTokenTimeoutMs only covers the first). 0 = off. */ idleTimeoutMs: number; /** Wrap the primary gateway route(s) in a circuit breaker (in-process state, per model+provider). */ circuitBreaker: boolean; /** Consecutive transient failures (network/server/rate_limit) that trip the breaker. */ failureThreshold: number; /** How long the breaker stays open before a half-open probe (ms). */ cooldownMs: number; }; /** Default model the service runs tasks on. **活的** —— center 热应用会整只换掉它 * (`config.model = config.models.default!`,config-center/apply-effective.ts 的 commit 段)。 */ model: Model; /** * S-188 —— **boot 期 env 派生的那只模型**,`loadConfig()` 铸一次,此后**永不换**(与 `model` 在 boot * 那一刻是同一个对象引用;center 热应用只动 `model`/`models`,不碰这里)。 * * 用途只有一个:**BL-8 继承源** —— 一条 center 目录条目省掉 `maxTokens` / `contextWindow` / * `reasoning` / `charsPerToken` / `defaultThinking` / `compat` 时,继承的是**运维在 env 里配的值**。 * 读活的 `model` 当继承源是错的(codex 对抗复审 r1 [high] 真复现):首次 commit 之后它已经是**上一代 * center 的默认模型**,于是 ①一条条目上的值会成为下一代所有条目的缺省(跨条目污染)②把那个值删掉 * 也撤不回来(残留到进程重启)—— 两条都与这几个键成文的语义(「inherits the operator's env values」)相反。 * * 🔴 **必填、无可选形**:一个 `?` 会立刻招来一条「缺席就回落 `config.model`」的臂,而那条臂正是本键 * 要消灭的东西([ref]:同一语义面禁两个写者)。测试夹具与 `loadConfig` 一样,与 `model` 同源铸。 */ envModel: Model; /** Model catalog by name (1.24): always `default` + the main model's id, plus an optional cheap model. * Used to resolve roles and to allow-list `@-model` mentions. */ models: Record; /** Per-model API-key override: catalog model NAME → the env var NAME holding that model's key (from * sema-registry `apiKeyEnv`). Drives the spec's `getApiKeyAndHeaders` so each model/rung authenticates * with its own upstream account; a model absent here falls back to the gateway key. Env path: empty. */ modelApiKeyEnv: Record; /** sealed-box 托管密钥的解封产物:catalog model NAME → PLAINTEXT api key(`ModelEntry. * sealedApiKey` 按 publicKeyId 选本机私钥 crypto_box_seal_open 解封,src/sealed-key.ts)。 * 🔴 只活在内存 —— 绝不落日志/盘/任何序列化面。互斥语义(registry-core 契约):模型带 sealedApiKey * ⇒ 它的 apiKeyEnv 引用不采(sealed 压过 env-NAME,解封失败也不回落——fail-loud 见 applyEffective)。 * Env path: empty;populated ONLY by applyEffective(重赋值;keyResolver 随 boot/refresh 重建)。 */ /** 值=解封明文 或 SealedKeyPoison 毒丸(解封失败标记;key-resolver 对其任务级 throw,恒不落 gateway key 兜底)。 */ modelApiKeys: Record; /** 配额倍率表(name+id 双键→ModelEntry.quotaWeight):weight-at-burn 的运行时源——tracer 在 * brain.call 记账点读它算 weightedTokens(lease 双轴+usage 批报)。registry apply IN-PLACE hot-apply; * env lane 恒空(无倍率概念,weight=1 兜底)。 */ modelQuotaWeights: Record; /** registry-core 0.9.0 档位组: the resolved active tier table (`tier word → catalog model name`, * e.g. `{pro: "ds-pro"}`) fed to `RunnerDeps.tiers` (core 1.265 expandTiers — tier words + CC aliases become * catalog keys). NO env source — populated ONLY by applyEffective from `models.tierGroups`/`activeTierGroup` * via registry-core `resolveActiveTiers` (single semantic source). Empty = INERT (core contract). Consumed at * Runner CONSTRUCTION (restart-to-apply, same tier as models/roles); mutateInPlace keeps the reference. */ tiers: Record; /** 142-S4(registry-core 0.10.0 projects 域):center 项目登记簿的 service 消费面 — * 键=projectId(PROJECT_ID_REGEX,generic lowercase UUID)。消费点:①`memoryScopeFor` 第三参数 * (projectId 在场 ⇒ 项目盘 v2 键 `proj:/`);②`defaultScopes` 作 * `TaskSpec.memory.scopes` 的种子(v2 scope 键串,OPEN 值域——本表只登记不解释)。NO env source — * populated ONLY by applyEffective(mutateInPlace,boot+refresh 整表替换);env lane 恒空(INERT)。 * ProjectRegistration 的审计字段(registeredBy/registeredAt/notes)不进 ServiceConfig(runtime 不消费)。 */ projects: Record; /** Role → model map (1.24): `subagent`/`summarize` → the cheap model when `MODEL_CHEAP_ID` is set * (council lenses + compaction get cheap), `default`/`team`/`synthesize` stay on the main model. */ roles: ModelRoles; /** [ref] / [ref] —— **`@`-mention 允许名单**(registry-core `models.atModelAllowlist` 的服务端消费面)。 * 语义(registry-core `ModelsDomain.atModelAllowlist` 的 JSDoc 同源):**缺席/空 = 全部启用模型都可被用户点选**;非空 = 用户 * 逐 turn 选择通道只能落在名单内的 catalog name 上。 * * 辖域(设计稿 233 v2 终裁,施工侧成文): * - **受限**=用户通道:`body.model` / `settings.model`(同属请求体,untrusted) / objective 内联 * `@name` / `body.compactionModel` / `POST /v1/side-query` 的 `body.model`; * - **不受限**=operator 配置通道:`models.roles` / `models.default` / 档位绑定表 / side-query 的 * `modelRole`(角色表选路)——名单管「用户可点什么」,不管「operator 配什么」。 * - **提交时门**:core 运行时路由(degrade / cascade / roles 解析)**不在射程**——那些是引擎按配置 * 自己走的路,不是一次用户选择(诚实声明,非运行时执法)。 * * NO env source —— env lane 恒空(INERT=全放行,ship-dark 缺省=旧行为);只由 applyEffective 从 * `models.atModelAllowlist` 整表重算填充(**非粘**:center 撤名单 ⇒ 本表清空 ⇒ 回全放行)。 */ atModelAllowlist: readonly string[]; /** * Session backend: in-memory (default), tidb (durable L1 event-log center), or `auto` — probe a * configured DB at startup and fall back to in-memory if it's unreachable (for local/intranet binaries: * "use my DB if I can reach it, else memory"). `auto` is resolved to a concrete value during startup. * NOTE (clay 2026-07-06 wording): the env value `mysql` is accepted as an alias and normalized to the * internal `tidb` label here — `"tidb"` in code reads as "the durable SQL store" generically (whichever * engine DB_BACKEND picks), a historical label kept to avoid churning every comparison site. */ sessionBackend: "memory" | "tidb" | "auto"; /** Warm-cache idle TTL (seconds) for woken TiDB sessions; 0 disables. Requires session affinity. */ sessionCacheTtlSec: number; /** B1 /health 活体 store 探测:后台缓存探针间隔 ms(STORE_PROBE_INTERVAL_MS;0=显式关,负/非数 boot * 响亮拒)。只对 SQL 后端接线;探针原语=backend.dbNowMs()(S10 时钟探针的真 DB 往返,零新 SQL 面)。 */ storeProbeIntervalMs: number; /** S-553 —— `DB_BACKEND=local` 数据根 `LOCK` 撞上**活**持锁人时的**有界等待窗** ms * (`STORE_LOCK_WAIT_MS`;缺省 60_000,地板 0 = 7.93.0 的当场拒,**不设上界**)。 * 病 = 壳自 spawn 引擎的单机形:旧引擎排空期间一直抱着锁,新引擎当场 fatal ⇒ 用户「刚关掉就再也起不来」。 * 失败方向一格不动:到窗仍拿不到 ⇒ 原码 fatal(文案多一句等了多久),**永不**放行第二个写者。 * 只有 file 后端读它;判据与偏离说明见 `plugins/store-lock-wait.ts` 顶注。 */ storeLockWaitMs: number; /** Long-term memory ([ref] S1, clay 2026-07-08): the injection-first file-based memory ENGINE * (core `RunnerDeps.memoryBackend` + `MemoryEngine`) is the ONLY memory plane — the legacy MemoryStore * path (MEMORY_BACKEND/EMBEDDING_* SQL/file note stores) was dropped without migration. Default ON; * `MEMORY_ENGINE=off` is the explicit kill-switch. Single-user only — main.ts gates the wiring on * `requirePrincipal !== true` (the file basement has no tenant isolation → multi-tenant stays dark). */ memoryEngineEnabled: boolean; /** Memory-engine config root override (env `MEMORY_ENGINE_DIR`). Unset → core's * `resolveMemoryEngineRoot` default chain (`AGENT_DATA_DIR` env → `~/.ai-agent`). */ memoryEngineDir?: string; /** N0 (夜测 2026-07-09) remote-lane plane-split override (env `MEMORY_ENGINE_REMOTE_LANE=allow`). The file * memory engine materializes/harvests on the WORKER's local fs, while a remote exec lane (e2b/k8s/ssh/adb/ * local-docker) routes the model's file tools to the SANDBOX fs — the two planes never meet (sandbox writes * are never harvested; the injected memory instructions just burn failing turns). Default: a remote lane * forces memory DARK (fail-closed). `allow` re-enables it for operators whose lane genuinely shares the * worker fs (e.g. ssh to a host on the same NFS mount). */ memoryEngineRemoteLaneAllowed: boolean; /** The single-user deployment's one memory scope name (env MEMORY_SCOPE; default "local" under * single-user). Feeds `spec.memory.scope` — the new engine consumes the same normalized scopes. */ memoryScope?: string; /** [ref](core 5.26.0 `TaskSpec.memoryPersistenceCapable`)—— 本部署对「这台机器上的任务**能不能**把 * 用户说的『记住 X』落到持久处」的**自我声明**,env `MEMORY_PERSISTENCE_CAPABLE`,三态: * · 缺席(默认)⇒ 键不上 TaskSpec,core 按自己的 roster 推断(已知落盘路 = 挂载且未排除的文件写工具 * 或可写 shell;remote 执行环境的手带写的是沙箱盘,不算够得着 host 记忆根); * · `true` ⇒ 部署自证有推断**看不见**的持久通道(自定义 writer / MCP 记忆服务 / remote lane 与记忆根 * 共享挂载)。它撤回引擎的只读披露,并在 remote lane 上**恢复** `# Memory` 写指令(core 5.26.0 r13/r14 * 收窄的官方恢复路径); * · `false` ⇒ 强制披露只读/无持久面,并关掉文件工具往记忆根的写通道(披露不会被存储事实推翻)。 * * 🔴 **`false` 自 core 5.27.0 起还多一层**([ref] F2,[ref] 提货):declared-false 的会话是**受限会话** * —— materialize/search/harvest 一律按**已提交账**(ledger + shadow)供给,磁盘上无事务背书的分歧 * 既不收编也不供给,而是留盘 + 响亮点名(`restricted_divergence`,`HarvestRejectionCode` 新成员), * 等下一次**非受限**会话走正常门收编。这补上了 5.26.0 自己登记的那条残余(读侧入站同步收编磁盘分歧 * **与会话意图无关**)——在那之前,这里写的「披露不会被存储事实推翻」只是半真话:披露说「本机存不下」, * 引擎却仍可能把别处落进记忆根的字节收进这次会话的账。 * ⚠️ **这一层的射程 = file 记忆引擎**(`MEMORY_ENGINE_BACKEND` 未设的默认单机形):受限视图是 * `FileMemoryEngineBackend.restrictedAdoptionView` 的实装,而 `engine.materialize` 取它走的是 * `?? this.backend` 的**可选** seam。我方 pg/tidb 两只记忆后端没有实装、也不需要——它们的读侧走库, * 盘上目录只是每任务的投影工作区,没有「读磁盘即收编」这条通道。`false` 在 DB 后端上仍照常买到 * 披露 / 写门 / 零收编 harvest 三件(引擎级,与后端无关)。 * * 与请求键 `memoryWrite`(MF-30)**两轴正交**,不是同一件事的两种写法:`memoryWrite` 是**本次运行**的 * 客户端表态(per-run 暂停写,writeScope:null),`memoryPersistenceCapable` 是**部署**声明(operator 面, * 与请求无关)。两轴任一取「否」方向即只读方向 —— 客户端能收紧本次运行,永远不能靠表态把部署声明的 * `false` 撑开(operator 旋钮不挂客户端表态派生腿)。 * ⚠️ **别把两轴的「只读」当成同一件事**([ref] F2 修后口径,逐字对齐 core):**受限**是**会话级的 * verdict 单键**(= 这里的 declared-false),而 `writeScope:null` 的只读**平面**(`memoryWrite:false` 的 * per-run 暂停 / org 层默认只读 / 双根非写面)**保持 adopt-on-read 原样** —— 受限是会话的**声明**, * 永远不是平面的**结构**。所以:一次 `memoryWrite:false` 的运行照常收编磁盘上的新内容(用户手改、 * git pull 落下的文件都还认),它只是不 harvest;而 declared-false 的部署连收编都不做。 */ memoryPersistenceCapable?: boolean; /** 142-S2.5-W1: TOC 同步 client 腿 —— file memory 形态把本地记忆盘与中心(DB 权威, * POST /v1/memory/sync/:scope)做一轮一 RTT 双向同步。env 三键:`MEMORY_SYNC_URL`(设了=开;缺省 * undefined=纯本地现状零变化)/`MEMORY_SYNC_TOKEN`(Bearer)/`MEMORY_SYNC_SCOPE`(可选,缺省= * formatUserScope(memoryScope);已是 v2 typed key 则原样直通,见 loadConfig)。半配置 fail-loud * (memoryEngineBackend 同款姿势);DB backend(pg/tidb)配了这些键=boot throw(中心侧自己就是 * 同步权威,不该再跑 client 腿)。 */ memorySync?: { url: string; token: string; scope: string; maxPushEntries?: number; maxPullEntries?: number; }; /** [ref](黑板 [ref]/[ref] 两裁):记忆检索的**向量面**接线 —— OpenAI 兼容 `/embeddings` 端点。 * env 四键(+ 可选 key):`MEMORY_EMBEDDER_ENDPOINT` / `MEMORY_EMBEDDER_MODEL` / `MEMORY_EMBEDDER_DIM` * (+ `MEMORY_EMBEDDER_TIMEOUT_MS` / `MEMORY_EMBEDDER_API_KEY`)。三问: * * ① **谁需要**:pg 记忆后端的部署。core 的记忆引擎三档(`vectorMode = lexical|portable|native`)不是 * 档位旋钮,而是「后端上限 × embedder 在场」的**推断结果**([ref] 裁①:不开档位 env —— 显式选档 * 只会造出「选了 native 却没 embedder」这类矛盾态)。本仓在 [ref] 之前从未注入过 embedder,所以 * pg 部署的检索**恒 lexical**(词面 jaccard),向量列空转。配上这几键 = 该部署真正拿到语义检索。 * ② **半配为何拒启**:三键里少任何一件,「向量面」都无从组装。静默忽略的后果不是功能缺席,而是 * **operator 以为开了**——检索照常返回结果(词面档),质量差异只在最终答案里,任何日志都不报。 * 所以照 `MEMORY_SYNC_*` / `REQUIRED_ENVS_BY_LANE` 的同款姿势整拒并点名缺哪几键。 * ③ **维度守卫为何是拒启不是 warn**:向量列写错维度是**静默毒库**形([ref] 裁③)——错长度的向量 * 既不会让 DB 报错(本仓 pg 记忆表的 embedding 是 `jsonb`,无维度约束),也不会让检索报错(维度 * 不匹配的行只是悄悄退回词面档),只会让「向量面开着」这句话变成谎。所以坏 DIM(非正整数)在 * boot 期拒;运行期真维度不符(模型换了 / 端点指错)由 embedder 自己抛,绝不截断补零。 * * ⚠️ 后端门:只有 `MEMORY_ENGINE_BACKEND=pg` 收这几键 —— tidb 记忆后端 v1 无向量档(传 embedder * 它自己就 fail-loud 拒),file 引擎压根没有 embedder 接缝。配错后端 = 拒启点名。 */ memoryEmbedder?: MemoryEmbedderConfig; /** [ref] 件A(clay 三裁 [ref]):org 记忆准入模式。`enforce`(默认)=判决即结果;`audit`=判决 * 照算但纯观察零行为变化(不整拒、不窄化 writeScope,只记 would-deny/would-narrow)——运维诊断位, * 非发布步骤(audit-first 灰度步按裁2 免除)。env `MEMORY_ORG_ADMISSION_MODE`(enumEnv 二值)。 */ memoryOrgAdmissionMode: "audit" | "enforce"; /** [ref](core 5.45.0 [ref],[ref] 裁定① 的部署半场):委派臂的**证据标准** —— 一次委派调用要不要把 * 本会话的记忆标记成 polluted,按哪一档证据判。env `MEMORY_DELEGATION_EVIDENCE`,两词闭集: * · `static-face` —— core 的缺省行为逐字:委派的 attestation 缺失/未知 **且** 静态工具面够得着外部 * 内容 ⇒ 标记(能力过近似:可能性即暴露); * · `attested-only` —— **只**豁免那一条静态面标记,且**真的豁免了一次**才响亮通告 * (`memory.delegation_static_mark_waived`,经本仓 `engine_notice` 转发面到运维;每个 prepared leg * **至多**一次)。⚠️ 通告**不是** leg 计数器:不含委派的 leg、污染面没挂载的 leg、以及送达了 * `external` attestation 的委派,都**不发** —— 「这一版没看到通告」的常态含义是「没有可豁免的事」, * 不是「配置坏了」。其余一律不变: * 送达 `"external"` attestation 照标、链上 `incomplete` 照记、非委派的污染类工具照标、委派工具 * 自身被分类为污染的照标。 * * 🔴 **缺席 = 不铸键**(不复制上游缺省 —— 上游改缺省之日起本仓不会成为第二份真源;`readFace` 同姿态)。 * 坏值 ⇒ 启动期响亮拒([ref]);core 侧 `prepareConfigDoors` 另有一道同判的门 * (`config.memory_delegation_evidence`,精确拼写、绝不真值判),两道门同向不冲突。 * * ⚠️ **部署席 ONLY**(与 `readDenyBuiltinTiers` 同判,core 侧的设计):TaskSpec 上没有同名键,也不在受治 * workflow 白名单里 —— 任务作者/受治脚本拿不到任何通道把证据标准放松到部署之下(本仓「部署级旋钮 * 无条件施加、不挂客户端表态派生腿」同向)。 * ⚠️ **已接受的代价**(core 成文,运维自己拥有):`attested-only` 下**后台**子代的真实外部接触不标记本 * 会话 —— 其内容经 TaskOutput / 任务通知注入 / AgentTranscript 步骤摘要回流,三条都不带 attestation; * 异常收尾(crash/salvage)的前台子代同样不按静态面标记(链上仍记 `incomplete`)。已标记的会话永不 * 回滚清洗,本键只管**新**标记。 */ memoryDelegationEvidence?: "static-face" | "attested-only"; /** [ref] 件4(core 5.46.0 [ref] §13-3 的部署半场,[ref]①/[ref]②):记忆 **provenance 总开关** —— * 一个会话被判为「已暴露」之后,它的记忆写要怎么处置。env `MEMORY_PROVENANCE`,两词闭集: * · `carry` —— core 缺省行为逐字:**普通**记忆写照常提交,但带上引擎铸的 `origin` 标记(随条目走 * 后端/同步/导出包);指令形文件(feedback / pinned / triggers / applies-when)仍被扣下隔离; * 派生索引的会话散文照回滚;内容扫描门原样跑(**标记不是豁免**); * · `off` —— [ref] 之前的行为:不铸 origin 标记,已暴露会话的 harvest **一条都不收**(整体 * 隔离候人审)。⚠️ 已提交的标记在编辑时照样带下去 —— `off` 停的是**铸**,从不抹掉已记的事实。 * * 与 {@link memoryDelegationEvidence} **正交**(core 顶注明写,四种组合全合法):那一键决定 * **什么时候**把会话判为已暴露(证据标准),本键决定判为暴露之后**对写做什么**(带标收录 vs 整体隔离)。 * * 🔴 **缺席 = 不铸键**(不复制上游缺省;`readFace` / `memoryDelegationEvidence` 同姿态)。 * 坏值 ⇒ 启动期响亮拒([ref] + 「部署级旋钮坏值禁静默回默认」);core 侧 `prepareConfigDoors` 另有 * 一道同判的门(`config.memory_provenance`,精确拼写、绝不真值判),两道门同向不冲突。 * * ⚠️ **部署席 ONLY**(与 `memoryDelegationEvidence` 同判,core 侧的设计):TaskSpec 上没有同名键, * 也不在受治 workflow 白名单里 —— 任务作者/受治脚本拿不到通道把 provenance 姿态改到部署之下。 * ⚠️ **不冻进 checkpoint**:resume 的那条腿跟**当前**部署配置走(core 成文)。 * 运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.provenance`(`null`=本部署未设)。 */ memoryProvenance?: "off" | "carry"; /** [ref] §3.1(core 7.0.0 片3,[ref] S-1b):本部署的记忆 **capture 姿态** —— per-principal * `RuntimeCaps.allowMemoryOptOut` verdict 被读进的那个框。env `MEMORY_CAPTURE_POLICY`,三词闭集: * · `open`(≡ 缺席时 core 的缺省)—— `TaskSpec.memory.capture:"off"` 声明按面值生效;只有 resolver 的 * **显式** `false` 拒它(`memory.capture_optout_denied`,terminal)。resolver **故障**(throw / 非 * boolean / 缺席)⇒ opt-out **仍生效** + `onError({phase:"config"})` 具名披露 —— 极性反转臂:隐私轴上 * 不可逆的一侧是「把用户明说不要记的会话记进去」,fail-safe 朝它的反面; * · `governed` —— per-principal verdict **必答**(混合车队:部分用户强制留存)。`true` 放行、`false` 拒; * resolver 故障/verdict 缺席 ⇒ **拒跑任务**(fail-closed:部署显式声明合规侧的不可逆更重); * · `capture-required` —— 全体声明具名拒(部署级强制留存,一行配置,不依赖 resolver)。 * * 🔴 **缺席 = 不铸键**(不复制上游缺省;`memoryProvenance` 同姿态)。坏值 ⇒ 启动期响亮拒:`governed` 与 * `open` 的故障极性**相反**,一个被静默折成缺省的拼写错误会让合规部署在故障窗里放行留存必需用户的 opt-out。 * core 侧 `prepareConfigDoors` 另有同判门(`config.memory_capture_policy`),两道同向不冲突。 * * 🔴 **`governed` 的半配置拒启**:该姿态要求本部署有 per-principal verdict 源。源**两条**(析取,[ref] 起): * SQL 后端上的 `memory_optout_grant` 表(S-2),或非 dry-run 的 config center 发 `EntitlementRuntimeCaps.allowMemoryOptOut` * (settings-schema 1.4.0 到货)。两源皆无 ⇒ 每个 opt-out 声明都被 core 拒且没人知道为什么 ⇒ * `boot/runtime-caps.ts` 的 `assertMemoryCapturePolicyWirable` 在启动期拒启(与 consolidation 阀门 D1 同姿态)。 * ⚠️ [ref]:center 那一臂只证「源在场」,不证 center 会**发**这个可选键 —— center-only 的部署起动后仍可能次次拒, * 两处 warn 把它响亮化(`memory_capture_governed_center_only_source` / `memory_capture_governed_verdict_absent`)。 * * ⚠️ **部署席 ONLY**:TaskSpec 无同名键;不冻进 checkpoint(resume 腿跟当前部署配置走,core 成文)。 * 运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.capturePolicy`(`null`=本部署未设)。 */ memoryCapturePolicy?: "open" | "governed" | "capture-required"; /** * [ref](core 5.58 [ref] 片②)—— 记忆 **consolidation driver 阀门**的三根旋钮。 * **恒在场**(不是可选块,与 {@link retentionSweep} 同姿态):三根都有文档化的缺省,而缺省本身 * 是一条要能被读出来的姿态 —— `{enabled:false, intervalSec:0, scopes:[]}` = 「阀门关着」。 * * 三问(设计稿 §消长账):谁需要 = 记忆库只涨不折的 TOB 部署(检索质量随库龄衰减);谁被伤 = * 误开 + 错 role 的部署(一轮 consolidation 读全库 ~1e5 prompt tokens,静默升级到最贵 default * 就是把一次维护跑成一笔意外账单);补偿 = 阀门默认关 + 座解析失败拒启 + D3 投影把每轮花了什么 * 摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。 * * 语义与解析见 {@link MemoryConsolidationDriverConfig} 与 config.ts 的 `parseMemoryDomain`。 */ memoryConsolidationDriver: MemoryConsolidationDriverConfig; /** debt [ref]-B(core 5.48 C18 提货件):委派入口 caps 的部署旋钮 —— env `DELEGATION_MAX_CONCURRENT` / * `DELEGATION_MAX_PER_SESSION`(整数,numEnvBounded 响亮拒坏值)。**缺席 = 不铸键**(core 自缺省 * CC parity 20/200;`memoryDelegationEvidence` 同姿态);两键各自独立可设,pair 合法性 * (maxConcurrent ≤ maxCumulativePerSession)由 core prepare 的 `config.delegation_entry_caps` * 响亮拒承担,本层不复刻第二判官。部署席 ONLY(TaskSpec 无同名键)。 */ delegationEntryCaps?: { maxConcurrent?: number; maxCumulativePerSession?: number; }; /** 件A 单机形目录:env `MEMORY_ORG_DIRECTORY_JSON` 原文(`Record>`)。 * config 解析期即整段校验(parseOrgDirectoryStatic,fail-loud);此处存**原文**(config=纯数据, * Map 在装配点重建)。与 config-center 目录腿互斥的裁决在装配点(两者都在=center 腿胜,env 表忽略 * 并 warn——授权面不做双源合并)。 */ memoryOrgDirectoryJson?: string; /** 件A 目录 granted(含负结果)缓存 TTL(ms)。新任务/新 resume 腿的准入判决滞后上限=此值(「TTL 即 * 有界 LKG」);与同仓保护面 grant TTL 对齐默认 60s。env `MEMORY_ORG_GRANT_TTL_MS`(numEnvBounded)。 */ memoryOrgGrantTtlMs: number; /** 件A 目录 unavailable 退避窗(ms)——取数失败的退避语义(bound center hammering),不是策略负结果。 * 默认 10s。env `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS`(numEnvBounded)。 */ memoryOrgUnavailableBackoffMs: number; /** [ref] v1b + [ref]: opt-in remote execution backend (PEER adapters, chosen by REMOTE_EXEC). Unset * (OA/review deployments) → in-process stub env, zero behavior change. Secrets (apiKey/privateKey) ONLY from env. * - `e2b` → per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker. * - `k8s` → per-task Kata pod on our k3s (isolated, NOT suspendable — no managed memory * snapshot). Self-hosted sandbox lane; auth = explicit K8S_* env or in-cluster service account. * - `ssh` → a real host over SSH (NOT isolated/suspendable; [ref] §5). AI-orchestrated batch deploy. * - `adb` → a real device over ADB (NOT isolated/suspendable). On-device APP testing. */ /** * S-213④ —— `REMOTE_EXEC_HOME_DIR`:**执行环境的 home 目录**(core `ExecutionEnv.homeDir`), * `~/…` 形权限规则(`Edit(~/.ssh/**)`)唯一的基,绝对路径。 * * 部署级旋钮,**跨车道同一根**(不按 provider 分七根 —— 它回答的是同一个问题:「这条部署的手, * 跑在哪个用户的 home 下」)。显式声明**恒赢**;缺席时只有 home 属于车道本身的那几条车道有答案 * (host/in-process = `os.homedir()`、e2b = 平台固定用户),k8s/local-docker(镜像属性)、 * ssh(目标机登录用户)、adb/device 一律缺席 ⇒ `~/` 规则在下单时被 422 响亮拒。 * 派生与逐车道理由的**单一属主** = `execution-lane-caps.ts` 的 `executionLaneHomeDir`。 * 非绝对路径 ⇒ **拒启**([ref] A 档;core 侧同判为 `config.execution_env_home_dir_invalid`)。 */ remoteExecHomeDir?: string; remoteExec?: { provider: "e2b"; apiKey: string; template?: string; timeoutMs?: number; livenessMs?: number; allowInternetAccess?: boolean; sandboxEnv?: Record; mountPath?: string; /** [ref]:崩溃遗留沙盒回收腿(E2B_ORPHAN_RECLAIM,默认 true;判据与 v1 边界见 * plugins/e2b-orphan-reclaim.ts 头注)。 */ orphanReclaim?: boolean; } | { provider: "k8s"; image: string; apiUrl?: string; token?: string; caCert?: string; insecureTls?: boolean; namespace?: string; runtimeClass?: string; mountPath?: string; timeoutMs?: number; /** Resource-profile knobs: sandbox pod limits (adapter defaults: 2Gi / 1 cpu). */ memory?: string; cpu?: string; s3Snapshot?: { endpoint: string; bucket: string; accessKey: string; secretKey: string; region?: string; keyPrefix?: string; presignTtlSec?: number; }; } | { provider: "ssh"; host: string; port?: number; username: string; privateKey: string; mountPath?: string; hostKey?: { fingerprintSha256?: string; knownHostsPath?: string; }; } | { provider: "adb"; serial: string; adbPath?: string; mountPath?: string; } | { provider: "host"; workspaceBase?: string; commandTimeoutMs?: number; } | { provider: "device"; /** `DEVICE_DELIVERY_TIMEOUT_MS`(默认 10s)= 等在途槽的上限;到期 = `device.busy`(§5.5)。 */ deliveryTimeoutMs?: number; /** `DEVICE_RECONNECT_GRACE_MS`(默认 120s)= 断连后在途不判丢的窗(§5.4 同 epoch 重连)。 */ reconnectGraceMs?: number; /** `DEVICE_EXEC_TIMEOUT_MS`(默认 1800s)= 指令级默认超时;core 的 per-command `timeout` 可收紧。 */ execTimeoutMs?: number; /** `DEVICE_MAX_INFLIGHT_PER_DEVICE`(§11-O3 clay 已裁 = 4)。 */ maxInflightPerDevice?: number; /** `DEVICE_MAX_PREAUTH_PER_REMOTE`(默认 8;字面 `off` = 显式停用 per-remote 层)= 未鉴权 * 握手的 per-remote 并发帽([ref]/[ref]:帽键是裸 remoteAddress——XFF 反代 / 公司 NAT 部署 * 形会把全员折叠成单键,per-remote 层退化成共享预算而非公平层,`off` + 全局帽 64 是诚实形)。 * 坏值拒启([ref] A 档),词表与拒因见 config.ts `parseDevicePreAuthPerRemoteEnv`。 */ maxPreAuthPerRemote?: number | "off"; /** `DEVICE_ENROLL_TOKEN_TTL_SEC`(§4.7 第五个旋钮;默认 900 = §7-T5 ① 的 15min)= enrollment * token 的有效期**时长**(整秒,自 **DB now** 起算 —— 时间权威是库,不是副本墙钟)。唯一消费点 = * 签发动词 `POST /v1/devices/enroll-tokens`(`http/routes/devices.ts`);S-470 之前它刻意未铸 * (端点不存在 ⇒ 铸了就是一个「配了不生效」的死键,[ref] 病族),随该端点同批铸 + 接线。 * 域 = `[1, 86400]` 的十进制正整数,坏值**拒启**([ref] A 档;解析器 `parseDeviceEnrollTokenTtlEnv`)。 * 缺席 ⇒ 签发点的属主默认 900(不在 config 层抄第二份默认,与本族其余旋钮同姿势)。 */ enrollTokenTtlSec?: number; /** `DEVICE_SUPERSEDE`(默认 `stale_only`)= 同 deviceId 二连的顶替策略(§5.2)。 */ supersede?: "stale_only" | "deny"; } | { provider: "local-docker"; image: string; mountPath?: string; dockerPath?: string; dockerHost?: string; memory?: string; cpus?: number; pidsLimit?: number; dropAllCaps?: boolean; network?: "none" | "bridge" | "host"; commandTimeoutMs?: number; env?: Record; }; /** * SVC-3 git-WORKTREE ISOLATION ([ref] CORE-6). Opt-in (WORKTREE_ISOLATION_ENABLED + WORKTREE_REPO_ROOT). * When set, the deployment's `executionEnvFactory` is wrapped so an agent core marks `isolation:"worktree"` * (via the TRUSTED RunInternals — never a TaskSpec) runs in its OWN detached git worktree under `repoRoot`. * Operator-supplied ONLY (never a task input). v1 covers the `host` lane (the TOC fan-out lane); other lanes * pass through unwrapped (a worktree-rooted REMOTE env is a follow-on). See plugins/worktree-isolation.ts. */ worktreeIsolation?: { /** Operator-trusted base repo root where `git worktree add/remove/prune` run. Absolute, a git repo. */ repoRoot: string; /** Operator-trusted allowlist of permitted roots; `repoRoot` must be within one (defence in depth). * Default `[repoRoot]`. */ allowedRoots?: string[]; /** Base commit/ref for every worktree (default core's `HEAD` at creation time). */ commit?: string; }; /** MCP servers from sema-registry (`/effective.mcp`), resolved at startup (env-NAME refs → values) and * filtered per scenario in `resolveSpec`. Not env-derived — populated by the sema-registry adapter when * present (restart-to-apply, like models/roles); undefined = no center MCP (scenarios use no MCP). * Carries both the mounted list and the full declared-name set — see {@link CenterMcp}. */ mcpServers?: CenterMcp; /** DESIGN-269 §2.1 —— A2A peers from the config center (`/effective.a2a`), resolved at startup * (header-NAME refs → values) and filtered per scenario in `resolveSpec` (`a2aForScenario`). Same * posture as {@link mcpServers}: not env-derived (no A2A_* env leg — a peer is a structured record, * not a scalar), populated by the config-center adapter when present, restart-to-apply. * undefined = no center peers ⇒ **the whole leg does not exist** for a task (`TaskSpec.a2a` stays * absent unless a single-user caller injects `body.a2aPeers`). */ a2aPeers?: ScopedA2aPeer[]; /** DESIGN-269 §3.3 车2 —— server-as-peer. **undefined = OFF** (the default), and OFF means both the * card and the JSON-RPC endpoint 404. See {@link A2aServeConfig}. Env-derived (`A2A_SERVE_*`), unlike * {@link a2aPeers}: the serve block is a handful of scalars an operator sets on the worker, not a * center-distributed record. */ a2aServe?: A2aServeConfig; tidb?: { host: string; port: number; user: string; password: string; database: string; connectionLimit?: number; }; /** Which SQL engine the durable stores run on (clay 2026-06-21 PG migration). Canonical value is `mysql` * = any MySQL-protocol server — MySQL, TiDB, MariaDB — over the same mysql2 pool; `pg` (PostgreSQL * dialect) reads `pg` below. [ref]/[ref]B-3(5.0.0):the env value `tidb` and the TIDB_* coord names * are RETIRED — `tidb` is rejected by the enum and any TIDB_* env in place refuses to start with the * MYSQL_* replacement named (see RETIRED_ENVS in config.ts). `mysql` reads MYSQL_* coords only. * TiDB itself stays fully supported (any MySQL-protocol engine works — memory no longer needs a VECTOR * column since [ref] moved it onto the file engine). Env: DB_BACKEND. */ dbBackend: "mysql" | "pg" | "local" | "memory"; /** clay 拍(2026-07-27):operator 是否显式设了 DB_BACKEND。main.ts 的降级判据——**默认推导**的 * local(裸 boot)在 mkdir/ensure 失败时降级 memory + warn + gauge(auto-DB 不可达先例); * **显式** DB_BACKEND=local 失败仍 fail-loud(operator 要了 durable,静默丢=丢数据)。 */ dbBackendExplicit: boolean; /** Data root for the file-backed `local` StoreBackend (clay 2026-06-26 P0.5 variant-2): the dir core's * `FileStorageBackend` opens (sessions/memory/policy/snapshots) so a locally-run HTTP service's sessions * SURVIVE a process restart + the resume picker is restart-visible. Resolved from `LOCAL_DATA_ROOT ?? * AGENT_DATA_DIR ?? ~/.ai-agent` — the SAME root main.ts:102 + run-local.ts:198 resolve, so the HTTP * service and a run-local on the same box agree on one data dir (boot-locked single-instance). Only the * `local` backend reads it. */ localDataRoot?: string; /** * [ref] 件B(`RUN_STORE_STRICT_HYDRATE`,普查表第 29 行的**终局格**)—— file 后端的**可选** fail-closed * 启动门。`true` ⇒ `FileRunStore.hydrate()` 列不出 `runs/` 时**拒启**(error 级遗言带绝对路径 + 异常 * 文案,然后抛);缺省 **false** ⇒ 件6 的既有形逐字不变(带病起动 + 一行 error + P-DEBT 计数)。 * * 为什么默认是 off:件6 当年把「不拒启」写成了刻意保留的行为,默认翻面等于替所有存量部署做主。 * 为什么值得有这根旋钮:空账起动之后每一次 `getRun` 恒 miss,而「恒 miss」与「本来就没有这条 run」 * 在 wire 上同形(#43 现场就是这么被误读的)—— 把 run 账本当真账本用的部署有权要求「读不出就别起」。 * 坏值**拒启**([ref] A 档):静默回默认会让 operator 要的 fail-closed 被无声吃掉。 * * 只有 `local`(file)后端读它;SQL 后端的 hydrate 不经这条路。 */ runStoreStrictHydrate: boolean; /** PostgreSQL connection (used iff dbBackend === "pg"). Mirror of `tidb`. Env: PG_HOST/PG_PORT/PG_USER/ * PG_PASSWORD/PG_DATABASE/PG_POOL_SIZE. */ pg?: { host: string; port: number; user: string; password: string; database: string; connectionLimit?: number; }; /** S9 deeper fix (SILENT-FALLBACK): per-query DB timeout in ms — a DB-side stall must surface as a rejected * query (which the S9 flush-streak / stalled-watchdog telemetry already counts), not an unbounded hang. * Scope differs by engine (native support): on `pg` it is POOL-WIDE (`statement_timeout` server-side cancel + * `query_timeout` client-side backstop — see pgPoolOptions); on `mysql` mysql2 has NO pool-level knob, so it * overrides only the write-behind counter family's per-query timeout (the S9 pain face; default 30s there even * when this is unset). Unset = pool-wide OFF (byte-compat: legit heavyweight statements — boot DDL, ≤64 MiB * snapshot-blob rows — make a universal default unsafe). Env: DB_QUERY_TIMEOUT_MS (soft knob, S20 warn-on-typo). */ dbQueryTimeoutMs?: number; /** Object-store offload for the E19/2c file-snapshot BLOBS (clay 2026-06-26). The conversation TRANSCRIPT stays * DB-first; ONLY the snapshot BYTES (binary file content, up to ~64 MiB/file — a perf-suboptimal TiDB blob row) move * to MinIO. The file-history MANIFEST (relPath→hash) STAYS in SQL, so the transcript hot read never * touches MinIO. Built (by the tidb/pg StoreBackend) into a MinioBlobBackend iff MINIO_ENDPOINT + MINIO_ACCESS_KEY + * MINIO_SECRET_KEY are all set — else the file-snapshot stores keep the SQL `snapshot_blob` default (byte-identical to * today). Reuses the SAME MinIO env names as the k8s workspace-snapshot lane (zero remap). */ snapshotBlobStore?: { endpoint: string; bucket: string; accessKey: string; secretKey: string; region?: string; keyPrefix?: string; presignTtlSec?: number; }; /** (a)(clay 拍 a+c,2026-07-27)SQL 默认店的 per-blob 帽显式覆写(字节)。未设 ⇒ SqlBlobBackend * 的方言默认(tidb=6MiB=TiDB 默认 txn-entry-size-limit 真库实测矮墙,pg=不设帽);设了 ⇒ 两方言 * 都生效(调高过 entry/packet 限的部署放宽用)。软旋钮:坏值=undefined 走默认。 */ snapshotBlobSqlMaxBytes?: number; /** (c) 的显式逃生口:云形(mysql|pg 后端)缺 MinIO 本应 boot fail-loud(bytes-in-DB 撞包墙已两役 * 实证,大字节归对象存储);`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true` 显式接受 SQL 店 + (a) 的帽 * (单机/测试台形)。严格 "true" 判——这是安全姿势开关,不收宽松真值。 */ snapshotBlobAllowSql: boolean; /** [ref] twin 保留条款(core [ref] §片2):per-file rewind 历史的**边界上限**,键形与 core * `FileStorageBackendOptions.fileHistory.retention.keep` **同域同名**(local 腿原样透传给 core 的 File 店; * SQL 双生店构造期同调 core `resolveFileHistoryRetention` —— 一套语义,三条车道)。缺席 = core 缺省 * (100,由 core 兑现,本仓不复制那个数);`{ keep: "unbounded" }` 是唯一的「不剪」拼法。坏值在 * loadConfig 就以 core 的 `config.retention_policy_invalid` 拒(运维接线处),绝不静默回落。 * env:`FILE_HISTORY_RETENTION_KEEP`。 */ fileHistory?: { retention?: FileHistoryRetentionPolicy; }; /** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面, * 消费在 capabilities/center-plugins.ts)。 */ pluginsAllowHosts: string[]; /** 监听绑址([ref] cli 实锤:此前 `HOST` **零消费**、`listen(port)` 恒绑所有接口)。 * `BIND_HOST`(兼容 `HOST`)。显式值恒生效;缺省见 {@link resolveBindHost}——**无鉴权写面 * (allowUnauthedWrites 且零凭证)自动收窄到 127.0.0.1**,其余维持全接口(既有部署零影响)。 */ bindHost?: string; /** [ref]③ `bindHost` 的取值来源(BIND_HOST 显式 / HOST 兼容名 / 未设)。`"HOST"` 时 main.ts 打一行 * 启动告警:zsh 常把 HOST 设成机器名,`{...process.env}` 起底的壳会静默继承给引擎——bind 面被 shell * 环境暗配而部署者不知情(HOST=机器名解析到 LAN 口时=静默扩暴露面)。 */ bindHostSource?: "BIND_HOST" | "HOST"; /** D-1 附件孤儿对象 GC 的 grace(毫秒;`ATTACHMENT_ORPHAN_GRACE_MS`,默认 1h,**0=关掉本腿**)。 * 复审 F10:同 tick 邻居(workflowOrphanGraceMs/attachmentUnboundTtlMs/scratchpadSweepTtlMs)都有 * 旋钮——存量对象巨大的部署要调慢/停掉,不该被迫取消 MinIO 配置(那会连带关掉整个附件面)。 */ attachmentOrphanGraceMs: number; /** ⚠️ INERT since [ref](workspace 浏览面随整树快照纪元退役,消费点归零)。键**保留**:center 热推 * 可能仍携带它,删键会让整份推送按未知键处置——正式摘除走 settings-schema 的弃用车道。 */ workspaceFileMaxBytes: number; /** SendUserFile dual-track public-link issuance (design settled cross-repo 2026-07-14; S3_PUBLIC_* are * [ref]② cross-repo contract names — never rename). Enabled iff the adapter-held MinIO/S3 creds are set: * `endpoint` = MINIO_ENDPOINT (the shared snapshot-lane name, wins when both are set) with S3_ENDPOINT * as the external-S3 compat alias (one address serving both internal + public faces), plus * MINIO_ACCESS_KEY + MINIO_SECRET_KEY (optional MINIO_REGION) — any of the three unset/empty ⇒ the whole * block is absent and the tool is honestly not mounted (prior behavior, fail-safe default). * `publicEndpoint` = S3_PUBLIC_ENDPOINT (falls back to S3_ENDPOINT when that is already public); absent ⇒ * the tool mounts but fail-louds at issue time with the fix (never mints an internal-host link). * Buckets: S3_PUBLIC_BUCKET (default "sema-public") for the permanent track — its anonymous policy MUST * be GetObject-only (`mc anonymous set-json`, NOT `set download` which also enables ListBucket and lets * the unguessable-key capability URLs be enumerated); the private presigned track shares * SESSION_SNAPSHOT_BUCKET (default "session-snapshots") under a "sendfile/" prefix. * SEND_USER_FILE_URL_TTL = default ttl seconds when the tool call omits one; 0 = permanent (the settled * default). Unset/non-integer/negative fail-SAFE to 0 — never NaN into a signature. */ sendUserFile?: { endpoint: string; publicEndpoint?: string; publicBucket: string; privateBucket: string; privateKeyPrefix: string; /** v2: endpoint the SANDBOX's presigned PUT is Host-bound to. Default publicEndpoint (an e2b cloud * sandbox only reaches the public face); in-cluster k8s pods may prefer the internal endpoint. * Env: SEND_USER_FILE_SANDBOX_PUT_ENDPOINT. */ sandboxPutEndpoint?: string; accessKey: string; secretKey: string; region?: string; defaultTtlSec: number; }; /** Bearer token the service requires from callers (optional). Single-token deployment tier (one-tenant / * on-prem): authenticates, but the request's `source` stays NULL (no system identity to derive). */ authToken?: string; /** Per-SYSTEM service credentials: token → system name (`SERVICE_AUTH_TOKENS="=oa,=cc-mcp"`). * The request's `source` is DERIVED from which credential authenticated — never self-declared by the * caller — and persisted on the run row (audit / per-door value attribution). Rotation/revocation is * per system. Coexists with `authToken` during migration (that one authenticates with source=NULL). */ authTokens: Record; /** Escape hatch for local dev: when true, task-submission endpoints (POST /v1/tasks[/stream], /v1/runs, * /v1/leader) accept requests even when no `authToken` is configured. Default false = fail-closed, so a * deploy that forgets `SERVICE_AUTH_TOKEN` REFUSES submissions (center: a worker must validate a * service-to-service token, else anything in-cluster can submit & bill directly). `ALLOW_UNAUTHED_WRITES`. * 有意设计(文档化):本开关只豁免「零凭证形态」的 fail-closed 503;一旦配了凭证 * (SERVICE_AUTH_TOKEN 或 TOKENS 名录),凭证必须使用——unauthed 仍 401,两 env 不叠加 * (半配置绕行=隐藏入口,刻意优先级)。 */ allowUnauthedWrites?: boolean; /** Optional READ-ONLY token that ALSO authorizes GET /metrics + /metrics/summary (in addition to * authToken). Set the SAME value fleet-wide so a control plane (sema-registry) can pull every worker's * metrics with ONE token it holds — without ever holding each worker's full `authToken`. */ metricsToken?: string; /** Read-only Task Trace API token (S1). Like `metricsToken` but for GET /v1/tasks[/:id/turns|/stream] — lets * sema-registry pull worker traces fleet-wide with ONE read-only token, never holding each worker's full * `authToken`. Read-only: cannot run tasks or change config. `TRACE_TOKEN`. */ traceToken?: string; /** CORS v2(2026-07-11):`CORS_ORIGIN` 接受 CSV **白名单**。单值=v1 兼容 * (无请求 Origin 也回显该值——curl/同源读者不受影响);多值=按请求 Origin 匹配回显+`Vary: Origin` * (缓存正确性),不匹配=不设 allow-origin(浏览器拒)。未设=无 CORS 面(默认关)。 */ corsOrigins: string[]; /** D-1 附件上传:单文件字节上限(server 权威;缺省 32 MiB)。字节本体在对象存储(clay 裁 2026-07-27 * 云形必配 MinIO),故上限不是 DB 行约束而是:①上传路径整段在内存缓冲(readRawBody reject-not-OOM, * 上限=单请求 RAM 束)②物化路径 env.writeFile 一次成写。32 MiB 真三方(TiDB+PG+MinIO)整程回读 * 实证在店套件。`ATTACHMENT_MAX_BYTES`。 */ attachmentMaxBytes: number; /** D-1 附件 mime 白名单(部署策略旋钮,`ATTACHMENT_MIME_ALLOWLIST` CSV;支持 `type/*` 通配)。 * **缺省不设限**:声明 mime 是客户端自述、不可验证,不构成安全边界——真防线=尺寸上限+沙箱内消费; * 设它是部署方的内容策略选择,不是安全开关(拒答带 415 + 明示允许集)。 */ attachmentMimeAllowlist?: string[]; /** D-1 未绑定附件 TTL(上传后从未被任何 task 引用的行,过期由 reaper 收割;缺省 24h)。 */ attachmentUnboundTtlMs: number; /** Header carrying the authenticated principal (S6). Identity never comes from the body. */ principalHeader: string; /** Require the principal header on task requests (set true in multi-tenant production). */ requirePrincipal: boolean; /** Runtime governance "second baton" (center §10): operator autonomy mode, expanded TIGHTEN-ONLY onto each * task's TaskSpec (`read-only`/`plan` → read-only hands, `ask` → shellGate always, `auto`/unset → no extra * tightening). Per-request HOT (read live in resolveSpec); primarily sema-registry-managed, `AUTONOMY` env for * the env/local path. See runtime-governance.ts. undefined = unmanaged. */ autonomy?: Autonomy; /** Runtime governance "second baton" (center §10): per-command coarse name-level rules, compiled into a * `combinePolicies`-layered ToolPolicy on top of the deployment baseline (deny-wins, tighten-only). COARSE * filter, NOT a sandbox (sh -c/$()/pipes bypass argv[0] → fail toward ask). Per-request HOT; sourced from * sema-registry `EffectiveConfig.runtime` (structured config, not an env scalar). undefined = unmanaged. */ commandPolicy?: CommandRule[]; /** * [ref] 件B —— 本部署的**合规档位**(core `RunnerDeps.compliancePostureResolver` 的供数)。 * env `COMPLIANCE_PROFILE`(闭集 `hipaa|zdr`)+ `COMPLIANCE_ADDITIONAL_DENIES`(闭集能力名 csv, * **只能收紧** profile 的内建 floor)。两个词表都是 core 的闭集,拼错一律 boot 拒 —— 一个拼错的 * 能力名等于「标着 hipaa 的部署根本没在执行那一条」([ref] §3.2)。 * * 🔴 **档位是部署级、不是 per-principal**:core 的座席按 principal 取值,但本仓能拿到的**唯一真源** * 是部署配置面([ref] §1 表:center 侧的档位真源标着「新建」= 尚不存在)。所以 resolver 对每个 * principal 返回同一份档位 —— 诚实的窄实现,而不是一条永远查不到值的 center 假腿。 * 缺席 = 无合规限制(core 契约),现存部署逐字零影响。 */ compliancePosture?: CompliancePosture; /** * [ref] 件C —— **管理员锁定层**的部署声明(core `RunnerDeps.lockedConfig.keys`)。 * env `LOCKED_CONFIG_KEYS`(csv;词表 = core 的 `LOCKED_KEY_REGISTRY` 闭集,未知键 boot 拒 = * `config.locked_registry`「不解析的安全控件不得半开」)。 * * ⚠️ 本仓的两条**装配相容性**判据在 boot/governance-seams.ts(`assertLockAssemblable`): * `toolPolicy` 锁与本仓「每条腿自铸 spec.toolPolicy」的装配形结构性冲突,`mcp` 锁与「部署自带 MCP * 基线」冲突 —— 两者都在启动期拒,而不是让每条任务腿在 prepare 期各撞一次 `config.locked_key`。 */ lockedConfigKeys?: readonly LockedKey[]; /** * [ref] 件D —— **托管留存期**声明(core `RunnerDeps.retentionPolicy`)。env `RETENTION_MAX_AGE_DAYS` * (有限非负数;坏值 boot 拒 —— 删数据的旋钮不许 NaN 流通)。 * * 🔴 语义边界(成文,别当成"数据会被删"):core **只**拿它做启动期能力校验(锁着 + 店未声明 * `retention:"managed"` ⇒ 拒启),引擎自己在任务路径上从不删数据;真执行(调度/重试/副本协调/审计) * 是部署侧的活,本仓**尚未**建([ref] §5.2「server 半场四件皆新建」)。所以配了策略而没有任何 * managed 店时,启动会打一条 `retention_policy_not_executed` 的响亮 warn。 */ retentionPolicy?: RetentionPolicy; /** * [ref] 车2 —— 托管留存 **sweep lane** 的两个旋钮(设计稿 `DESIGN-270-retention-lane.md` §3/§4)。 * `retentionPolicy` 说的是「保留多久」(一句声明),本格说的是「谁在什么节律上按那句话删」。 * **恒在场**(不是可选块):两根都有文档化的缺省,而缺省本身是一条要能被读出来的姿态 —— * `{mode:"audit-only", intervalSec:0}` = 「lane 关着,真开起来也先只记账」。 */ retentionSweep: RetentionSweepConfig; /** Tool names that require operator approval before running (F4). Empty = no approval gate. */ approvalRequire: string[]; /** Principals allowed to act as OPERATORS on the F4 approval queue (`/v1/approvals`): list ANY * owner's pending + decide (approve/deny). A non-operator principal may only LIST ITS OWN pending and * CANNOT decide (a requester approving its own high-risk op would defeat the gate). **Empty/unset = * legacy behavior** (the shared service token IS the operator boundary — back-compat). */ operatorPrincipals: string[]; /** * DESIGN-278 §5 S2([ref])—— `GET /v1/config/catalog`(配置目录自描述端点)的显式旋钮。 * 默认 **ON**(设计稿 §0.5:合规部署可关)。端点本身已是 operator-only(`explicitOperatorOk`, * 空名单=谁都不是),本键给的是「即便 operator 也不许从 wire 读部署事实」的合规姿态:关 ⇒ 路径 * 不存在(404 族,与 memory-consolidation 阀门同判:旋钮没开 ≠ 能力缺失)、能力位 `configCatalog` * 同步翻假。`CONFIG_CATALOG_ENABLED=false`。 */ configCatalogEnabled: boolean; /** Tool names that are always denied. */ approvalDeny: string[]; /** [ref] durable-checkpoint (core 1.67): when true (+ a TiDB pool), F4 uses durable **suspend/resume** * instead of poll-blocking — a gated tool `ask` suspends the task (status:"suspended" + checkpoint), an * operator resumes it on any replica. Default OFF([ref] A-5:轮询车道已退役,OFF+门意图 ⇒ core UNGATED * 告警,不再有回退门形). `DURABLE_APPROVAL=true`. */ durableApproval: boolean; /** S-178:上面那一位**是谁定的**(闭集四词,见 `posture-source.ts` —— 与 `readFaceSource` 同一张表)。 * 7.67.0 起本键进 posture 家族:单机 turnkey ∧ 有耐久店 ⇒ 默认 ON,多租户/内存底座仍 OFF,显式 env * 两个方向恒赢。来源上 operator 读面(`GET /v1/diagnostics/wiring` 的 `serverGates`),因为「这台机器 * 为什么开着 park/resume」在升级后是运维必须看得见的一句话(与 S-167 的补偿项同族)。 */ durableApprovalSource: PostureSource; /** S-178:`STREAM_ASK_WINDOW_MS` 的来源(同一张四词表)。值在 {@link StreamApprovalConfig.windowMs}; * 单机 turnkey 缺席 ⇒ 24h(posture),多租户缺席 ⇒ 5min(engine-default),显式 env ⇒ env。 */ streamAskWindowMsSource: PostureSource; /** [ref] seam #2 (preemption) / [ref] ("失败≠没做完" third state): when true (+ durable mode), the * /v1/runs durable path opts every task into core `resourceSuspend` — a budget/turns/walltime limit OR a * scheduler preempt (`POST /v1/assistant/tasks/:id/preempt`) durably SUSPENDS the task (resumable via * `POST /v1/assistant/tasks/:id/resume`) instead of FAILING. 🔴 PRECONDITION for `preemptSignal` to fire at * all (core eligibility: resourceSuspend opt-in AND a durable checkpointStore AND durable tool-results AND a * remote suspendable env — else preempt is a silent no-op). Default OFF → unchanged fail-on-limit behavior. * `RESOURCE_SUSPEND=true`. */ resourceSuspend: boolean; /** Retention TTL (seconds) stamped on a resource/preempt checkpoint's `deadline` so the durable reaper GCs an * ABANDONED (never-resumed) suspend. 0 ⇒ core default (30d, `DEFAULT_RESOURCE_SUSPEND_TTL`). A legitimate * resume happens long before this; the deadline only reaps abandonment. `RESOURCE_SUSPEND_TTL_SEC`. */ resourceSuspendTtlSec: number; /** * [ref] 件 S-3 —— **跨片总额**:一条任务在整条 park→resume→再 park 链上允许烧掉的 token 总量。 * `RESOURCE_SUSPEND_TOTAL_TOKENS`,**0 / 缺省 = 不设**(维持现状)。 * * 🔴 为什么这是一根独立的旋钮、而不是 `MAX_TASK_TOKENS` 的同义词:`limits.maxTokens` 是 core 的 * **per-SLICE 窗**(core `types.d.ts` 逐字:"PER-SLICE windows, not whole-task totals"),而一次 resume * 会**重开满窗**。跨片总额是唯一能跨越 park 边界约束总量的轴 —— core 把它冻在 checkpoint 的 * resource ledger 上,`remainingTokens(ledger)` 在 total 缺席时回 `undefined` = **无天花板**。 * [ref](plan_review approve 后一条 resume 腿烧 75 万 token 无产出)的三个叠加因子之一就是本值缺席。 * * 🔒 **冻结语义**:core 把它冻在 checkpoint 的 resource ledger 上("Set ONLY on the FIRST slice"; * `debitLedger` 逐字 `prior ?? total`)⇒ 续跑腿重供**无效**,「一次 resume 刷新不了自己的额度」这条 * 安全语义由 core 保证。本仓每片都供值是无害的(首片生效,其后被 prior 挡掉)。 */ resourceSuspendTotalTokens: number; /** [ref] 件 S-3 —— 跨片总额的 **$ 轴**(`totalTokens` 的姊妹,语义与冻结形同上)。 * `RESOURCE_SUSPEND_TOTAL_BUDGET_USD`,0 / 缺省 = 不设。可为小数(美元)。 */ resourceSuspendTotalBudgetUsd: number; /** * [ref] 件 S-3 —— 跨片 **片数**上限:整条 resume 链最多几片,超出 core 以 `"suspend.loop"` 收场。 * `RESOURCE_SUSPEND_MAX_SLICES`,0 / 缺省 = 不设。 * * 它是**零进展 slice 循环**的兜底(两条总额才是主约束),与重启环的 `maxSuspends` 是两个独立计数。 * core 的 JSDoc 逐字点名了本仓这个角色:"a host driving resumes unattended **should set this**"。 * * 🔴 **它与两条 totals 的冻结语义不同**(codex 交叉复审 R1-[medium],验真后改口):core **不把它写进 * 账本**(`debitLedger` 的返回形里没有这一格),每片现读 `spec.resourceSuspend.maxSlices` 去比账本上 * 累计的 `sliceCount`。⇒ 本仓在**每一片**(含续跑)供值是**载荷不是冗余**:只在首片供值等于让续跑腿 * 供 `undefined` = 上限当场消失。代价如实登记:滚动部署改这根 env,已 park 的链按**新**值判 —— 部署 * 级旋钮本就该跟着当期部署走,但别把它误读成「冻在链上」。钉:test/resource-suspend.test.ts 的上游契约格。 */ resourceSuspendMaxSlices: number; /** [ref] D-E (upgrade-budget): max NORMAL gated tool calls AUTO-APPROVED per run-leg before the rest * escalate to a human — a circuit-breaker against a runaway/inflated approval loop (an AI stuck re-asking), * NOT a cap on safety asks. 0 = OFF (every gated tool asks a human — unchanged, back-compat default). Clamped * to ≤10000 (the hard ceiling). A Supervisor deployment sets this high to get "the AI auto-approves routine * asks, the human only sees the rest". `APPROVAL_AUTO_BUDGET`. */ approvalAutoBudget: number; /** [ref] D-E (inv#2 — safety asks are never budgetable): gated tools that ALWAYS require a human even when * the auto-budget is on — the "irreversible / never auto-approve" set. empty = the budget may auto-approve any * required tool THIS layer sees. `APPROVAL_NEVER_AUTO`. * * 🔴 **NOT the only human-gate under an auto-budget**(2026-08-10 修口,[ref] 件2)。这里原来写的是 * "(In-service every gated ask is gate.kind=\"human\", so this set is the ONLY way to keep an irreversible * tool human-gated under an auto-budget.)" —— 两句都已过期,且过期方向是**高估本旋钮的责任**: * · [ref] D-2 起,带 egress / irreversibility 静态标记的工具铸的是 `irreversible_ask`,不是 `human`; * · core 5.24.0([ref]/[ref]/[ref])起,带 `requiresRealApproval` 的 ask 一律铸 * `irreversible_ask` + `gate.realApproval:{origin}` + checkpoint v8 —— **不再铸可预算的 `human`**。 * 而 `requiresRealApproval` 的生产者(`createTranscriptIntegrityPolicy` / `createUnverifiableDeletePolicy`) * 被 core 在 `prepare-task.js` 里**无条件**焊进每一条策略链,所以这条路径在**每一个**部署上都活着, * 不是 org 治理部署的专属面。 * ⇒ 本旋钮承担的是**本层**(`createDurableAskPolicy` 的 per-leg 计数预算)的 inv#2;安全类 ask 另有 * core 侧那两层守卫兜底,两层各自独立、不互为前提。消费侧钉:`test/durable-approval-policy.test.ts` * 的「[ref] 件2」describe(含反向锚:普通调用仍走预算自动批)。 * * 🔴 **NOT required to be a subset of `approvalRequire`**(2026-07-31 修口)。这里原来写的是 * "Subset of approvalRequire",而 `approval.ts` 的 inv#2 执行面逐字相反: * 「even if it was not also listed in requireApproval — the footgun: neverAuto ⊄ requireApproval」。 * 两处文档互相矛盾**不是文字问题**:`main.ts` 的 `operatorGateIntent` 是照着"子集"那句写的 * ——若真是子集,枚举 `approvalRequire.length > 0` 就覆盖了它,漏掉看着无害。实际不是子集,于是 * 「只配 APPROVAL_NEVER_AUTO」这一形被判成"零门意图" ⇒ 落进单用户 allow-all 基线 ⇒ 那张表 * 从未进入任何 policy。判定现在单源在 `hasOperatorGateIntent()`(与 inv#2 同文件)。 */ approvalNeverAuto: string[]; /** [ref] D-G (HMAC = integrity, not identity; ✅ WIRED — the "SCAFFOLD, not yet wired" note here was STALE, * corrected 2026-07-25: consumed by `verifyDirectDoorProof` → `verifyApprovalHmac`, reached from the live decide * routes — the two `verifyDirectDoorProof(` call sites in `http/routes/approvals-assistant.ts`): the * sema-registry-managed, ROTATING key-set used to verify an * approval-decision envelope's integrity on a direct-connect door. Empty (unset `APPROVAL_HMAC_KEYS`) ⇒ D-G * inactive (BFF-minted-principal is the only /decide door — back-compat), which is why "wired but off" is the * accurate description of every stock deployment. sema-registry mints + rotates the set; the shape is * provisional (ASK-CENTER-2026-06-17). */ approvalHmacKeys: ApprovalHmacKey[]; /** [ref] D-G: open the DIRECT-connect /decide door (a CLI/desktop client without the BFF). When true, this * worker is in DIRECT MODE — it REQUIRES the crypto direct-door proof (signed principal JWT + HMAC envelope) and * treats the trusted `x-agent-principal` header as UNTRUSTED (the BFF runs on a separate non-direct worker). Off * (default) ⇒ the BFF trusted-header door is byte-identical. `DIRECT_APPROVAL_DOOR=true`. */ directApprovalDoor: boolean; /** [ref] D-G: the issuer's PUBLIC JWKS (non-secret, config-projected like operatorPrincipals) the worker * verifies a direct client's principal JWT against. Empty ⇒ no trust anchor ⇒ the direct door cannot open. * `PRINCIPAL_JWT_PUBKEYS` (JSON [{kid,alg,key(PEM),status}]). */ principalJwtPubkeys: PrincipalJwtKey[]; /** Expected `iss` / `aud` of a direct client's principal JWT. `PRINCIPAL_JWT_ISS` / `PRINCIPAL_JWT_AUD`. */ principalJwtIss?: string; principalJwtAud?: string; /** Max accepted principal-JWT lifetime (exp − now), seconds — caps the replay window past the short-lived mandate. * 0 = no cap. `PRINCIPAL_JWT_MAX_TTL_SEC` (default 120). */ principalJwtMaxTtlSec: number; /** Computed: the direct door is truly active (all anchors present). Off ⇒ direct branch never runs. */ directDoorActive: boolean; /** [ref] b (item 3): the INFRA cost rates the service owns (the engine prices only LLM tokens), composed * with core's costBreakdown for the supervisor cost surface. All default 0 (= not priced, like MODEL_COST_*). * `INFRA_COST_TOOL_CALL` / `INFRA_COST_SANDBOX_SEC` / `INFRA_COST_EGRESS_GB` (micro-USD). */ infraCostRates: InfraCostRates; /** v2 leader endpoint ([ref]): enable POST/GET /v1/leader. Requires an ISOLATED remote-exec backend — * REMOTE_EXEC=e2b, OR REMOTE_EXEC=k8s (Kata pod-sandbox) WITH an S3/MinIO config for the workspace-durable * snapshot/merge(装配点 `boot/leader.ts#assertLeaderDurableStore`) — + (deploy) git on the host + a durable remote with creds. Orthogonal to the * D-G direct door (auth axis) — a worker can be direct-mode + kata + leader at once. Default OFF. `LEADER_ENABLED=true`. */ leaderEnabled: boolean; /** [ref] A6 (correctness-first knob): `LEADER_FANOUT_ENABLED=false` pins the leader router to the * deterministic single route (never fans out, zero router cost). Default true (router decides). */ leaderFanoutEnabled: boolean; /** A value router (S1 verdict §6.3 cash-out, src/router/route-orchestration.ts): auto-decide per-task * orchestration — default single, escalate to the SUP prevention posture (shellGate gate) on a DETERMINISTIC * capability-danger signal (write/exec on a NON-isolated env). DEFAULT OFF (`ROUTER_ENABLED=true`): ships the * mechanism dark; enable after a live validation (the router only ever TIGHTENS via tightenTaskSpec + honors * caller-explicit modes, so enabling can't loosen any gate). */ routerEnabled: boolean; /** S8 LLM self-orchestration ([ref]): mount core's `run_workflow` tool so a strong model can author a * deterministic JS orchestration script, run under the HARDENED-vm sandbox (src/orchestration/hardened-vm-runner.ts). * DEFAULT OFF (`SELF_ORCHESTRATION_ENABLED=true`). When off, core is FAIL-CLOSED (tool not mounted, prompt not * injected) — zero impact. When on: the hard runner + a CONSERVATIVE governance baseline (workflow children are * `handsReadOnly` by default) are wired into RunnerDeps; loosen deliberately via SELF_ORCHESTRATION_MODELS etc. */ selfOrchestrationEnabled: boolean; /** §4 (Fork): deployment enablement for CC `/fork` (the core `Fork` tool wire). Posture default (single-user * turnkey → ON; multi-tenant OFF); `FORK_ENABLED` overrides. 🆕 In single-user turnkey the Fork tool is mounted for * the LLM BY DEFAULT (clay 2026-07-01; per-task opt-OUT via `body.enableFork:false`); multi-tenant is honored only * with a per-principal entitlement resolver wired (core enforces `allowFork` per-principal, registry-core 0.1.47) — * else fail-closed (see enableForkFromBody). */ forkEnabled: boolean; /** observer 开闸线(core 1.270.0/1.273.0 `RuntimeCaps.allowObservers`,`=== true` 显式 * opt-in、默认 OFF —— 极性与 allowWorkflows/allowFork 相反):`EXPERIMENTAL_OBSERVER_AGENTS=true` 在 * **单用户部署**直开(env 基线;命名先例=CC 的 CLAUDE_CODE_EXPERIMENTAL_OBSERVER_AGENTS * 对儿)。**多租户(REQUIRE_PRINCIPAL=true)不认 env,只认 center caps 的 allowObservers 键** * (置了也只 boot warn 一次,行为零变——半配置 fail-loud)。⚠️ 非 postureOn:observer 是实验面, * core ships dark,单用户 turnkey 也不默认开 —— 只认显式 "true"。合成序见 applyObserverEnvOptIn。 */ experimentalObserverAgents: boolean; /** S8 governance baseline: the model NAMES an LLM-authored workflow script may pick ([ref] §2.5). Empty ⇒ * FAIL-CLOSED (the script cannot pick a model → spawned children use the workflow's default role). NEVER the * whole catalog. `SELF_ORCHESTRATION_MODELS=a,b`. */ selfOrchestrationModels: string[]; /** S8 v2 resource isolation: run the workflow script in a `worker_thread` (heap cap + terminate) instead of * in-process. Adds DoS isolation (memory-bomb / microtask-starvation) on top of the escape membrane. Default * OFF (in-process, faster). `SELF_ORCHESTRATION_WORKER_ISOLATION=true`. */ selfOrchestrationWorkerIsolation: boolean; /** SVC-1: the durable WorkflowRunStore backend for BACKGROUND workflow runs. * `auto` (DEFAULT) = the SQL-backed cross-replica store when a SQL backend is configured, else the * crash-safe `FileWorkflowRunStore` ledger under `localDataRoot/workflows` (dispatch: main.ts, keyed off * `sqlWorkflowRunStore` presence). `file` = force the file ledger even with SQL present (single-replica * pinning). Either durable form means a background run's record SURVIVES a restart, which is what lets the * at-least-once completion notify re-derive a run's terminal state after a crash. `memory` = the ephemeral * `InMemoryWorkflowRunStore` (process-local; no crash recovery, no notify journal) — an explicit opt-out for * a single-instance/ephemeral deployment. ([ref]: the old docblock claimed `file` was the default and * omitted `auto` entirely — a reader picking a backend off this comment would never learn the SQL twin * exists.) `WORKFLOW_RUN_STORE=auto|file|memory`. Only consulted when SELF_ORCHESTRATION_ENABLED. */ workflowRunStoreBackend: "auto" | "file" | "memory"; /** SVC-1 (adversarial-review): a workflow still `running` past this age is deemed a crash-orphan by the periodic * notify-recovery sweep (core never resumes/reaps a prior `running` row) → finalized-as-abandoned so the * originator learns it died + the journal entry stops leaking. MUST exceed the max expected workflow runtime * (the run store has no cross-replica liveness signal). Default 24h. `WORKFLOW_ORPHAN_GRACE_MS`. */ workflowOrphanGraceMs: number; /** R7 self-wake: opt-in self-wake scheduler on the single-user TOC host lane. When true (and * the host lane is single-user, `requirePrincipal:false`), a `FileSchedulerBackend` is injected into the host * `RemoteHostExecutionEnv` → `hasScheduler(env)` mounts core's CronCreate/CronDelete/CronList tools(L8 订正:CronCancel 旧名、Sleep 已撤除), and * the TOC shell `SchedulerDaemon` reads the SAME store to fire due intents. DEFAULT OFF (the model self-scheduling * is a power; multi-tenant deployments route scheduling to center, not this host daemon). `SCHEDULER_ENABLED=true`. */ schedulerEnabled: boolean; /** [ref] C4: host-lane project-memory injection (CLAUDE.md + git narrative). Default **ON** (subject to the * host-lane + single-user gate), so "open like CC" works without config. `PROJECT_MEMORY_ENABLED=false` opts out. * [ref] B4 (lens4 N8): this was `projectMemoryDisabled` — the ONE negative boolean on a ServiceConfig whose * other 16 flags are positive `*Enabled`/`*Allowed`, so every read site had to invert twice. 🪦 server 3.0.0: * the retired env name `PROJECT_MEMORY_DISABLED` is a **fail-loud tombstone** — setting it (any value) refuses * boot with a message naming this knob; it is no longer read. */ projectMemoryEnabled: boolean; /** R7: override the shared scheduler store path. Default `SEMA_CONFIG_DIR`→`~/.sema/scheduled_tasks.json` (the * server writer + the shell daemon reader MUST agree). `SCHEDULER_STORE_PATH`. */ schedulerStorePath?: string; /** Boot config-fetch budget (ms): the remote first pull races this window — inside it, boot applies * the config synchronously (今日常态,健康中心 <300ms);过窗则 server 立即以 env/兜底起服,该次拉取 * 转后台,到货即触发一次刷新 tick 热应用(roster 开门/restartRequired 语义照旧)。真机实测:黑洞形 * 中心让同步 boot 挂 ~5.7s(SYN 重传节奏,8s fetch 超时内)——本地 TOC 用户的「启动 5 秒」就是它。 * 默认 1500;调大(≥8000)≈旧的全阻塞行为。`CONFIG_BOOT_FETCH_BUDGET_MS`。 */ configBootFetchBudgetMs: number; /** [ref]②/[ref]③ supportsSessionWakeup host-signal (core 1.309 ScheduleWakeup 治本位): the SHELL knows * whether a resident daemon exists to honor session wakeups — when it spawns a headless engine with none, * it sets `SCHEDULER_SESSION_WAKEUP=false` and core's ScheduleWakeup refuses up front (fail-loud) instead * of parking a wakeup that would never fire (TB background-reap族 [ref]②b). Default ON (the co-resident * TOC daemon does honor its own sessions' wakeups); opt-out only — SCHEDULER_ENABLED 同门. */ schedulerSessionWakeup: boolean; /** [ref]/[ref] session-lifetime 同族 host-signal(5.6.0):headless 部署(spawn 时已知无常驻 daemon * 收割 session-lifetime 记录)设 `SCHEDULER_SESSION_LIFETIME=false` ⇒ capability 面 * `supportsSessionLifetime:false` ⇒ core 的 CronCreate 前门拒收 durable:false(fail-closed 于工具层), * backend.schedule 第二道同拒——session 记录不落盘=孤儿不产生。Default ON(co-resident TOC daemon * 真会收割);opt-out only,WAKEUP 同形。 */ schedulerSessionLifetime: boolean; /** R-survey/EnterPlanMode (core 1.167): MODEL-DRIVEN plan mode. When true, runs start with * `TaskSpec.enablePlanMode:true` but NOT `handsReadOnly` (writable-start) → core auto-mounts `enter_plan_mode` * (+ `present_plan`) so the model can self-call `enter_plan_mode` at ANY time it judges a task needs planning * (CC EnterPlanMode parity). 🆕 DEFAULT ON (clay: model-driven plan is resident — the model decides when to plan); * set `PLAN_MODE_ENABLED=false` to opt out. USER-driven plan (`body.permissionMode=plan` → enablePlanMode + * handsReadOnly, read-only from start) is unaffected/independent. */ planModeEnabled: boolean; /** * [ref]⑥(core 5.56 `Hooks.timeoutMs` 座)—— **每一只 hook 席位单次调用的时间上限**(ms)。 * `HOOKS_TIMEOUT_MS`;**缺席 = 不铸键**,由 core 自默认(600s = CC 自己的数;server 不复制上游默认)。 * * 🔴 `0` 是有效值(core 契约逐字:写下零的部署说了一件事 —— 每个席位立即到期),不是「未设」; * 坏值(非整数 / 负 / 超 `setTimeout` 32 位上限)**拒启**,解析见 config.ts `parseHooksTimeoutEnv`。 * 施加点 = `main.ts` 的 `deploymentHooks`(server 车道)与 `run-local.ts` 的同名记录(CLI 本地车道, * merge-rescan 745 补折:该腿今日仅同步计数器席位、折上为防倒退)。server 车道一处施加两条腿同时 * 生效:既是 `RunnerDeps.hooks`,又是 `resolve-spec` 折进 task hooks 的部署基线。 */ hooksTimeoutMs?: number; /** SVC-2 (adversarial-review): the workflow_journal GC retention — the reaper purges journal rows older than * this (bounds the heaviest, TaskResult-bearing table; a resume older than it re-runs live). Default 7 days. * `WORKFLOW_JOURNAL_RETENTION_MS`. */ workflowJournalRetentionMs: number; /** Retention (the store never auto-purges; reap is explicit and the * deployer's job): the reaper deletes TERMINAL workflow_run rows older than this. SQL twins only (the * cross-replica backend is where unbounded growth bites; the contract has no cross-scope enumeration, so the * twins add a DISTINCT-scope sweep — the File store keeps the transcripts-like keep-everything posture). * Running rows are never reaped (that is the orphan-grace sweep's job). Default 30 days, floor 1m. * `WORKFLOW_RUN_RETENTION_MS`. */ workflowRunRetentionMs: number; /** [ref]-T1 — core 5.8.0 部署级治理窗(`RunnerDeps.usageWindows`):滑动窗内跨任务预算, * key=principal(缺席=GLOBAL 一口锅,per-key 各一份额度——混流部署语义见 USAGE.md)。 * `USAGE_WINDOWS` 单键 JSON 数组(每项 {windowMs, maxTokens?, maxCostUsd?, anchor}); * 缺省未设=关(零行为变化)。[ref](core 7.0.0):`maxCostUsd` 是与 maxTokens **对等**的第二只 * ceiling(绝对美元;whichever fills first 即耗尽;缺席的 ceiling 不参与——非 0、非 ∞)——两者 * each optional on its own、**at least one required**(双缺=parseUsageWindows 响亮拒)。fail-loud: * 坏 JSON/空数组/坏值/未知 anchor → boot throw(parseUsageWindows)。窗形变更≈重开账(first-use * 桶按 windowMs 配对,老桶下次 charge 即弃)。$ 窗要求 run 可计价:无价目模型撞 $ 窗=core 门口拒 * (`config.usage_window_unpriced`),USAGE.md §9.2。 */ usageWindows?: readonly { windowMs: number; maxTokens?: number; maxCostUsd?: number; anchor: "first-use" | "rolling"; }[]; /** core 1.364 durable background agents([ref] 提货单③):reaper 经 `TaskRegistry.reapDurableAgents` * (联合 reap——条件删赢了才 release 转录 session;裸 store.reap 会 strand 转录)按 scope 清 TERMINAL * `a*` 行,老于此值的删。SQL twins 走 DISTINCT-scope 枚举;local FileBackgroundAgentStore 单租户 * scope="default"。Default 7 days, floor 1m。`BG_AGENT_RETENTION_MS`。 * 🔴 [ref]([ref]②,设计判定 2026-08-30):这根旋钮**同时是子代转录(placed 分区)的生效留存视界**—— * 联合 reap 行赢删才 release 转录,分区孤儿腿 `olderThanMs` 同源(core `reapDurableAgentsLane` 亲核), * retention lane 对 placed 显式让权(retention-store-sql.ts 第五腿)。core 词表常量 * `SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT`(30d)在 core dist 零消费,本服务未按它跑;天数形 * 专用旋钮无 core policy 座,候 core 扩座后再铸(生效值已投 wiring_static 日志)。 */ backgroundAgentRetentionMs: number; /** 同批:stale-running 翻转窗——running 行的写者心跳(core 60s)断更超过此值 = 宿主进程死,行翻 * failed/stoppedBy=system(诚实终态,跨实例读面永不虚构存活)。core 契约要求 ≥10min(60s 心跳的 * 安全裕度),**下限 10min 硬钳**。Default 15min。`BG_AGENT_STALE_RUNNING_MS`。 */ backgroundAgentStaleRunningMs: number; /** core 1.382([ref],[ref] 提货单④)`TaskRegistry.reconcileParkedAgents` 的 stale-claim 清算窗 * ——core 该整块逻辑被 `opts.staleClaimMaxAgeMs !== undefined` 门住,缺席 = **整块清算永不运行** * (claim 后 claimer 进程崩死的行永久卡在"已 claim 不可再 claim"态,只能等 checkpoint TTL 自然过期; * checkpoint 已 resolved 但 finalize 未落的行——人已做出的审批决定——同样不会被诚实翻 failed)。 * cli 复查([ref] F1)抓获:server 侧此前调用漏传本项,清算臂结构性不可达。Default 1h(claim→drive→ * finalize 正常全链本应秒级完成,1h 是给"进程慢但仍在跑"的宽松裕度)。`BG_AGENT_PARK_CLAIM_STALE_MS`。 */ backgroundAgentParkClaimStaleMs: number; /** [ref] MED2:agent_roster 的 TTL 清理窗(core [ref]②③ 派给部署的半场——task-scoped 具名 spawn * 每 run 一新行,无清理=线性无界)。按行的 recorded_at_ms(最后 upsert 触碰)删;SQL twins 扩展面, * File/Memory 店 core 自带 maxAgeMs。Default 30 days, floor 1m。`ROSTER_RETENTION_MS`。 */ rosterRetentionMs: number; /** [ref] 车4 件1(codex R1-high②):`leader_run` 行停在 `running` 超过这个时长即被清算成 `failed` * 并写清 WHY —— 驱动它的副本已经死了,没有任何东西在跑。**不是接管**(见 store 的 * `reapStaleRunning` 头注)。默认 26h(leader run 的文档上限 24h + 余量),下限 1m。 * Env: `LEADER_RUN_STALE_MS`。`LEADER_ENABLED=false` 的部署上这条腿根本不注册。 */ leaderRunStaleMs: number; /** 修8(三路复审 absorb-2):periodic scratchpad sweep TTL — the reaper removes per-session scratchpad dirs * ([ref]③ ensureScratchpadDir) whose mtime is older than this (env-facts.ts `sweepStaleScratchpads`; the E21 * session-DELETE purge is the primary lifecycle, this sweep is the orphan backstop). Default 7 days, floor 1m * when enabled; `SCRATCHPAD_SWEEP_TTL_MS=0`(or negative)disables the sweep entirely. */ scratchpadSweepTtlMs: number; /** RFC B5 (ENV-SELECT-AND-REGION-SOURCE 抽象点②): the deployment-region package-source selector injected into * every sandbox (`SEMA_PKG_SOURCE=cn|global|custom` — the image's pkg-source.sh hook materializes apt/pip/uv/ * npm sources at start). 🆕 Default `global` (clay 拍 2026-07-13 全部以海外为准 — official endpoints ride * env-first onto EXISTING images, no rebake); CN deployments set `SANDBOX_PKG_SOURCE=cn`; `none` = inject * nothing (the pre-2026-07-13 unset behavior — byte-identical sandbox). Operator env `SANDBOX_PKG_SOURCE`; * per-region via the registry effective-config (shell B3) later. */ sandboxPkgSource?: string; /** Auto-generate a session title at first submit (one cheap-model call per * session, write-once; the user rename layer is BFF-side). Default ON; `SESSION_AUTO_TITLE=false` opts out. */ sessionAutoTitle: boolean; /** S-178:上面那一位的来源(同一张四词表)。本键**不进 posture**(缺省恒 true,两形同值)—— * 报来源是为了让运维分辨「这台机器显式关了自动标题」与「它本来就开着」;壳此前注 `false` 的那条 * 理由(gateway 不可达时的 warn 噪音)已在 `session-titler.ts` 侧修掉。 */ sessionAutoTitleSource: PostureSource; /** RFC A2 (clay 拍 A 切片先行): mount the model-facing `SelectEnvironment` tool (spec.tools, 判定②(a)) when * the per-task image chain is live (k8s + image catalog). Default ON; `SELECT_ENVIRONMENT_TOOL=false` opts out. */ selectEnvironmentTool: boolean; /** RFC A1: inject structured sandbox envFacts into the task (`TaskSpec.envFacts` — core 1.240.0 whitelist * field; rendered as the `# Environment` block, sanitized+bounded core-side). Default ON — * facts only ride when the deployment actually knows them (image binding/pkgSource/egress), absent = block * unchanged. `SANDBOX_ENV_FACTS=false` opts out. */ envFactsEnabled: boolean; /** ③ sensitive-path write DENY set (core 1.295 `createSensitivePathPolicy`). The PATTERN SET is core's call * (RECOMMENDED_SENSITIVE_PATTERNS — .env/.ssh/keys/.git hooks+config/cloud creds/histories, rationale * documented core-side); the server only passes it through. `SENSITIVE_WRITE_PATTERNS` (comma-separated): * explicitly set = FULL REPLACEMENT of the recommended set (not a merge); `off` or an empty value = disabled * (empty array); unset = core's recommended set. * 🔴 **施加点 = governance 层,无条件([ref] / [ref] issue #29;搬家前「composed into the fs-write gate fold * on host-semantics lanes — task-settings.ts」的旧描述已作废)**:与 `AUTONOMY`/`MANUAL_MODE_SHELL_GATE` * 同拍,resolve-spec 预铸成一条纯 DENY policy 递给 `applyRuntimeGovernance`,**不看客户端 settings/ * permissionMode 的在场性、不看落在哪个模式臂、不看执行 lane**。旧家把它唯一的合成点挂在 * `deriveSettingsPolicy` 的 fs-write gate 闭包里,而那个闭包只在 default/auto/acceptEdits 三臂被调用 ⇒ * `bypassPermissions` / `permissionMode` 键缺席(headless `-p` 的默认姿势)/ body 连 settings 都没有 * 这三形整条 DENY 腿不建(邻仓 46 格真机矩阵实证)。裁决 env 仍按 lane 分形(host 真 fs / 沙箱 * `DeferredSandboxPathEnv` 代理),与 fs-write ask 门同一份、单点构造。 * 折叠仍是 deny-wins(tightenTaskSpec ⇒ combinePolicies),所以会话豁免 / acceptEdits 自动 allow / * 客户端 settings allow 一律越不过它;非守卫目标该 policy 返回 `action:"allow"`(core 的 ToolPolicy * 没有「无意见」第三态;这个 allow 在 combinePolicies 的 deny/ask 优先折叠里不构成一票,故在折叠 * 语境下等价于弃权)⇒ 不给 bypass 加 ask 门。 * ⚠️ 非法模式(不含任何路径段,如 `"/"`)在 boot 期 fail-loud(createResolveSpec 先编译一次)—— * **对存量部署是行为变更**:搬家前这种坏值只在 default/auto/acceptEdits 三臂上每请求炸,只跑 * headless 的部署带着坏值也能起服务;现在起不来(方向=运维当场看见,而不是每任务一条 500)。 * ✅ 旧注写的「沙箱 lane 相对形有一格无人裁决(cwd 里的守卫段看不见)」自 core 5.19.0([ref])起 * 已收口:守卫按 `ToolCallRequest.cwd` 解析写目标,两条 lane 同得。剩余射程边界(引擎未盖戳的直接 * 调用形)成文在 `deployment-governance.ts` 的 `RelativeTargetLexicalEnv` 类注。 */ sensitiveWritePatterns: string[]; /** * S-138 —— core **写保护名表**(`RunnerDeps.writeProtectedPaths` 座)的部署决定。两根 env 旋钮 * 共铸这**一个**键(它们写的是同一条语义面,两根同写 ⇒ boot 拒启,见 config.ts 的解析点): * · `WRITE_PROTECTED_EXTRA` ⇒ `{knob:"extra", entries:[...WRITE_PROTECTED_DEFAULT_TABLE, …增量]}`; * · `WRITE_PROTECTED_TABLE_REPLACE` ⇒ `{knob:"replace", entries:}`(`[]` = 显式无表)。 * * 🔴 **缺席 = 不铸键**(与 `readFace` 族同律):座缺席时 core 的 `resolveWriteProtectedTable` 返回它 * 自己的缺省 51 行 —— 本仓**不复制那张表**,上游改表之日起不会有第二份真源。「缺席 = 没装表」是 * B-023 F2 的那个误读,读面(`writeProtection` 两位)正是为了让它不可能再发生。 * * 🔴 为什么带 `knob` 而不是只存数组:`extra` 与 `replace` 铸出的 `entries` 在**值**上分不出来 * (一份 `[...DEFAULT, 两行]` 既可能是加法、也可能是运维手抄的整表),而两者对运维的意义相反 —— * 读面的 `source`/`replaced` 要回答的正是「运维动没动那张表」。从值反推 = 猜,故意存事实。 * 座席(纯数组)由 `buildDeploymentPostureSeats` 从本键**派生**(`crossSessionInbound` 同族形制: * config 存决定、座席存引擎形)。 * * ⚠️ 与 {@link sensitiveWritePatterns} 是**并列机制、不互相覆盖**(core d.ts 原话):那一套是模式 * (正则/glob)型 deny,这一套是字面名表的 `allow → ask` 降级。两个读面因此分开报。 */ writeProtectedPaths?: { /** 哪根旋钮写的(闭集;读面 `source` / `replaced` 的唯一判据 —— 不从 entries 的值反推)。 */ readonly knob: "extra" | "replace"; /** 座席原样递给 core 的表项(裸名简写或 `{name,kind}` 行;坏值在 boot 期由 core 响亮拒)。 */ readonly entries: readonly WriteProtectedEntry[]; }; /** [ref]§四 opt-in (cli[ref]② finding, core[ref] suggested mechanism): the CC manual-family permission * modes (default/auto/acceptEdits) gate every fs WRITE hand tool (Write/Edit/NotebookEdit) but never touched * Bash — a model can `echo -n '…' > file` straight past the write-approval card. `MANUAL_MODE_SHELL_GATE` * opts a deployment into ALSO tightening `TaskSpec.shellGate` (core `bashReversibilityProbe` under * `"classify"` asks on constructively-irreversible commands — shell redirects etc. — **plus** the boundary * case in the 🔴 note below; it is not a "constructive-irreversibility only" filter). * 🔴 **施加点 = governance 层,无条件([ref] / [ref] 分单②;搬家前的旧描述已作废)**:与 `AUTONOMY` * 同拍在 `applyRuntimeGovernance`(runtime-governance.ts)里施加,**不看客户端 settings/permissionMode * 的在场性、不看落在哪个模式臂、不看执行 lane**。旧家挂在 task-settings 的 default/auto/acceptEdits 三臂 * + host-lane fs-write wiring 上,等于让一个部署级旋钮的生死由客户端表态决定(print/headless 腿不 stamp * settings ⇒ 整块跳过;bypassPermissions 臂不回 shellGate;非 host lane wiring 不建 ⇒ 沙箱 lane 全失效)。 * rank 合成:与 autonomy 派生值取大(`ask`→`"always"` 压过 `"classify"`),对 base 只升不降。 * 客户端 settings 折叠在其后、tighten-only,拿不掉这个值;SUP 路由姿态只会把它抬到 `"always"`。 * 🔴 **语义随 core 版本演进(2026-07-31 核;core 7.15.0 再核)**:`"classify"` 下「纯读自动放行」这句 * 自 core 2.7.0 起**不再无条件成立** —— [ref] 给 classifier 加了 root 边界扫描,core 内部自铸 boundary * (`prepare-task.ts` 的 `shellReadBoundary`:roots=任务 root+additionalRoots+additionalReadRoots, * cwd=活 handsCwd),**读到 roots 之外的路径同样会 ask**(server 侧零接线即得:我们只传 shellGate, * 边界是 core 自算的)。 * 🔴 **而自 core 7.15.0([ref])起,那道读边界不再是本旋钮买来的** —— 它**每一档都判**(`off` 含在内), * 本旋钮买的只剩**剩余**风险那一半:`"classify"` = 可证只读且在界内的命令自动放行、其余发问; * `"always"` = 每条命令都发问。⇒ 别再把「越界读会问」写成开这根旋钮的理由(它在不开的部署上同样成立); * 这根旋钮仍然是 tighten-only、仍然无条件施加,语义一字未改。 * Absent/unset = `undefined` = **zero behavior change** (this is a tighten-ONLY opt-in, never a * default — a deployment must explicitly choose it); `"always"` = CC-strict parity (every Bash asks). Any * other value (incl. `"off"`) is treated as unset — this knob has no "off" value to opt BACK OUT with once a * session's baseline already tightened elsewhere (tightenTaskSpec would reject that as a loosen anyway). */ manualModeShellGate?: "always" | "classify"; /** [ref] ③(黑板 [ref]/[ref],core [ref] 合修)—— CC `permissions.disableAutoMode` 的本地镜像 * (`PERMISSIONS_DISABLE_AUTO_MODE`,布尔词表旋钮,opt-in 缺省 false):本部署对**每个** principal 把 * `RuntimeCaps.autoMode` 折成 `false` = org deny 位(core [ref] 三态谓词 `runtimeCaps?.autoMode !== false` * 的拒臂)。**tighten-only**:center 授予 `true` 翻不回;未设 = 不动 caps(无 center 的部署 caps 仍缺席 ⇒ * 「缺席=不阻」⇒ 壳发 `permissionMode:"auto"` 即武装)。词表外的值 boot 拒启(安全轴旋钮不许静默回默认)。 * 合成点 = `boot/runtime-caps.ts` 最外层(applyLocalAutoModeDeny);读面 = `/v1/capabilities.permissionModeAuto`。 */ permissionsDisableAutoMode: boolean; /** 7.78.0(core 7.18.0 [ref])—— **分类器席答不出话时怎么办**(`AUTO_MODE_CLASSIFIER_FALLBACK`, * 部署级旋钮,闭二词 `main` | `none`,词表外的值 boot 拒启)。 * * 为什么这一格必须由部署说了算:core 7.18.0 把**缺省**从「分类器跑不出裁决 ⇒ 这一次门控调用整条拒」 * 改成「主模型代判」(`main`)。auto 模式里的分类器是一道**筛子**,而 `main` 让**被筛的那只模型** * 兼任筛子 —— 这是权限轴上的一次放宽,愿不愿意接受它是部署的事,不是引擎的事。`none` = 只留分类器 * 那一级梯子 = **7.77.0 及以前的行为逐字**。 * * 🔴 **缺席 = 不铸键**(与 `readFace` 族同律,不复制上游缺省):上游改缺省之日起本仓不会成为第二份 * 真源。所以缺席时这台机器跟随 core 的 `main`,运维面由 DEPLOY-PREREQS 的升级段点名(「默认变了」 * 是升级须知,不是一个可以静默过去的实现细节)。 */ autoModeClassifierFallback?: "main" | "none"; /** S-111([ref] §4.4)—— CC `settings.crossSessionInbound` 的**部署/组织层**(引擎层形的 `managed` 层, * CC `policySettings` 对位):本部署对入站跨会话消息的治理三态。`accept` 投递 / `hold` 停在收件会话的待审 * 队列 / `refuse` 整体退出车道。**未设 = 本层不设**(不是 accept):引擎读作「managed 层未表态」,由其余层 * 与 mode-parity 决定。词表外的值 boot 拒启(保护型旋钮不许静默回默认)。 * ⚠️ 供层点 = `cross-session-settings.ts`,生效前提 = 引擎的 peer 目录席在场(座缺席 ⇒ 整条车道不挂)。 */ crossSessionInbound?: CrossSessionInboundSetting; /** S-111 —— CC `settings.dialogExpiry` 的部署层:held 跨会话消息等待人审多久后按安全缺省结算(带回执过期, * 不静默吞)。`"never"` = 不设期限。**未设 ⇒ 不铸键**,由引擎落它自己的缺省(`"5m"`)——本仓不复制上游 * 缺省值。词表外 boot 拒启。⚠️ CC 同名键还有第二个消费面(远端审批对话框停靠时长),本旋钮今天不驱动它。 */ crossSessionDialogExpiry?: CrossSessionDialogExpiry; /** [ref]④ defer face (EXPERIMENTAL, default OFF): mark the default scenario's LONG-TAIL first-party tools * (today = SelectEnvironment only) `defer:true` (core tool-disclosure: the model sees a placeholder until it * ToolSearch-loads the schema). Assessment says the win is marginal for us (spec.tools total ≈4.2k tokens, of * which Agent + the Task quartet ≈3.2k are CC-core posture = must stay inline; the deferrable tail is * ≈200-500 tokens/turn) and SendUserFile is deliberately NOT in the defer set (proactive-delivery tool — * hiding it behind ToolSearch suppresses exactly the spontaneous use it exists for). `TOOL_DEFER_LONGTAIL=true` * opts in. * 🔴 **core 7.15.0([ref])起 `defer:true` 只是「进候选集」的声明,不是「一定占位」的保证**:候选集 * (caller spec 的 `defer:true` + 远端协议铸名 + 内置自声明 defer)整体受**一道**阈值门,全或无 —— * 内联字节 ≥ 上下文窗的 **10%** 才延迟,否则全内联且不挂 `ToolSearch`。同一条收窄也让「MCP 工具恒被 * core 自动延迟」这句(本行旧文案)作废:缺省窗下一个中小 MCP 名册够不着阈值,首请求带全 schema。 * 唯一不受门的是调用方的 `TaskSpec.deferTools` 字面名单。口径全文见 src/capabilities/tool-defer.ts 头注。 */ toolDeferLongtail: boolean; /** LSP code-intelligence tool ([ref] §13.1): mount core's `lsp` tool, routed to an E2B LSP sidecar. Requires * REMOTE_EXEC=e2b. Default OFF (the on-the-fly language-server install is heavy until an E2B template bakes it). * `LSP_ENABLED=true`. */ lspEnabled: boolean; /** TOC local LSP (core 1.190 `NodeLspManager`, host lane only): mount core's `lsp` tool over LOCAL `child_process` * language servers (CC `services/lsp` parity). Unlike the sandbox lane it needs NO baked template — just the * server binary on PATH, and it DEGRADES gracefully (ENOENT → grep/read) + spawns lazily/cached, so it is DEFAULT * ON for the host lane (CC-parity); set `LSP_HOST_ENABLED=false` to opt out. (`REMOTE_EXEC=host` only — ssh/adb/ * local-docker can't reach a local child process to the right filesystem.) * [ref] B4 (lens4 N7): this lane used to share `LSP_ENABLED` with `lspEnabled` above — one env name driving * two OPPOSITE defaults. It now has its own knob. 🪦 server 3.0.0: `LSP_ENABLED=false` (the pre-split host * opt-out) is a **fail-loud tombstone** — it refuses boot naming `LSP_HOST_ENABLED`; `LSP_ENABLED=true` keeps * its own meaning (the sandbox lane's opt-in above) and is unaffected. */ lspHostEnabled: boolean; /** Graceful drain: how long SIGTERM waits for in-flight turns (live streams + bg/resume legs on this * instance) to finish before the hard shutdown. During drain, new billable submissions get 503+Retry-After and * /health carries `draining:true`. Default 10min (an interactive turn is minutes, not seconds — a * 10s hard-cut would kill long turns); floor 10s. Env: DRAIN_GRACE_MS. SIGINT (Ctrl-C dev * ergonomics) and a SECOND SIGTERM skip the drain and hard-stop immediately. */ drainGraceMs: number; /** B3 (shell [ref]②): SIGHUP idle window. SIGHUP means "the process-group owner's terminal went away" — but * under the shared-engine shape (several shell sessions on one config, engine rides the FIRST shell's process * group per the T11 anti-orphan call) other live sessions may still be using this engine, so an unconditional * drain kills a peer mid-turn (499) and 503s new submits. Instead SIGHUP arms an idle watch: shut down only * after `inflight()` (bg/resume legs + live streams) has been 0 for this long, WITHOUT flipping `draining` * (peers stay fully served). A true orphan (last window closed) self-terminates when the window expires — * T11 stands, just bounded instead of immediate. A SECOND SIGHUP (or SIGTERM) escalates to the normal drain * path. Default 120s, floor 5s. Env: SIGHUP_IDLE_GRACE_MS. */ sighupIdleGraceMs: number; /** [ref](判据 S-292 / 出口 S-306 改版):引擎 **parent 监视**。壳自 spawn 引擎的同机形里,壳被 SIGKILL * (崩溃/强杀,没有任何清理钩子)后引擎被 reparent 到 init 而**继续活着** —— 孤儿引擎占端口、占库连接、 * 占模型配额、抱着 `LOCAL_DATA_ROOT/LOCK`(活孤儿是合法持锁人 ⇒ 下一只壳的引擎必撞锁),而且再没有人会给 * 它发 SIGTERM(T11 的进程组腿只覆盖信号送得到的形)。设了本键 = 引擎每 2s 自查一拍,**两支任一成立 * 即把自己标成孤儿**:① `process.ppid` 不再是启动那一刻的值(被过继走,强身份);② `process.kill(pid, 0)` 抛 * `ESRCH`(弱身份)。`EPERM` = 进程在、只是本进程无权给它发信号 ⇒ **算活着**。 * 🔴 **S-306(硬 breaking)**:成孤儿**不等于**停机 —— 看门狗的 drain 臂整条删。退不退由**同一条** * 附着租约律判(无附着 ∧ 无在飞 ∧ 静默满窗),孤儿态只是把窗从 {@link engineLingerMs} 换成 * {@link engineOrphanLingerMs}。于是「A 起引擎、B 按 engine.port 复用、A 正常退」不会再在 B 的 turn * 中间把引擎拆掉;自退仍走与 SIGTERM **逐字同一条** graceful drain 腿,不是裸 `process.exit`。 * **缺席 ⇒ 看门狗不装配**(opt-in;存量部署逐字零变化);**非正整数 ⇒ 拒启**(带指路文案)。 * 拍频固定 2s,不开 interval 旋钮(有真需求再议,防旋钮增殖)。 * 已知残余(成文,不收窄)见 `src/parent-watch.ts` 头注(boot 窗内的僵尸父 / 中间 wrapper 进程两形)。 * 监视是**尽力自愈**、不是强一致的父子生命周期绑定。 Env: `SEMA_PARENT_PID`。 */ parentPid?: number; /** [ref](黑板 [ref]②):引擎**附着租约自退**总开关。壳自 spawn 引擎的本机形里,壳退出之后引擎 * 继续活着 = 占端口/占库连接/占模型配额的孤儿。设了本键 = 引擎每 15s 自查一次「还有没有人附着」, * **三合取**(无附着 SSE 流 ∧ 无在飞 run ∧ 无非探针 HTTP 触点)**持续**满 {@link engineLingerMs} * 才自退,且自退走的是与 SIGTERM **逐字同一条** graceful drain 腿(不是裸 `process.exit`)。 * **默认 false** —— 服务器部署形永不自退(k8s replica 半夜没人用是正常低谷,不是「该退了」); * 壳 spawn 形由壳显式注入 `true`。**部署级旋钮无条件生效**,不派生自任何客户端表态。 * 🔴 **S-306**:与 {@link parentPid} 不再是「各走各判据的两件」,而是**一件 + 一个输入** —— 租约腿在 * 「本键为真 ∨ `parentPid` 在场」时装配,本键只决定 **owned 态**(父还活着 / 没被监视)的窗:为真 ⇒ * {@link engineLingerMs},为假 ⇒ **永不自退**(窗 = `Infinity`,与 S-306 前逐字同)。父死之后一律 * 改用 {@link engineOrphanLingerMs}。 Env: `ENGINE_AUTO_EXIT`(默认 off)。 */ engineAutoExit: boolean; /** [ref]:自退前三合取必须**持续**成立的静默窗。默认 60s —— 防的是**壳重启窗**:壳崩了/重启的 1-3s * 里附着数确实是 0,窗太短就在用户眼皮底下把引擎杀了,窗内任何一次重连即把计时清零。 * {@link engineAutoExit} 关着时本值不生效,但**照样解析、坏值照样拒启**(部署级旋钮的解析不挂在 * 另一个旋钮的表态上 —— 否则手滑值躺在配置里,等哪天有人打开自退时当场生效且从没人报过警)。 * **坏值拒启**([ref] A 档):非安全整数 / 落在 `[1000, 86400000]`(1s…24h)之外一律 boot 期 throw * 带指路 —— 「把 60(秒)填进一个毫秒键」是这类旋钮最常见的手滑,而 60ms 的 linger 等于把整条防误杀 * 腿静默摘掉;上界那一侧同样承重(codex R1-F2):`1e100` 能过朴素的「正整数吗」检查,而 elapsed * **永远追不上它** ⇒ 看着配好了、自退其实整件失效。要「永不自退」请用 `ENGINE_AUTO_EXIT=false`。 * Env: `ENGINE_LINGER_MS`(默认 60000,闭区间 [1000, 86400000])。 */ engineLingerMs: number; /** S-306:**孤儿态**的静默窗 —— `SEMA_PARENT_PID` 那个进程已经不在了之后,三合取还要**持续**成立多久 * 才自退。默认 **2s**:部署面的判据是「壳挂掉后数据根秒级重新可用」(B-117 的原病 = 孤儿引擎抱着 * `LOCAL_DATA_ROOT/LOCK` 不放),而此时已经没有「壳重启窗」要防 —— 那只壳不会回来了(要回来的是**新** * 壳,它走的是新 boot)。窗仍然不是 0:有活附着 / 有在飞 turn 时这条窗压根不开始计时(三合取先不成立)。 * 与 {@link engineLingerMs} **同一只校验器、同一对上下界**,坏值同样**拒启**;解析**无条件**,不看 * `SEMA_PARENT_PID` 在不在场(operator-knob 铁律)。 Env: `ENGINE_ORPHAN_LINGER_MS`(默认 2000,闭区间 * [1000, 86400000])。 */ engineOrphanLingerMs: number; /** E23 (shell-host contract): inbound MCP elicitation HITL. When enabled, the service mounts `RunnerDeps.onElicit` + the * `POST /v1/elicitations/:id/respond` route so an opted-in MCP server (`McpServerSpec.elicitation`, itself default * OFF) can ask the END USER for input mid-tool-call. LIVE-ONLY (no durable suspend; lost on crash/replica-change). * DEFAULT OFF (fail-closed): absent ⇒ `onElicit` is not wired ⇒ core advertises NO elicitation capability to any * server (doubly fail-closed with the per-server opt-in). `MCP_ELICITATION_ENABLED=true`. The throttle is the * deployment's job (core caps nothing) — bound concurrency/total/burst/TTL so an over-eager server can't flood. */ mcpElicitation: { enabled: boolean; throttle: ElicitationThrottle; }; /** §4④: AskUserQuestion LIVE-stream HITL — mount the QuestionCoordinator on `RunnerDeps.onQuestion` + the * `POST /v1/questions/:id/respond` route so the agent's AskUserQuestion tool routes to a live human over the run's * stream (the durable-suspend leg is wired separately). A CC cap → posture-gated (single-user turnkey → * ON; multi-tenant opt-in); `ASK_QUESTION_ENABLED=true/false` overrides. */ askQuestionEnabled: boolean; /** [ref] 裁2:AskUserQuestion 的**裁量窗**——每 run 腿的并发帽 / 总量帽 / 无人应答的释放窗。窗用尽或到期 * 不是拒绝、也不是替人作答:协调器报 `unavailable`,落点(durable park / 非 durable 合成续跑)归 core。 * `QUESTION_MAX_CONCURRENT_PER_RUN`(默认 2)/ `QUESTION_MAX_TOTAL_PER_RUN`(20)/ `QUESTION_TTL_MS` * (300000),越界值启动期响亮拒(照 `MCP_ELICITATION_*` 同门,不静默夹取)。缺席 ⇒ 协调器出厂窗 * {@link import("./question.js").DEFAULT_QUESTION_THROTTLE}(与本旋钮族默认逐字同值)。 */ questionThrottle?: QuestionThrottle; /** [ref]/[ref]② live tool-approval HITL — mount the ToolApprovalCoordinator on `RunnerDeps.onAsk` + the * `POST /v1/tool-approvals/:id/respond` route so a policy `ask` (the fs-write gate, ask-lists, non-durable safety * asks) routes to the live human as the CC three-choice card on the run's stream (core 1.290 sync-ask leg). A CC * cap → posture-gated like askQuestion (single-user turnkey → ON; multi-tenant opt-in); * `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([ref]⑤ fail-closed). */ toolApprovalEnabled: boolean; /** * [ref] 车二(core [ref] 持久化权限规则「不再询问」车道)**总开关**。`PERMISSION_RULES_ENABLED`, * **[ref] 起默认 ON**(clay [ref] 裁)。 * * 🔴 它曾经默认 OFF,而那个默认有**唯一**一条理由(codex 车二 round7 [high] 一):这条车道**只会加 * 放行、不会减**,而车二**没有撤销面**——列出 / 删除自己已存规则的口子没建,core 的 * `removePersistedRule` 在包里但本仓零调用点。一次误导入的全局规则因此会一直生效,租户自己拿它没 * 办法,只能找运维改库。[ref] 交付的正是那条撤销面本身(`GET /v1/rules` + `DELETE /v1/rules`,后者 * 恒经 `removePersistedRule` 产墓碑)加上 operator 的超范围回收腿 ⇒ **前置条件兑现,默认翻转**。 * 记这段史不是怀旧:哪天有人想把某条「只加放行」的车道默认打开,判据就是这一条 —— 收得回来才敢开。 * * 显式 `PERMISSION_RULES_ENABLED=false` 仍然关得掉(默认翻转不吃部署侧的表态);升级到本版的既有 * 部署若库里已有规则桶,boot 期会打一条 `permission_rules_activated_by_default` 诊断行,理由与三条 * 判据见 `boot/permission-rules-audit.ts`。 * * 关 ⇒ 规则店根本不装配:`RunnerDeps.permissionRuleStore` 缺席(引擎的 `permissionRules.storeWired` * 如实报 false)、ask 帧不带 `ruleOffers`、回决的 `persistRule` 拒、`/v1/rules/*` 全族 501。 * 三张表仍随中央 `ensureSchema` 建(纯 CREATE、零行),这不是行为面。 */ permissionRulesEnabled: boolean; /** 运维是否**显式**设过 `PERMISSION_RULES_ENABLED`(取值之外的一位,同 {@link dbBackendExplicit} 的 * 姿势)。唯一消费者 = boot 期休眠行审计:对显式表过态的部署说「N 只桶随默认 ON 激活」是假话。 */ permissionRulesEnabledExplicit: boolean; /** [ref] 车2([ref] §3.3 D3,窗长三元的安全余量):`ToolApprovalCoordinator` 的可选 `windowMarginMs` * 构造项——有效窗 = `min(ttlMs, legRemainingMs − 本值)`,余量不足 ⇒ 不开窗直接走窗到期同路(park)。 * 仅在装配点把 `ToolApprovalRunContext.legDeadlineMonotonic` 传给协调器(车3 的活)时才实际生效——本 * 旋钮本身缺省对现行为零影响(D1)。`STREAM_ASK_WINDOW_MARGIN_MS`,默认 10000(10s)。ttl 概念沿用既有 * `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */ streamAskWindowMarginMs: number; /** [ref] 车3([ref] 流内审批协议)总开关 + 四个从属旋钮。 * * **现行默认(clay 裁 2026-08-08 走 (a),[ref] 翻真验证后)**:`STREAM_APPROVAL_ENABLED` 默认 **true** * = 协议整体默认 **ON**;`STREAM_ASK_WINDOW_MS` 默认 **300000**(5min,与 sync 腿既有活卡窗、与 * `DEFAULT_APPROVAL_TTL_MS` 对齐——「开协议」不再顺带把窗砍短)。⚠️ 版本坐标:翻转**已在树上生效**, * 发布线上落在 7.4.0 之后的下一个发布,所以一台自报 7.4.0 的**已发布** worker 仍是 OFF,别只按 * `/health` 的版本号推默认。显式 `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到 * 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、`askStore` 不注入 * 协调器。⚠️ **窗不在这份还原清单里了([ref])**:`STREAM_ASK_WINDOW_MS` 自此在关协议的部署上照样 * 生效(缺省 300000 == `DEFAULT_APPROVAL_TTL_MS` ⇒ 没配过它的部署仍然字节不变),理由见 * `StreamApprovalConfig.windowMs` 的辖域段。 * * **旋钮为什么存在(2026-08-08 翻转前的成文前提,已被上面的裁定取代,留作沿革)**:那一版默认 false + * 60s 窗,三问记的是那个世界—— * - 谁需要 60s 窗:opt-in 开了协议的部署——approver 就盯着流、秒级应答,窗短 ⇒ 少占一条活腿、更快落到 * 可恢复的 park 面。 * - 谁被伤:①未 opt-in 的存量部署(把 5min 砍成 60s = 人离开工位 90 秒回来卡已经没了);②**park 设施 * 不在场**的部署——窗到期后 `"unavailable"` 没有降级目的地,core fail-closed deny,60s 会把 * 「人 90 秒后批准 = 放行」变成「拒绝」,是真行为回归。 * - 补偿:把 60s 绑在**新旋钮 + 新开关**上,一个字不动 `DEFAULT_APPROVAL_TTL_MS`。 * ⇒ 翻真时「窗变短」这一项被从翻转里摘出(窗默认抬回 300000),被伤面①②因此消失;60s 只在运维显式设 * `STREAM_ASK_WINDOW_MS=60000` 时才回来。 * * `STREAM_APPROVAL_ENABLED` / `STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` / * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK` / `STREAM_APPROVAL_ADMIT_MAX_PER_OWNER` / * `STREAM_APPROVAL_RECONCILE_BATCH` / `STREAM_APPROVAL_PENDING_GRACE_MS` / * `STREAM_APPROVAL_ADHOC_GRACE_MS` / `STREAM_APPROVAL_ORPHAN_TTL_MS`(后四键 = 车5 收敛器)。 */ streamApproval: StreamApprovalConfig; /** * [ref] R-13 C(`UNATTENDED_APPROVAL_POLICY`;裁定 [ref]Q1「裁2:A+C」/[ref] 裁②)——一只治理 ask * 到了而**没有任何人**能答时,这条部署要什么终局。闭集 `park`(缺省)/ `deny`,**坏值 boot 拒启** * ([ref] A 档:一个拼错的词若被当成「未设」静默回缺省,运维会以为自己关掉了 park 积压而实际没有)。 * * **谁需要**:auto / headless / 无人值守部署(夜间批处理、CI 里的 agent、没有任何人盯屏的 worker)。 * 它们的 park 是死信:run 挂起候一个永远不会来的人,占着 checkpoint 与配额直到 TTL。 * **谁被伤**:配了 `deny` 却其实有人的部署 —— 那些本可以补批的操作变成当场拒绝。补偿 = 这是**显式** * 一句声明,缺省永远是 `park`;改回来只需删掉这一行 env。 * **不看任何客户端表态**(纯部署级;memory: operator-knob-must-be-unconditional —— [ref] shellGate 让 * 一个部署级旋钮的生死由客户端 posture 决定,同病犯过两次)。 * * 施加点 = `ToolApprovalCoordinator` 的构造参 `unattendedPolicy` **一处**,五条「无人可答」的臂读同一 * 个值(设计稿 §1 的表:(a) 有店窗到期赢 CAS /(b) 无店窗到期 /(c) 店报错 catch /(d) windowZero 与 * headless 腿 /(e) 断连强转 + emit 全灭 + 写侧准入超限 + **投递集合入口活性过滤清空**([ref] 归 * 位:与「查无活流」同一件事实的两个时刻,详见 `askBroadcast` 的该臂注))。`"timeout"` 宿主自报只留在 (b)(c) —— * 结束等待的确实是本仓的窗;其余三臂结束等待的是容量/连接/装配,自报恒缺席。 * * 🔴 **射程边界(成文,不是遗漏)**: * · 唯一让本旋钮**无处施加**的键是 `TOOL_APPROVAL_ENABLED=false`(协调器整体不构造,core 走它自己的 * headless 姿态)。**不造推断腿**:旋钮只在协调器在场时有意义,这一句是成文而不是代码分支。 * · ⚠️ **与 `STREAM_APPROVAL_ENABLED` 正交**(codex 交叉复审 R2-F2 验真后纠正 —— 本注上一版把两者 * 写成了一回事,是错的):协议开关关掉只是不注入 `askStore`/协议窗,协调器照常在场,于是 R-13 的 * D1 窗到期臂**照样**走 park 路由。这不是漏网:设计稿 §2 明写 park 的承载是 **core checkpoint** * 而不是 server 的 ask 行,「无 askStore 但有 durable 设施」正是 R-13 点名的**主受益部署**。 * 要回滚 R-13 的行为面,键是 `UNATTENDED_APPROVAL_POLICY=deny`,**不是**协议开关。 * 射程钉:`test/unattended-approval-policy.test.ts` 的 codex R2-F2 两格。 * · APPROVAL_REQUIRE 名单工具走 `createApprovalBaselinePolicy` 的 durable gate 类,**不经流内臂**, * 结构上不受本旋钮影响(裁定「非名单工具」的限定因此无需码内名单分支)。 * · **持久回放**(窗到期后向真源收敛、`ensureAsk` 幂等重入撞上已终结的行)读出 PARKING/PARKED 时 * **同样按旋钮改判** —— 它们与 (a) 是同一件事(赢家可能是别的副本或上一次调用),不统一的话同一只 * ask 的终局会取决于「谁赢了那把 CAS / 重试没重试」。人的真决议回放不受影响。 * · 唯一**不受**改判的是 `ensureAsk` 身份未定 / 坏行两臂(「身份不确定」的处置,不是「没有人」的 * 处置),于是 `deny` 部署上仍可能落极少数 park(店抖动期),已知且有意的残余。 * · `deny` 部署上 ask **行**仍会经 PARKING 记账(旋钮改的是交给引擎的终局,不是店语义)。收敛器只把 * PARKING 行绑到**已经存在的** checkpoint 上,而 deny 部署里引擎从不 park ⇒ 绑不上,行按 run 终局 * 自行收敛成 DENIED/VOID,不会凭空生出 park;残余 = 那类行的归因记成 `routing_failure`。 */ unattendedApprovalPolicy: UnattendedApprovalPolicy; /** [ref]① (clay A 案 = workflow 权限全面 CC parity): workflow 子 agent 的默认权限基线与主 LLM 同权(base `{}`)。 * 安全论证:主 LLM 与子 agent 同 root 同信任域,主 LLM 本就能写这棵树([ref] ask 门照管),单独钳子 agent 的 * 安全增益≈0(只防绕路不防直路的门不是边界);实测产品代价=[ref]A 死锁。`WORKFLOW_AGENTS_READONLY=true` = * TOB 保守部署的可选旋钮:workflow 子 agent 基线加 handsReadOnly(1.190 前的旧默认)。默认 off。 */ workflowAgentsReadOnly: boolean; /** Sandbox-image-pool BAKE job API (IMAGE-API-DESIGN.md §P2): enable /v1/images/bakes* (operator-submit + the * bake-runner claim/ingest control plane). Requires a TiDB pool (the durable bake row/event/lease tables) AND * a NON-EMPTY `operatorPrincipals` (boot-asserted — never an unrestricted-write build-host exec door). Default OFF. * `IMAGE_BAKES_ENABLED=true`. */ imageBakes: ImageBakeConfig; /** prove-before-scale stopgap: log every tool call (pre+post, incl. resumed legs) for diagnosis. `TOOL_TRACE=true`. Default OFF. */ toolTrace: boolean; /** Task Trace API (S1): persist the agent's reasoning/thinking into the durable run event log (for the CC-style * trace UI). Default ON; `TRACE_THINKING=false` disables capture for sensitive deployments. Redacted on persist. */ traceThinking: boolean; /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。显式坏词启动期响亮拒;只影响读面,写面永不 * 受它管(core 契约)。 * 🔴 **S-167(7.65.0)起「缺席」按 posture 分两形**(clay 裁定 2):单用户 turnkey(`REQUIRE_PRINCIPAL` * 未设)⇒ 铸 `open`(工作区外读默认放行);多租户 ⇒ 仍**不铸**(core 引擎默认 roots,server 不写死 * 上游默认)。谁定的这一档由 {@link ServiceConfigFlat.readFaceSource} 答 —— 值与来源**同一处写**。 */ readFace?: "open" | "roots"; /** S-167:上面那一档**是谁定的**(闭集四词,见 `read-face-posture.ts`)。不变量: * `readFace === undefined` ⟺ `readFaceSource === "engine-default"`。 * 🔴 承重面**不是**装饰:`config-center/read-face.ts` 的「env 恒赢」逐键判据读的就是它(修前读的是 * 「键在不在场」这个代理,posture 会铸键 ⇒ 代理当场失真,center 的 face 会被从没设过 env 的机器挡掉)。 */ readFaceSource: ReadFaceSource; /** [ref]:部署级 READ deny 追加表(additive-only,core 内建表永不可缩)。env=JSON 数组 * [{pattern, caseSensitive?}],boot 时用 core `compileReadDeny` 预验——坏形拒启。缺席=不铸。 */ readDenyPatterns?: { pattern: string; caseSensitive: boolean; }[]; /** [ref] 件 G5(core 5.33.0 [ref],clay 裁定件):**内建** deny 表的档位选择(闭集**五档**:credentials / * shell-history / browser / wallet / **agent-config**)。`READ_DENY_BUILTIN_TIERS`=逗号分隔;`none`=显式 * 空姿态(core 的 `[]`:一档内建都不要);坏档名 / 空串**启动期响亮拒**(校验用 core 自己的 * `resolveReadDenyBuiltins` —— 词表**单一属主在 core**,本仓零镜像:加档这天校验自动认,这段散文是 * 唯一要跟手的东西)。 * 🔴 **`agent-config`(core 7.14.0 [ref] C-e 新加的第五档)**:agent **自己的配置文件** —— 它们的 `env` / * server 段里明文放着 API key(壳的 `settings.json` env 块、项目 `.mcp.json` 的 server env、引擎宿主 * data-root 下的 `.env`)。此前对这类文件的一次结构化读会把密钥**逐字**落进工具结果、从而落进会话记录。 * 行名的是**文件**不是配置**目录**(`.sema`/`.claude` 底下还住着 agents/skills/hooks 与记忆库 —— 模型 * 当作日常材料读写的东西);同族工具的 token **存储**(`.credentials.json`/`.codex/auth.json` …)归 * `credentials` 档,不归本档 ⇒ 为排查 agent 配置而关掉本档的部署,凭证行照样拦着。 * 🔴 **缺席 = 不铸 ⇒ OFF**(core 7.22.0 / [ref] 翻面;7.21.x 及以前是「除 shell-history 外全开」)。 * 未配置的部署从内建表里**什么都不拒**,而且 shell 读边界的 deny 判官席 present-iff 编译集有条目 ⇒ * 递归读降级臂也随之不接线(`grep -r` 一族零卡)。要老姿态**必须显式写下** * `credentials,browser,wallet,agent-config`。本仓**不复制上游缺省**:这里只记「不铸」,解析归引擎。 * **仅 deployment 席**:TaskSpec 上没有同名键, * 任务/受治脚本无法据此放宽自己的部署(core 侧的设计,与本仓「部署级旋钮无条件施加」同向)。 */ readDenyBuiltinTiers?: readonly string[]; /** [ref] 件 G5:按**行名**(内建表的 canonical pattern)删内建条目,在档位选择**之后**生效。 * `READ_DENY_BUILTIN_EXCLUDE`=逗号分隔;不在表里的行名 / 空串**启动期响亮拒**。 * 🔴 不变量(判据帖 G5b):删内建**绝不**削弱任务自己显式声明的 deny —— 那是另一层(additions)。 */ readDenyBuiltinExclude?: readonly string[]; /** Structured log level. */ logLevel: "debug" | "info" | "warn" | "error"; /** Per-principal request cap per minute on task/run endpoints. 0 disables. */ rateLimitPerMin: number; /** Operator CEILING on per-task cost (USD) / tokens (1.37 budget gate). A request may ask for a * LOWER limit but is capped to this; 0/unset = no ceiling. The review gateway sets these to stop a * single task from burning the shared cloud key. Enforced by core (failed + errorCode budget.*). */ maxTaskCostUsd: number; maxTaskTokens: number; /** Per-principal CUMULATIVE cost ceiling (USD) within a rolling window — blocks a tenant's NEXT task * once their accumulated spend crosses it. 0/unset = no per-principal quota. */ maxPrincipalCostUsd: number; /** Rolling window (seconds) for the per-principal cost quota. Default 86400 (1 day). */ costQuotaWindowSec: number; /** Degradation to a cheaper model `to` (a catalog name) instead of hard-failing (core 1.39/1.40): * - **prospective** (1.40, `atCostFraction`): near-budget — at `atCostFraction × maxCostUsd` switch at * the turn boundary (needs a per-task cost ceiling). * - **reactive** (1.39, `reactive`): brain-level — on `rate_limit` / `breaker_open` from the primary, * serve `to` instead (a degrading brain wrapped OUTERMOST). `downgradeOn` picks the triggers. * Unset (`MODEL_DEGRADE_TO` absent) = disabled. NOTE: pick a `to` whose gateway/account is NOT the one * being rate-limited, else reactive degrade hits the same limit. */ degrade?: { to: string; atCostFraction: number; reactive: boolean; downgradeOn?: import("@sema-agent/core").DegradeReason[]; toSupportsImages: boolean; }; /** Quality-gate cascade ladder (core 1.45 `runCascade`, opt-in via `cascade:true`): catalog model * names cheapest→strongest. A task runs on the cheap rung; if it doesn't pass the gate (default = * `status==="completed"`, i.e. the cheap model failed) it escalates to the next. Empty/unset = * cascade unavailable. e.g. MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro. ⚠️ each rung is a COLD * re-run (write tools execute again) — use it for read-only / idempotent tasks. */ cascadeLadder: string[]; /** Optional OTLP/HTTP metrics export (1.37 observability). When the endpoint is set, the metric * registry is periodically pushed to an OTel collector; unset = disabled (zero overhead). */ otel?: { endpoint: string; intervalMs: number; serviceName: string; headers: Record; }; /** Expire pending approvals older than this (seconds). 0 disables the reaper. */ approvalTimeoutSec: number; /** Background reaper interval (seconds) for stale runs / expired approvals / rate windows. */ reapIntervalSec: number; /** TTL (seconds) for offloaded tool results in the durable store. Must comfortably exceed a run's * recoverable window (a resumed run may re-fetch an old ref after a cross-replica wake); a completed * run only needs the preview. Reaped by the background reaper. Default 86400 (24h). */ toolResultTtlSec: number; /** A `running` run whose heartbeat is older than this is treated as stalled/dead. The running * instance heartbeats every 30s, so this only needs to exceed a couple of heartbeat intervals * (default 120s) — it is NOT tied to event cadence (a quiet-but-alive subagent still heartbeats). */ runStaleSec: number; /** §4 staged-import 的**每会话租约**判定为陈旧的秒数(默认 600)。Phase A 遇到超过这个时长没有活动的租约就 * **接管**它 —— 这是「客户端在 Phase A 与 Phase B 之间崩掉」的自愈通道(此前那种会话到进程重启前永远 * 推不上云,而且没有任何客户端可调的释放动作)。租约按 entries 上传腿的活动刷新,所以大会话不会被误判。 * 设 0 = 关掉接管(退回旧行为:只能靠重启释放)。 */ syncImportLeaseStaleSec: number; /** Git host API base (e.g. https://git.example.com) for the read-only repo tools (code-review). */ gitApiBaseUrl?: string; /** Read-only Git host token (server-side only; never reaches the model). */ gitApiToken?: string; /** Git host API dialect for the repo tools (env GIT_API_KIND, closed set gitea|github; default gitea). * "github" = api.github.com or GHE `/api/v3` root — the Gitea client's `/api/v1` + `.diff` shapes * 404 there (P1-2: our own repos live on GitHub and were unreviewable). Unknown word = refuse boot. */ gitApiKind: GitApiKind; /** S3-TOB 设计 §1.3:memory 持久面方言(env MEMORY_ENGINE_BACKEND,缺省 file)。boot-only。 */ memoryEngineBackend: "file" | "pg" | "tidb"; /** CC `workflowSizeGuideline` (advisory, injected into the Workflow tool card via * core `RunnerDeps.workflowLimits.sizeGuideline`). env WORKFLOW_SIZE_GUIDELINE; unset = absent = * byte-compat (core treats missing/unrestricted identically). boot-only (per-task card snapshot). */ workflowSizeGuideline?: "small" | "medium" | "large" | "unrestricted"; /** Scenario used when a request omits `scenario`. NOTE: per-principal scenario governance does * NOT live on config — the ruling `{scenario, allowlist}` arrives per-request via center's caps view * (runtime-caps-resolver.scenarioRuling), single semantic source in center; raw assignments never hit a wire. */ defaultScenario: string; /** Directory of baked-in skill Markdown files, loaded once at startup. */ skillsDir: string; /** Central sema registry (sema-registry). When set, the service pulls the effective config * on boot (and refreshes) and applies it OVER the env defaults — env is the fallback. Logical config * only (model roster/roles/teams); secrets/gateways stay in this service's env. * `dryRun` (SEMA_REGISTRY_DRY_RUN=true; legacy CONFIG_CENTER_DRY_RUN now boot-rejects, no dual-read): pull + LOG what the registry would change vs the env-derived * config, but do NOT apply it — a safe-rollout verification step before flipping to live. * `worker` (SEMA_REGISTRY_WORKER=; legacy CONFIG_CENTER_WORKER now boot-rejects, no dual-read; injected per-worker by the reconciler): scopes the pull to * that worker's roster via `/effective?worker=`. Unset → the global roster (backward compatible). */ configCenter?: { baseUrl: string; token: string; dryRun: boolean; worker?: string; }; /** Dual-mode config SOURCE (DUAL-MODE-DESIGN §4): "remote" (sema-registry HTTP) | "local" (.env + config.d * via @sema-agent/settings-schema FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env. * Env: CONFIG_PROVIDER. The same resolver/EffectiveConfig either way (can't drift). */ configProvider?: string; /** Root dir holding `config.d/.json` for the local config source. Env: CONFIG_LOCAL_DIR. */ configLocalDir?: string; } /** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */ export type ServiceStoreConfig = Pick; /** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */ export type ServiceModelPlaneConfig = Pick; /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上 * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */ export type ServiceApprovalConfig = Pick; /** 组:memory(记忆面 + TOC 同步腿)。 */ export type ServiceMemoryConfig = Pick; /** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析 * 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 * [ref] 的三件部署治理声明(`compliancePosture`/`lockedConfigKeys`/`retentionPolicy`)同归本组: * 三者都是 operator 在配置面写下的**治理声明**(与 autonomy/commandPolicy 同族),不是存储旋钮 * ——`retentionPolicy` 尤其容易被误归 store 组:它的消费点是 core 的启动期能力校验与将来的调度面, * config 这一层它只是一句声明。 */ export type ServiceAuthConfig = Pick; /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */ export type ServiceOrchestrationConfig = Pick; /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */ export type ServiceLimitsHttpConfig = Pick; /** 组:observability(可观测)。 */ export type ServiceObservabilityConfig = Pick; /** 组:integrations(外部集成)。`mcpServers` / `a2aPeers` 由 sema-registry 适配器填(无 env 腿),归本组。 */ export type ServiceIntegrationsConfig = Pick; /** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写 * `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */ export interface ServiceConfigGroups { store: ServiceStoreConfig; modelPlane: ServiceModelPlaneConfig; approval: ServiceApprovalConfig; memory: ServiceMemoryConfig; auth: ServiceAuthConfig; orchestration: ServiceOrchestrationConfig; limitsHttp: ServiceLimitsHttpConfig; observability: ServiceObservabilityConfig; integrations: ServiceIntegrationsConfig; } /** Service configuration = 平铺面(唯一存储处,消费点原样) ∩ 九个组视图(additive,不可枚举)。 */ export interface ServiceConfig extends ServiceConfigFlat, ServiceConfigGroups { } //# sourceMappingURL=config-types.d.ts.map