@sema-agent/server 7.93.7 → 7.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/USAGE.md +52 -0
  2. package/dist/boot/resolve-spec.js +14 -1
  3. package/dist/boot/stage-09-leader.js +1 -1
  4. package/dist/boot/stores.js +6 -3
  5. package/dist/boot/workflow-orchestration.js +5 -1
  6. package/dist/config-catalog.js +5 -3
  7. package/dist/config-center/types.d.ts +1 -1
  8. package/dist/config-invariants.d.ts +2 -2
  9. package/dist/config-invariants.js +17 -0
  10. package/dist/config-types.d.ts +44 -3
  11. package/dist/config-types.js +1 -0
  12. package/dist/config.js +24 -4
  13. package/dist/hosted-posture.d.ts +176 -0
  14. package/dist/hosted-posture.js +76 -0
  15. package/dist/http/resume-ctx-read.d.ts +40 -0
  16. package/dist/http/resume-ctx-read.js +25 -0
  17. package/dist/http/resume-legs.js +62 -42
  18. package/dist/http/routes/capabilities.js +3 -0
  19. package/dist/http/routes/images.js +12 -0
  20. package/dist/http/routes/runs.js +18 -4
  21. package/dist/http/routes/workflows.js +3 -2
  22. package/dist/http/wire-types.d.ts +13 -4
  23. package/dist/observability/corrupt-read-seat.d.ts +56 -0
  24. package/dist/observability/corrupt-read-seat.js +16 -0
  25. package/dist/observability/fail-open.d.ts +4 -0
  26. package/dist/observability/fail-open.js +4 -0
  27. package/dist/orchestration/workflow-journal-entry.d.ts +24 -6
  28. package/dist/orchestration/workflow-journal-entry.js +17 -10
  29. package/dist/plugins/approval-ask-store-sql.js +2 -2
  30. package/dist/plugins/checkpoint-store-sql.d.ts +16 -3
  31. package/dist/plugins/checkpoint-store-sql.js +7 -9
  32. package/dist/plugins/image-bake-store-sql.js +85 -13
  33. package/dist/plugins/image-index-sql.js +75 -12
  34. package/dist/plugins/local-checkpoint-store.d.ts +23 -2
  35. package/dist/plugins/local-checkpoint-store.js +40 -33
  36. package/dist/plugins/local-session-store.d.ts +14 -0
  37. package/dist/plugins/local-session-store.js +12 -7
  38. package/dist/plugins/local-sidecar-quarantine.d.ts +10 -0
  39. package/dist/plugins/local-sidecar-quarantine.js +15 -0
  40. package/dist/plugins/local-task-attachment-store.d.ts +1 -1
  41. package/dist/plugins/local-task-attachment-store.js +5 -2
  42. package/dist/plugins/mailbox-store-sql.d.ts +6 -3
  43. package/dist/plugins/mailbox-store-sql.js +45 -43
  44. package/dist/plugins/memory-embedder-fingerprint.js +8 -12
  45. package/dist/plugins/memory-engine-pg.js +34 -16
  46. package/dist/plugins/memory-engine-tidb.js +19 -12
  47. package/dist/plugins/memory-engine-vector-util.d.ts +26 -0
  48. package/dist/plugins/memory-engine-vector-util.js +3 -0
  49. package/dist/plugins/memory-sync-store-pg.d.ts +6 -0
  50. package/dist/plugins/memory-sync-store-pg.js +14 -10
  51. package/dist/plugins/memory-sync-store-tidb.js +7 -11
  52. package/dist/plugins/pg-session-storage.js +26 -28
  53. package/dist/plugins/session-event-payload.d.ts +37 -0
  54. package/dist/plugins/session-event-payload.js +20 -0
  55. package/dist/plugins/sql-json-column.d.ts +124 -22
  56. package/dist/plugins/sql-json-column.js +22 -1
  57. package/dist/plugins/sql-row-helpers.d.ts +10 -16
  58. package/dist/plugins/sql-row-helpers.js +0 -29
  59. package/dist/plugins/store-backend.js +10 -12
  60. package/dist/plugins/store-contracts.d.ts +4 -6
  61. package/dist/plugins/store-contracts.js +0 -60
  62. package/dist/plugins/task-attachment-store.d.ts +21 -12
  63. package/dist/plugins/task-attachment-store.js +12 -0
  64. package/dist/plugins/tidb-session-storage.d.ts +2 -2
  65. package/dist/plugins/tidb-session-storage.js +12 -11
  66. package/dist/plugins/tidb-session-store.d.ts +2 -2
  67. package/dist/plugins/tidb-session-store.js +20 -22
  68. package/dist/plugins/usage-window-store-sql.d.ts +9 -1
  69. package/dist/plugins/usage-window-store-sql.js +23 -9
  70. package/dist/plugins/workflow-journal-store-sql.js +16 -4
  71. package/dist/run-local.js +4 -10
  72. package/dist/usage-analytics.d.ts +14 -1
  73. package/dist/usage-analytics.js +11 -17
  74. package/package.json +2 -2
package/USAGE.md CHANGED
@@ -747,6 +747,58 @@ READ_DENY_PATTERNS='[{"pattern":"**/secrets/**"},{"pattern":".ssh","caseSensitiv
747
747
  ⚠️ 本地 `config.d/` 车道今天**接不上**这一域(registry-core 的可移植域表里没有它),只有
748
748
  `CONFIG_PROVIDER=remote` 的部署走得通。
749
749
 
750
+ **可选 — 任务层「掀开写围栏」的运维天花板(`TASK_WRITE_FACE_ESCALATION`,7.95.0 起 / S-591)**
751
+
752
+ ```bash
753
+ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮拒;空串按未设
754
+ # 不设 = 本层不表态 ⇒ 按「托管形」posture 判(见下表)
755
+ ```
756
+
757
+ - **它管什么**:`permissionMode:"bypassPermissions"` 派生 `TaskSpec.writeFace:"open"` ——
758
+ 也就是说**一个调用方**可以掀掉写围栏(任务根之外也能写)。而引擎的解析序在普通部署上是
759
+ **任务席赢部署席**,引擎里唯一压得住它的两形(org 治理 / 只读 verifier 挂载)在本仓一个结构上不可达、
760
+ 一个要靠 `AUTONOMY=read-only|plan`(那是**整条 run 禁写**,不是等比例旋钮)。⇒ 天花板由 **server 在
761
+ 受理请求时**执行:不给的时候 **422 响亮拒**(`errorCode: request.host_posture_refused`),而**不是**
762
+ 被静默钳回 roots(静默钳是本仓明令要修掉的失败形:调用方明说「别拦我」,却撞上一道它掀不掉
763
+ 也看不见的墙)。
764
+ - 🔴 **它只抬天花板,不是「部署席」**:它**不会**给一条没声明过的 run 开根外写面。
765
+ `RunnerDeps.writeFace`(部署**缺省**席)本版**仍然缺席** = 引擎默认 `roots`,那是另一件事、另一根
766
+ 尚未铸出的旋钮(落点登记在 `src/boot/runner-deps.ts` 那一行上)。
767
+ - **三态,与 `READ_FACE` 同律(显式 env 恒赢 > posture > 默认)**:
768
+
769
+ | `TASK_WRITE_FACE_ESCALATION` | 托管形? | `permissionMode:"bypassPermissions"` 的结局 |
770
+ |---|---|---|
771
+ | `allow` | 任意 | **放行**(部署允许任务层开写面) |
772
+ | `refuse` | 任意 | **422 拒**(运维显式表态**无条件**恒赢,不挂任何客户端派生腿) |
773
+ | 未设 | 否(单用户 turnkey) | **放行** —— 与 7.94 及以前逐字节相同 |
774
+ | 未设 | **是** | **422 拒**,拒因点名 `TASK_WRITE_FACE_ESCALATION=allow` 与判成托管的证据 |
775
+
776
+ - 🔴 **`REMOTE_EXEC=device` 车道上本门不适用**(任何 `REQUIRE_PRINCIPAL` 值):那条车道的路径落在
777
+ **发起者自己的设备**上,归属由 `device_session` 绑定行 + 准入链的 owner 谓词验过 —— host 闸防的
778
+ 跨租户宿主穿越在那儿**结构性不存在**(成文裁定,与 `settings.env` / `cwd` 两键在该车道上的既有臂同源),
779
+ 也叠加「device 车道不额外加一道审批」这条既定纪律。⚠️ 运维的**显式** `refuse` 在 device 上照样恒赢(旋钮不挂车道派生腿)。
780
+ - **「托管形」怎么判**(判据单点 `src/hosted-posture.ts`,两类三条证据,任一成立即托管):
781
+ ① `REQUIRE_PRINCIPAL=true`(定义式);② `PRINCIPAL_JWT_PUBKEYS` / `_ISS` / `_AUD` 任一在场;
782
+ ③ `AUTH_BRIDGE_ISSUER` **显式**在场。②③ 的共同标准是**运维点名了一个外部身份签发方** ——
783
+ 一台机器一个可信主人的形不需要向任何人验签。
784
+ 🔴 **「受控于控制面」不算证据**:`SEMA_REGISTRY_*`(含 worker 名)与 auth-bridge 那条
785
+ `?? configCenter.baseUrl` 回退**都不读** —— fleet 管理轴 ≠ 租户轴(一台 `sema run-local` 从组织下发面
786
+ 取配置是正当用法)。其余被排除的候选(`OPERATOR_PRINCIPALS`、`SERVICE_AUTH_TOKENS` 条目数、
787
+ `DB_BACKEND`、`BIND_HOST`)逐条写在 `docs/DEPLOY-PREREQS.md`。
788
+ - **同族的 `approverPosture` 是「忽略 + 留声」,不是拒**:提交体的 `approverPosture` 同样是一句宿主姿态
789
+ 声明(引擎据此把 bypass 指令渲给模型 —— 那段文字指示模型改用 Bash 改文件而不是用文件工具),
790
+ 托管形上它被**忽略**并留一条 `task_approver_posture_ignored`(与 `settings.hooks` / `cwd` /
791
+ `settings.env` 三条同姿势)。⚠️ 刻意**不拒**:它零授权,而续跑腿逐字重放盘上的体、调用方删不掉它 ⇒
792
+ 拒会把一条 parked 任务永久砖死。
793
+ - **续跑腿**:一条在托管形机器上带着 `bypassPermissions` 的 parked 任务,赎回时被同一道门挡住,
794
+ 上层折成 `409 resume_blocked_by_policy`(`blockedBy: request.host_posture_refused`)。
795
+ 运维的解法是 `TASK_WRITE_FACE_ESCALATION=allow`。
796
+ - **能力面披露位**:`GET /v1/capabilities` 的 `taskWriteFaceOpen`(`false` ⇒ 发那个键会拿 422)——
797
+ 壳不必靠踩一次 422 才学会。
798
+ - **`AUTONOMY=read-only|plan` 一行未动**。⚠️ 两者叠加时**本门先说话**:托管形 + `AUTONOMY=read-only`
799
+ + `bypassPermissions` 的请求现在是 422,而 7.94 及以前它会跑起来(写被 autonomy 禁掉)。
800
+ 要保留旧结局就 `TASK_WRITE_FACE_ESCALATION=allow`(写面仍被 autonomy 压回 roots)。
801
+
750
802
  - **MCP 撤销腿(7.39 起)**:MCP 服务器 mount 仍是 restart-to-apply(新增/改址要
751
803
  restart 才生效),但**从 enabled 集删除一个服务器即时生效**——refresh 拍收货后,被删服务器的
752
804
  MCP 调用当场按 coded 拒绝 `mcp.server_revoked` 结算(known-not-executed,引擎每次派发前探询
@@ -13,6 +13,7 @@ import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApp
13
13
  import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane, scratchpadCoveringTaskRoot } from "../env-facts.js";
14
14
  import { FleetEventBus } from "../fleet/fleet-bus.js";
15
15
  import { composeHooks, createTaskHooks } from "../hooks/hook-runner.js";
16
+ import { HOST_POSTURE_REFUSED_CODE, taskApproverPostureHonored, taskWriteFaceRefusal } from "../hosted-posture.js";
16
17
  import { headerStr } from "../http/principal-gate.js";
17
18
  import { explicitOperatorOk } from "../http/server.js";
18
19
  import { isModelAllowlisted, matchCatalogModel, modelSupportsImages, resolveTaskModel, terminalCatalogName } from "../model-select.js";
@@ -68,6 +69,11 @@ export function createResolveSpec(ctx) {
68
69
  assertRequestA2aUnlocked(body.a2aPeers, lockedKeys);
69
70
  const gated = await gateScenarioAndAppend(body, auth, opts);
70
71
  const effMode = effectivePermissionMode(body, gated.parsedSettings.settings);
72
+ if (effMode !== undefined && writeFaceForMode(effMode) === "open") {
73
+ const why = taskWriteFaceRefusal(config);
74
+ if (why !== undefined)
75
+ throw new HttpError(422, why, { code: HOST_POSTURE_REFUSED_CODE });
76
+ }
71
77
  noteSessionShellGate?.(auth?.sessionId, effMode !== undefined && shellGateForMode(effMode) === "off");
72
78
  const lane = bindSettingsCwdEnvAndModel(body, auth, gated, effMode, opts);
73
79
  const anchors = await resolveHistoryAnchors(body, auth);
@@ -296,7 +302,14 @@ export function createResolveSpec(ctx) {
296
302
  ...(effMode ? { shellGate: shellGateForMode(effMode) } : {}),
297
303
  ...((f) => (f !== undefined ? { writeFace: f } : {}))(effMode !== undefined ? writeFaceForMode(effMode) : undefined),
298
304
  ...((f) => (f !== undefined ? { autoModeRequested: f } : {}))(effMode !== undefined ? autoModeRequestedForMode(effMode) : undefined),
299
- ...(body.approverPosture !== undefined ? { approverPosture: body.approverPosture } : {}),
305
+ ...(() => {
306
+ if (body.approverPosture === undefined)
307
+ return {};
308
+ if (taskApproverPostureHonored(config))
309
+ return { approverPosture: body.approverPosture };
310
+ logger.warn("task_approver_posture_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
311
+ return {};
312
+ })(),
300
313
  selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
301
314
  forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
302
315
  retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
@@ -16,7 +16,7 @@ export function runStage09(ctx) {
16
16
  });
17
17
  const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
18
18
  const drainState = { draining: false };
19
- const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
19
+ const authBridgeIssuer = config.authBridgeIssuer ?? config.configCenter?.baseUrl;
20
20
  const registryJwtVerifier = authBridgeIssuer
21
21
  ? createRegistryJwtVerifier({
22
22
  issuer: authBridgeIssuer,
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore, FileUsageWindowStore, InMemoryUsageWindowStore, mailboxCrossProcessMountVerdict } from "@sema-agent/core";
3
+ import { createCorruptReadSeat } from "../observability/corrupt-read-seat.js";
3
4
  import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
4
5
  import { memoryEmbedderFor } from "../plugins/memory-embedder.js";
5
6
  import { ensurePgMemoryEmbedderMetaSchema, reconcileEmbedderFingerprint } from "../plugins/memory-embedder-fingerprint.js";
@@ -231,7 +232,7 @@ export async function openStores(ctx) {
231
232
  }
232
233
  else if (backend?.kind === "local") {
233
234
  rosterStore = new FileRosterStore(join(config.localDataRoot ?? localRoot, "roster.json"), {
234
- onCorruptRead: (info) => logger.warn("roster_corrupt_read", { path: info.path, reason: info.reason }),
235
+ onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "roster_corrupt_read"),
235
236
  });
236
237
  }
237
238
  if (rosterStore)
@@ -251,7 +252,9 @@ export async function openStores(ctx) {
251
252
  backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
252
253
  }
253
254
  else if (backend?.kind === "local") {
254
- backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
255
+ backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot, {
256
+ onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "background_agent_corrupt_read"),
257
+ });
255
258
  }
256
259
  if (backgroundAgentStore)
257
260
  logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
@@ -305,7 +308,7 @@ export async function openStores(ctx) {
305
308
  }
306
309
  else if (backend?.kind === "local") {
307
310
  mailboxStore = new FileMailboxStore(config.localDataRoot ?? localRoot, {
308
- onCorruptRead: (info) => logger.warn("mailbox_corrupt_read", { path: info.path, reason: info.reason }),
311
+ onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "mailbox_corrupt_read"),
309
312
  });
310
313
  mailboxRecipientLifecycle = createDropOnlyRecipientLifecycle(mailboxStore);
311
314
  }
@@ -2,6 +2,7 @@ import { FileWorkflowRunStore, InMemoryWorkflowRunStore } from "@sema-agent/core
2
2
  import { join } from "node:path";
3
3
  import { FleetEventBus } from "../fleet/fleet-bus.js";
4
4
  import { LocalWorkflowAgentSessionIndex } from "../orchestration/workflow-agent-session-index.js";
5
+ import { createCorruptReadSeat } from "../observability/corrupt-read-seat.js";
5
6
  import { FileWorkflowCompletionInbox, InMemoryWorkflowCompletionInbox, resolveCompletionRoute } from "../orchestration/workflow-completion-inbox.js";
6
7
  import { FileWorkflowNotifyJournalStore, JournalingWorkflowRunStore, WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
7
8
  import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
@@ -13,7 +14,10 @@ export function createWorkflowOrchestration(ctx) {
13
14
  const baseWorkflowRunStore = config.selfOrchestrationEnabled
14
15
  ? config.workflowRunStoreBackend === "memory"
15
16
  ? new InMemoryWorkflowRunStore()
16
- : (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
17
+ : (sqlWorkflowRunStore ??
18
+ new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows"), {
19
+ onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "workflow_run_corrupt_read"),
20
+ }))
17
21
  : undefined;
18
22
  const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
19
23
  ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
@@ -1,6 +1,6 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
2
  import { THINKING_FORMATS } from "./model-compat.js";
3
- import { RESILIENCE_DEFAULTS } from "./config-types.js";
3
+ import { RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
4
4
  import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-search.js";
5
5
  import { AUTONOMY_MODES } from "./runtime-governance.js";
6
6
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
@@ -74,7 +74,7 @@ export const CONFIG_CATALOG = [
74
74
  r("ATTACHMENT_ORPHAN_GRACE_MS", "limitsHttp", "number", "附件孤儿对象 GC 宽限(ms;0=关本腿,坏值回默认+warn)", { staticDefault: "3600000", danger: DATA, center: cLimits("attachmentOrphanGraceMs") }),
75
75
  r("ATTACHMENT_UNBOUND_TTL_MS", "limitsHttp", "number", "未绑定附件的 TTL(ms)", { staticDefault: String(24 * 3600 * 1000), danger: DATA, configKey: "attachmentUnboundTtlMs", center: cLimits("attachmentUnboundTtlMs") }),
76
76
  r("AUTH_BRIDGE_AUDIENCE", "auth", "string", "registry SSO-JWT 验签期望 aud", { danger: SEC }),
77
- r("AUTH_BRIDGE_ISSUER", "auth", "url", "registry SSO-JWT issuer(auth bridge 的开关之一)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 用 configCenter.baseUrl(配了 center 即默认开桥)" }),
77
+ r("AUTH_BRIDGE_ISSUER", "auth", "url", "registry SSO-JWT issuer(auth bridge 的开关之一;S-591 起也是托管形判据的一条证据 —— 只认显式值,不认 center 回退)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 桥的装配处回退到 configCenter.baseUrl(配了 center 即默认开桥);但**托管判据不读那条回退**(fleet 管理轴 ≠ 租户轴)", configKey: "authBridgeIssuer" }),
78
78
  r("AUTH_BRIDGE_JWKS_URL", "auth", "url", "registry SSO-JWT 的 JWKS 地址(缺省从 issuer 派生)", { danger: SEC }),
79
79
  r("AUTONOMY", "auth", "enum", "运维 autonomy 模式,tighten-only 叠到每个 TaskSpec(read-only/plan ⇒ 只读手,ask ⇒ 恒 shellGate);center 腿热且 non-sticky", { danger: SEC, enumValues: AUTONOMY_MODES, configKey: "autonomy", center: cGov("autonomy") }),
80
80
  r("AUTO_MODE_CLASSIFIER_FALLBACK", "approval", "enum", "auto 分类器席答不出裁决时的梯子(main=主模型代判 / none=只留分类器那一级,整条拒)", { enumValues: ["main", "none"], danger: SEC, derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随引擎缺省 main(core 7.18.0 起;空串按未设,与其余词表旋钮同判);none = 7.77.0 及以前的行为逐字;词表外的值启动期拒启。权限轴放宽,升级须知见 DEPLOY-PREREQS", configKey: "autoModeClassifierFallback" }),
@@ -353,8 +353,9 @@ export const CONFIG_CATALOG = [
353
353
  r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
354
354
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
355
355
  r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
356
- r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: REMOTE_EXEC_PROVIDER_WORDS, danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
356
+ r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: REMOTE_EXEC_PROVIDER_WORDS, danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub(手带整族不挂;残余名册看活体 wiring_manifest 里 mountedBy=hands 的条目数,不按工具名点。想让这一形拒启见 REQUIRE_EXECUTION_LANE)", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
357
357
  r("REMOTE_EXEC_HOME_DIR", "orchestration", "path", "执行环境的 home 目录(`~/` 形权限规则的基;core ExecutionEnv.homeDir 座)", { danger: SEC, derivedDefaultNote: "缺席:host/in-process ⇒ os.homedir();e2b ⇒ /home/user;k8s/local-docker/ssh/adb/device ⇒ 无(`~/` 规则 422 响亮拒)", configKey: "remoteExecHomeDir", resolve: (c) => c.remoteExecHomeDir ?? null }),
358
+ r("REQUIRE_EXECUTION_LANE", "orchestration", "boolean", "开了之后:REMOTE_EXEC 未点名 ∧ CONFIG_PROVIDER≠local ⇒ **拒启**并点名该设什么。默认关 = 照旧启动,但执行环境是进程内 stub、手带整族不挂,任务仍回 status=completed(活体读数:mountedBy=hands 条目 10 → 0,名册 38 → 19)—— 与「模型不肯用工具」在用户面同形。残余名册判据 = 活体 wiring_manifest 里 mountedBy=hands 的条目数,不按工具名点。托管/云部署推荐开(docs/DEPLOY-PREREQS.md)", { staticDefault: "false", danger: SEC, configKey: "requireExecutionLane" }),
358
359
  r("REQUIRE_PRINCIPAL", "auth", "boolean", "多租户开关(posture 判别位:true ⇒ CC 能力族默认收紧)", { staticDefault: "false", danger: SEC, configKey: "requirePrincipal" }),
359
360
  r("RESOURCE_SUSPEND", "approval", "boolean", "durable 路径 opt-in core resourceSuspend(预算/preempt ⇒ 可恢复挂起)", { staticDefault: "false", configKey: "resourceSuspend" }),
360
361
  r("RESOURCE_SUSPEND_MAX_SLICES", "approval", "number", "resume 链片数上限(0=不设;零进展循环兜底)", { staticDefault: "0", configKey: "resourceSuspendMaxSlices" }),
@@ -440,6 +441,7 @@ export const CONFIG_CATALOG = [
440
441
  r("TASK_MAX_TURNS_MAX", "limitsHttp", "number", "调用方自报 limits.maxTurns 的运营方封顶(同族三旋钮;只钳显式请求值,不是全队默认限额)", { danger: BILL, derivedDefaultNote: "缺席 ⇒ 不封顶" }),
441
442
  r("TASK_TIMEOUT_MAX_SEC", "limitsHttp", "number", "调用方自报 limits.timeoutSec 的运营方封顶(同族三旋钮)", { danger: BILL, derivedDefaultNote: "缺席 ⇒ 不封顶" }),
442
443
  r("TASK_TIMEOUT_SEC", "limitsHttp", "number", "任务墙钟超时(秒);0/缺席=按 tenancy(单用户无墙;多租户 2400s、大任务 3600s);正值**只抬不降**(Math.max 基值);负值拒启", { staticDefault: "0", derivedDefaultNote: "0/缺席 ⇒ 单用户无墙、多租户 2400/3600s;要低于基值的配速请用 body.limits.maxWalltimeMs" }),
444
+ r("TASK_WRITE_FACE_ESCALATION", "orchestration", "enum", "任务层「掀开写围栏」声明的运维天花板档位(S-591;只抬天花板,不是部署席)", { enumValues: [...TASK_WRITE_FACE_ESCALATIONS], danger: SEC, derivedDefaultNote: "三态:allow ⇒ 任务层 bypassPermissions 的开放写面成立;refuse ⇒ 无条件拒;**缺席 = 本层不表态** ⇒ 按托管 posture 判(托管拒 / 单用户 turnkey 放行)。判据单点 src/hosted-posture.ts;device 车道不适用(成文裁定:那条穿越结构性不存在)", configKey: "taskWriteFaceEscalation" }),
443
445
  r("TOOL_APPROVAL_ENABLED", "approval", "boolean", "活体工具审批 HITL(onAsk 装配;关 ⇒ core resolveAsk 走 headless 缺省)", { danger: SEC, derivedDefaultNote: "posture 三态:显式词恒赢,缺席 = 单用户(REQUIRE_PRINCIPAL≠true)⇒ on / 多租户 ⇒ off", configKey: "toolApprovalEnabled" }),
444
446
  r("TOOL_DEFER_LONGTAIL", "orchestration", "boolean", "长尾一方工具 defer 呈现(实验)", { staticDefault: "false", configKey: "toolDeferLongtail" }),
445
447
  r("TOOL_RESULT_TTL_SEC", "limitsHttp", "number", "工具结果店 TTL(秒)", { staticDefault: "86400", danger: DATA, configKey: "toolResultTtlSec", center: cLimits("toolResultTtlSec") }),
@@ -369,7 +369,7 @@ export interface EffectiveConfig {
369
369
  * ruling riding the caps view. `required=false` / ruling absent = no enforcement (legacy). `allowedLanes` is
370
370
  * an OPEN value domain (adding one is never BREAKING; consumers tolerate unknown names). Known lane names:
371
371
  * `host`/`e2b`/`k8s`/`ssh`/`adb`/`local-docker`/`device` (the REMOTE_EXEC providers) **plus `in-process`**
372
- * — the REMOTE_EXEC-unset fleet posture (OA/review-style workers; named distinctly from the explicit
372
+ * — the REMOTE_EXEC-unset fleet posture (S-601: previously "OA/review-style workers", a scenario family retired in 5.0.0; named distinctly from the explicit
373
373
  * `host` lane on purpose). ⚠️ Admins writing `required=true` policies must include `in-process` when such
374
374
  * workers should stay admittable — center UI/doc 词表待补齐。
375
375
  *
@@ -28,9 +28,9 @@ import type { ServiceConfigFlat } from "./config-types.js";
28
28
  */
29
29
  export declare const LIVENESS_HEARTBEAT_MS = 30000;
30
30
  /** 闭集:新增一条不变量必须在这里加词(拼错 = 编译红,而不是运行期悄悄不判)。 */
31
- export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo";
31
+ export type CrossInvariantId = "run-stale-heartbeat" | "bind-posture" | "bake-runner-credential" | "direct-door-anchors" | "operator-principals-required" | "bake-operator-principals" | "sensitive-write-patterns" | "leader-host-lane" | "leader-durable-store" | "device-durable-store" | "device-durable-approval" | "model-provider-word" | "gateway-url-userinfo" | "require-execution-lane" | "hosted-requires-principal";
32
32
  /** 表读的键集(取自平铺配置型 ⇒ 键名/类型永不与真配置漂)。 */
33
- export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic">>;
33
+ export type CrossInvariantView = Readonly<Pick<ServiceConfigFlat, "runStaleSec" | "authToken" | "authTokens" | "allowUnauthedWrites" | "imageBakes" | "directApprovalDoor" | "durableApproval" | "approvalHmacKeys" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "requirePrincipal" | "operatorPrincipals" | "sensitiveWritePatterns" | "leaderEnabled" | "remoteExec" | "requireExecutionLane" | "dbBackend" | "tidb" | "pg" | "envModel" | "gatewayBaseUrl" | "gatewayFallbackUrls" | "anthropic" | "authBridgeIssuer">>;
34
34
  export interface CrossInvariantInput {
35
35
  /** 现役配置(boot 期 = 候选自身)。 */
36
36
  readonly live: CrossInvariantView;
@@ -1,4 +1,5 @@
1
1
  import { assertGuardPatternsUsable } from "./deployment-governance.js";
2
+ import { hostedPrincipalRefusal } from "./hosted-posture.js";
2
3
  import { modelProviderRefusal } from "./model-provider.js";
3
4
  import { modelRouteUrlRefusal } from "./model-route-url.js";
4
5
  export const LIVENESS_HEARTBEAT_MS = 30_000;
@@ -80,6 +81,13 @@ export const CONFIG_CROSS_INVARIANTS = [
80
81
  ? "IMAGE_BAKES_ENABLED with an EMPTY OPERATOR_PRINCIPALS is a world-writable bake door (the runner runs build.sh on the privileged build host)"
81
82
  : undefined,
82
83
  },
84
+ {
85
+ id: "hosted-requires-principal",
86
+ keys: ["requirePrincipal", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud", "authBridgeIssuer"],
87
+ restartReason: "hosted-posture",
88
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:hosted-requires-principal" },
89
+ check: ({ next }) => hostedPrincipalRefusal(next),
90
+ },
83
91
  {
84
92
  id: "sensitive-write-patterns",
85
93
  keys: ["sensitiveWritePatterns"],
@@ -95,6 +103,15 @@ export const CONFIG_CROSS_INVARIANTS = [
95
103
  }
96
104
  },
97
105
  },
106
+ {
107
+ id: "require-execution-lane",
108
+ keys: ["requireExecutionLane", "remoteExec"],
109
+ restartReason: "execution-lane",
110
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:require-execution-lane" },
111
+ check: ({ next }) => next.requireExecutionLane && next.remoteExec === undefined
112
+ ? "REQUIRE_EXECUTION_LANE=true with no execution lane named (REMOTE_EXEC unset/empty and CONFIG_PROVIDER not \"local\") — the deployment asked to refuse this state"
113
+ : undefined,
114
+ },
98
115
  {
99
116
  id: "leader-host-lane",
100
117
  keys: ["leaderEnabled", "remoteExec"],
@@ -37,6 +37,11 @@ export interface MemoryEmbedderConfig {
37
37
  * 的 `mode` 列共读它);两词之外的值在启动期响亮拒 —— 一个拼错的档位静默落回 `audit-only` 会让运维
38
38
  * 以为在删、其实没删,而拼成别的方向就是反过来那件更坏的事。
39
39
  */
40
+ /** S-591:任务层「掀开写围栏」的天花板档位闭集(**词表属主在此** —— config 型面与
41
+ * `hosted-posture.ts` 的判据读同一份;抄第二份的那天两边会在加词时分家)。 */
42
+ export declare const TASK_WRITE_FACE_ESCALATIONS: readonly ["allow", "refuse"];
43
+ /** 天花板档位(闭集两词;缺席 = 本层不表态)。 */
44
+ export type TaskWriteFaceEscalation = (typeof TASK_WRITE_FACE_ESCALATIONS)[number];
40
45
  export declare const RETENTION_MODES: readonly ["audit-only", "enforce"];
41
46
  export type RetentionMode = (typeof RETENTION_MODES)[number];
42
47
  /** [ref] §3/§4 —— sweep lane 的节律与灰度档(语义与三问见 {@link ServiceConfigFlat.retentionSweep})。 */
@@ -696,7 +701,9 @@ export interface ServiceConfigFlat {
696
701
  * 默认 10s。env `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS`(numEnvBounded)。 */
697
702
  memoryOrgUnavailableBackoffMs: number;
698
703
  /** [ref] v1b + [ref]: opt-in remote execution backend (PEER adapters, chosen by REMOTE_EXEC). Unset
699
- * (OA/review deployments) → in-process stub env, zero behavior change. Secrets (apiKey/privateKey) ONLY from env.
704
+ * → in-process stub env (S-601: this used to read "OA/review deployments", a scenario family retired wholesale
705
+ * in 5.0.0 — see RETIRED_ENVS; the real discriminant is CONFIG_PROVIDER, and REQUIRE_EXECUTION_LANE turns this
706
+ * arm into a boot refusal). Secrets (apiKey/privateKey) ONLY from env.
700
707
  * - `e2b` → per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
701
708
  * - `k8s` → per-task Kata pod on our k3s (isolated, NOT suspendable — no managed memory
702
709
  * snapshot). Self-hosted sandbox lane; auth = explicit K8S_* env or in-cluster service account.
@@ -714,6 +721,25 @@ export interface ServiceConfigFlat {
714
721
  * 非绝对路径 ⇒ **拒启**([ref] A 档;core 侧同判为 `config.execution_env_home_dir_invalid`)。
715
722
  */
716
723
  remoteExecHomeDir?: string;
724
+ /**
725
+ * S-598 —— `REQUIRE_EXECUTION_LANE`:**「没点名车道」要不要拒启**。默认 **false**(缺省行为逐字不变)。
726
+ *
727
+ * `REMOTE_EXEC` 未设/空串 **∧** `CONFIG_PROVIDER ≠ local` ⇒ {@link remoteExec} 缺席 ⇒ `boot/execution-env.ts`
728
+ * 一条工厂臂都不走 ⇒ core 的 `handsEnabled` 为 false ⇒ **整条手带一件不挂**,而任务照跑、回执
729
+ * `status=completed`。[ref] 的 fail-closed 门只咬「点了名却配不齐/认不出」,**「根本没点名」是文档化的
730
+ * 部署姿态**,所以那道门放行。
731
+ *
732
+ * 本键把那条姿态交回给运维表态:`true` ⇒ 装配尾**拒启**并点名该设什么(`config.ts` 跨域不变量区)。
733
+ * 🔴 **为什么默认 off**:今天正在无手运行而自己不知道的部署(根 `docker-compose.yml` / review compose /
734
+ * `--sandbox=kata` 档 / web-admin 编排器起的 worker)会**升级即挂**,而其中一部分只跑只读/分析任务、
735
+ * 今天「能用」。缺省翻面留到下一个 major,翻之前生态有一版时间适配。托管清单推荐开
736
+ * (`docs/DEPLOY-PREREQS.md`)。
737
+ * 运维读面(2026-09-22 主会话验收订正:车原写「`GET /v1/diagnostics/wiring` 的 `executionLane.requireExecutionLane`」,
738
+ * 而该端点今天**没有** `executionLane` 段、`src/` 零投影 —— 一句没兑现的承诺,按 [ref] 不信 JSDoc 只信实现行的纪律改写):
739
+ * boot 期 `exec_lane_selected` 一行(`config.ts` 装配尾,带 `lane` / `source`)+ `GET /v1/config/catalog` 的目录行。
740
+ * 本键的**有效值今天没有运行期读面**;要出就随 S-602 的执行座三态在一处铸,不在这里单独长一位。
741
+ */
742
+ requireExecutionLane: boolean;
717
743
  remoteExec?: {
718
744
  provider: "e2b";
719
745
  apiKey: string;
@@ -1195,6 +1221,11 @@ export interface ServiceConfigFlat {
1195
1221
  /** Expected `iss` / `aud` of a direct client's principal JWT. `PRINCIPAL_JWT_ISS` / `PRINCIPAL_JWT_AUD`. */
1196
1222
  principalJwtIss?: string;
1197
1223
  principalJwtAud?: string;
1224
+ /** S-591:registry SSO-JWT 的**显式** issuer(`AUTH_BRIDGE_ISSUER`)。
1225
+ * 🔴 **只是显式 env,不含 boot 期那条 `?? configCenter.baseUrl` 回退**(回退仍在
1226
+ * `boot/stage-09-leader.ts` 的桥装配处)—— 本字段是「运维**点名了**一个外部签发方」这个事实的载体,
1227
+ * 而那条回退的事实来源是「配了 center」= fleet 管理轴,不是租户轴(理由全文见 `hosted-posture.ts`)。 */
1228
+ authBridgeIssuer?: string;
1198
1229
  /** Max accepted principal-JWT lifetime (exp − now), seconds — caps the replay window past the short-lived mandate.
1199
1230
  * 0 = no cap. `PRINCIPAL_JWT_MAX_TTL_SEC` (default 120). */
1200
1231
  principalJwtMaxTtlSec: number;
@@ -1736,6 +1767,16 @@ export interface ServiceConfigFlat {
1736
1767
  * 🔴 承重面**不是**装饰:`config-center/read-face.ts` 的「env 恒赢」逐键判据读的就是它(修前读的是
1737
1768
  * 「键在不在场」这个代理,posture 会铸键 ⇒ 代理当场失真,center 的 face 会被从没设过 env 的机器挡掉)。 */
1738
1769
  readFaceSource: ReadFaceSource;
1770
+ /** S-591(7.95.0,车LT):任务层**掀开写围栏**这条声明的**运维天花板档位**(`TASK_WRITE_FACE_ESCALATION`)。
1771
+ * `"allow"` = 这台部署允许任务层把写容纳面开到任务根之外;`"refuse"` = 无条件拒;
1772
+ * **缺席 = 不表态** ⇒ 交给托管 posture 判(托管拒 / 单用户 turnkey 照旧放行)。判据单点在
1773
+ * `src/hosted-posture.ts` 的 `taskWriteFaceRefusal`。
1774
+ *
1775
+ * 🔴 **它只抬天花板,不是「部署席」**(clay 裁定② 2026-09-22):`RunnerDeps.writeFace` 那个**部署缺省席**
1776
+ * 是另一件事 —— 把这根旋钮同时递过去,会让一条 `permissionMode:"default"` 的 run **也**拿到根外写面,
1777
+ * 而它从没声明过(一根旋钮两个语义)。部署席保持**缺席**(= 引擎默认 `roots`),它的接线仍是登记在
1778
+ * `boot/runner-deps.ts` 那一行上的未闭环项。 */
1779
+ taskWriteFaceEscalation?: TaskWriteFaceEscalation;
1739
1780
  /** [ref]:部署级 READ deny 追加表(additive-only,core 内建表永不可缩)。env=JSON 数组
1740
1781
  * [{pattern, caseSensitive?}],boot 时用 core `compileReadDeny` 预验——坏形拒启。缺席=不铸。 */
1741
1782
  readDenyPatterns?: {
@@ -1878,9 +1919,9 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1878
1919
  * 三者都是 operator 在配置面写下的**治理声明**(与 autonomy/commandPolicy 同族),不是存储旋钮
1879
1920
  * ——`retentionPolicy` 尤其容易被误归 store 组:它的消费点是 core 的启动期能力校验与将来的调度面,
1880
1921
  * config 这一层它只是一句声明。 */
1881
- export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
1922
+ export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
1882
1923
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
1883
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readFaceSource" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1924
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "remoteExecHomeDir" | "requireExecutionLane" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1884
1925
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1885
1926
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "engineOrphanLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
1886
1927
  /** 组:observability(可观测)。 */
@@ -1,3 +1,4 @@
1
+ export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
1
2
  export const RETENTION_MODES = ["audit-only", "enforce"];
2
3
  export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
3
4
  export const RESILIENCE_DEFAULTS = {
package/dist/config.js CHANGED
@@ -10,13 +10,15 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS } from "./config-types.js";
13
+ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
14
14
  import { buildWriteProtectionPosture } from "./write-protection.js";
15
15
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
16
  import { readFaceForPosture } from "./read-face-posture.js";
17
17
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
18
  import {} from "./posture-source.js";
19
+ import { hostedPrincipalRefusal } from "./hosted-posture.js";
19
20
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
+ import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
20
22
  import { AUTONOMY_MODES } from "./runtime-governance.js";
21
23
  import { modelProviderRefusal } from "./model-provider.js";
22
24
  import { modelRouteUrlRefusal } from "./model-route-url.js";
@@ -1590,6 +1592,7 @@ function parseAuthDomain(ctx) {
1590
1592
  principalJwtPubkeys,
1591
1593
  principalJwtIss,
1592
1594
  principalJwtAud,
1595
+ authBridgeIssuer: process.env.AUTH_BRIDGE_ISSUER || undefined,
1593
1596
  principalJwtMaxTtlSec,
1594
1597
  };
1595
1598
  }
@@ -1637,9 +1640,11 @@ function parseOrchestrationDomain(ctx) {
1637
1640
  }
1638
1641
  }
1639
1642
  return {
1643
+ requireExecutionLane: boolEnv("REQUIRE_EXECUTION_LANE", false),
1640
1644
  ...(process.env.READ_FACE !== undefined
1641
1645
  ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]), readFaceSource: "env" }
1642
1646
  : readFaceForPosture(singleUserTurnkey)),
1647
+ ...optionalEnumKey("taskWriteFaceEscalation", "TASK_WRITE_FACE_ESCALATION", process.env.TASK_WRITE_FACE_ESCALATION, TASK_WRITE_FACE_ESCALATIONS),
1643
1648
  ...(process.env.READ_DENY_PATTERNS !== undefined ? { readDenyPatterns: parseReadDenyPatternsEnv(process.env.READ_DENY_PATTERNS) } : {}),
1644
1649
  ...(() => {
1645
1650
  const tiers = process.env.READ_DENY_BUILTIN_TIERS !== undefined ? parseNameListEnv("READ_DENY_BUILTIN_TIERS", process.env.READ_DENY_BUILTIN_TIERS, { emptySentinel: "none" }) : undefined;
@@ -2054,17 +2059,17 @@ const AUTH_GROUP_KEYS = [
2054
2059
  "authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
2055
2060
  "commandPolicy", "compliancePosture", "lockedConfigKeys", "retentionPolicy", "retentionSweep",
2056
2061
  "operatorPrincipals", "configCatalogEnabled", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud",
2057
- "principalJwtMaxTtlSec", "bindHost", "bindHostSource",
2062
+ "principalJwtMaxTtlSec", "authBridgeIssuer", "bindHost", "bindHostSource",
2058
2063
  ];
2059
2064
  const ORCHESTRATION_GROUP_KEYS = [
2060
- "remoteExec", "remoteExecHomeDir", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
2065
+ "remoteExec", "remoteExecHomeDir", "requireExecutionLane", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
2061
2066
  "selfOrchestrationModels", "selfOrchestrationWorkerIsolation", "forkEnabled", "experimentalObserverAgents",
2062
2067
  "schedulerEnabled", "schedulerSessionWakeup", "schedulerSessionLifetime", "schedulerStorePath", "planModeEnabled", "hooksTimeoutMs", "workflowRunStoreBackend",
2063
2068
  "workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
2064
2069
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
2065
2070
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2066
2071
  "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
2067
- "imageBakes", "readFace", "readFaceSource", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2072
+ "imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2068
2073
  ];
2069
2074
  const LIMITS_HTTP_GROUP_KEYS = [
2070
2075
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -2164,6 +2169,10 @@ export function loadConfig() {
2164
2169
  if (dgDurable && requirePrincipal && dgOperators.length === 0) {
2165
2170
  throw new Error("DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)");
2166
2171
  }
2172
+ const hostedRefusal = hostedPrincipalRefusal({ requirePrincipal, principalJwtPubkeys, principalJwtIss, principalJwtAud, authBridgeIssuer: auth.authBridgeIssuer });
2173
+ if (hostedRefusal !== undefined) {
2174
+ throw new Error(hostedRefusal);
2175
+ }
2167
2176
  if (imageBakesEnabled && dgOperators.length === 0) {
2168
2177
  throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
2169
2178
  }
@@ -2204,6 +2213,17 @@ export function loadConfig() {
2204
2213
  "Set DURABLE_APPROVAL=true, or unset REMOTE_EXEC (device-executor-lane-v2 §4.7 boot invariant)");
2205
2214
  }
2206
2215
  }
2216
+ if (orchestration.requireExecutionLane && orchestration.remoteExec === undefined) {
2217
+ throw new Error(`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
2218
+ `With no lane the engine falls back to its in-process stub environment, where at least the Bash tool is ` +
2219
+ `not mounted, yet every task still returns status=completed — indistinguishable from "the model chose ` +
2220
+ `not to use its tools". (What the roster actually keeps is a per-run fact: read the wiring_manifest ` +
2221
+ `event and count entries whose mountedBy is "hands".) ` +
2222
+ `Cause: REMOTE_EXEC is unset/empty AND CONFIG_PROVIDER is not "local". ` +
2223
+ `Fix: set REMOTE_EXEC to one of ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")} (and that lane's required env), ` +
2224
+ `or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
2225
+ `or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
2226
+ }
2207
2227
  EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2208
2228
  return attachConfigGroups({
2209
2229
  ...store,