@sema-agent/server 7.94.0 → 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.
- package/USAGE.md +52 -0
- package/dist/boot/resolve-spec.js +14 -1
- package/dist/boot/stage-09-leader.js +1 -1
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/types.d.ts +1 -1
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +17 -0
- package/dist/config-types.d.ts +44 -3
- package/dist/config-types.js +1 -0
- package/dist/config.js +24 -4
- package/dist/hosted-posture.d.ts +176 -0
- package/dist/hosted-posture.js +76 -0
- package/dist/http/resume-legs.js +38 -28
- package/dist/http/routes/capabilities.js +3 -0
- package/dist/http/wire-types.d.ts +13 -4
- package/dist/run-local.js +2 -0
- package/package.json +1 -1
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
|
-
...(
|
|
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 =
|
|
19
|
+
const authBridgeIssuer = config.authBridgeIssuer ?? config.configCenter?.baseUrl;
|
|
20
20
|
const registryJwtVerifier = authBridgeIssuer
|
|
21
21
|
? createRegistryJwtVerifier({
|
|
22
22
|
issuer: authBridgeIssuer,
|
package/dist/config-catalog.js
CHANGED
|
@@ -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
|
|
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"],
|
package/dist/config-types.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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(可观测)。 */
|
package/dist/config-types.js
CHANGED
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,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-591 / S-564(车LT,7.95.0)—— **「这台部署是不是托管 / 多租」的单一铸点**。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是一处
|
|
5
|
+
*
|
|
6
|
+
* 本仓此前有两条腿各自回答这同一个问题,而且**答得不一样**:
|
|
7
|
+
* · 写容纳面的天花板挂在 **core 的 `orgGoverned`** 上(core `prepareHandsReadFace` 读
|
|
8
|
+
* `deps.permissionRuleStore?.partitions.org === true`)—— 而本仓从不给那只 provider 传 `org` 分区
|
|
9
|
+
* (`boot/stage-04-stores.ts` 的 `createPermissionRuleStoreProvider({ durable })` 只递 durable),
|
|
10
|
+
* core 的 provider 按「传了没有」冻出 `partitions` ⇒ 本仓 `org` 恒 `false` ⇒ 那条天花板**结构上不可达**。
|
|
11
|
+
* ⚠️ 这句话此前在本仓被抄成「`boot/resolve-spec.ts` 阶段④ 的 `partitions` 字面量」——`src/` 里**没有**
|
|
12
|
+
* 那个字面量(全仓 `partitions` 的 src 命中全是注释;尺子:同目录 `config` 3721 命中)。手抄的**坐标**
|
|
13
|
+
* 比手抄的值更难发现烂掉,[ref] 的第一排查点就是它;
|
|
14
|
+
* · 能力授予面的赦免挂在 **`requirePrincipal !== true`** 上 —— 但**三条面各有各的谓词,不是一只**
|
|
15
|
+
* (异源复核实测,别照「它们都一样」写):
|
|
16
|
+
* · `settings.hooks` —— `boot/resolve-spec.ts` 的 hooks 臂,判据**只有** `config.requirePrincipal === true`
|
|
17
|
+
* ⇒ 忽略(**不看车道**,任何 `REMOTE_EXEC` 都一样);
|
|
18
|
+
* · `additionalDirectories` / `additionalReadDirectories` —— **只有** `cwdHonored`
|
|
19
|
+
* (= `provider === "host" ∧ requirePrincipal !== true`);
|
|
20
|
+
* · `settings.env` / `cwd` —— `cwdHonored ‖ deviceCwdHonored`,而
|
|
21
|
+
* **`deviceCwdHonored` 这一臂里没有 `requirePrincipal` 项**(`task-cwd.ts`:device 车道上那条路径落在
|
|
22
|
+
* **发起者自己的设备**上,归属由 `device_session` 绑定行验过 ⇒ 跨租户面结构性不存在)。
|
|
23
|
+
*
|
|
24
|
+
* 两条判据的**交集之外**有一形谁都不管:**多用户部署但运维没开 `REQUIRE_PRINCIPAL`,也没有 org 治理**。
|
|
25
|
+
* 那台机器上每一条赦免都成立(它自称单用户),而每一道天花板都不成立(它没有 org 分区)。
|
|
26
|
+
* ⇒ 先把这个判定位铸成**一处**,再让两条腿都读它(clay 裁定③,2026-09-22):**不许各自加特判**——
|
|
27
|
+
* 每加一条特判,「这台机器算不算托管」在本仓就多一个可以自己漂的答案。
|
|
28
|
+
*
|
|
29
|
+
* ## 判据的**唯一**标准:运维**显式点名了一个外部身份签发方**
|
|
30
|
+
*
|
|
31
|
+
* 🔴 判据里**必须**有不读 `requirePrincipal` 的臂,否则腿①(S-564)是同义反复
|
|
32
|
+
* (「`REQUIRE_PRINCIPAL` 没设 ⇒ 要求设 `REQUIRE_PRINCIPAL`」永不触发)。所以词表分两类:
|
|
33
|
+
* · `require-principal` —— 运维**已经说了**这是多租户(定义式,零假阳);
|
|
34
|
+
* · 另外两条 —— 运维没说,但他**显式点了一个外部签发方的名字**。一台机器一个可信主人的形
|
|
35
|
+
* (CC 的信任模型)不需要向任何人验签:签发方存在 ⇒ 身份来自这台机器之外 ⇒ 有别人。
|
|
36
|
+
*
|
|
37
|
+
* ## 🔴 「受控于控制面」**不是**托管证据(主会话裁定⑥ 的二选一,本仓选这一侧)
|
|
38
|
+
*
|
|
39
|
+
* 初版收过第三条臂 `managed-worker`(`SEMA_REGISTRY_WORKER` 在场 = reconciler 管着这台 worker)。
|
|
40
|
+
* **实测两个反例后撤回**:
|
|
41
|
+
* · `src/run-local.ts` 强设 `REQUIRE_PRINCIPAL=false` 之后才 `loadConfig()` —— 一个共享 `.env` 带着
|
|
42
|
+
* registry 键就让本机 CLI **拒启**,而拒因指的那根旋钮正是它自己会覆盖回去的(codex 对抗复审同批独立复现);
|
|
43
|
+
* · 本仓自己的 `test/config-coverage-blocks.test.ts` 两格(configCenter 块)就是「有 worker 名、没开
|
|
44
|
+
* `REQUIRE_PRINCIPAL`」——它们在初版上当场红。
|
|
45
|
+
* ⇒ **fleet 管理轴 ≠ 租户轴**。同理 `AUTH_BRIDGE_ISSUER` 只收**显式 env**,**绝不**收 boot 期那条
|
|
46
|
+
* `?? config.configCenter?.baseUrl` 的回退(`boot/stage-09-leader.ts`)—— 那条回退的事实来源同样是
|
|
47
|
+
* 「配了 center」,收它等于把刚撤回的那条臂换个名字装回来,精度还更差。
|
|
48
|
+
*
|
|
49
|
+
* ## 精度优先于召回(逐条写下被排除的候选,不留「我没想到」的空白)
|
|
50
|
+
*
|
|
51
|
+
* 假阳 = 一台单用户 turnkey 被拒启 / 被收走一个它一直在用的能力。假阴 = 缺口照旧。
|
|
52
|
+
* 两者都坏,但假阳会当场打到在跑的部署,所以词表只收**单用户形写不出来**的证据:
|
|
53
|
+
* · ❌ `operatorPrincipals` 非空 —— **真假阳**:`IMAGE_BAKES_ENABLED` 那条既有不变量
|
|
54
|
+
* (`config-invariants.ts` 的 `bake-operator-principals`)**强制**开烤门的部署把它填非空,
|
|
55
|
+
* 于是一台开了烤门的单用户机器会被判成托管。
|
|
56
|
+
* · ❌ `configCenter`(含 `worker` 名)/ auth-bridge 的 center 回退 —— 见上一段,已实测撤回。
|
|
57
|
+
* · ❌ `SERVICE_AUTH_TOKENS` 条目数 —— 一台机器上同时给 cli 与 web-admin 发一只令牌是单用户常态。
|
|
58
|
+
* · ❌ `DB_BACKEND=mysql|pg` —— 那是**耐久性**轴,不是租户轴(单用户要 durable run 也得开)。
|
|
59
|
+
* · ❌ `BIND_HOST` 非回环 —— 容器里 `0.0.0.0` 是单用户常态(compose/k8s 默认)。
|
|
60
|
+
* · ❌ `COMPLIANCE_PROFILE` / `RETENTION_MAX_AGE_DAYS` / `LOCKED_CONFIG_KEYS` —— 数据/治理轴;
|
|
61
|
+
* 一个自己给自己上合规档的单用户部署不该因此被拒启。
|
|
62
|
+
*
|
|
63
|
+
* **已知召回缺口(显式登记,不用「应该很少」填空)**:一台既不验 principal-JWT、也没点 auth-bridge
|
|
64
|
+
* 签发方的团队机器(例如只靠反代加一层 basic auth)本表**看不见**。它的运维面补偿是:
|
|
65
|
+
* `REQUIRE_PRINCIPAL=true` 本身就是那台机器该设的键,而 `docs/DEPLOY-PREREQS.md` 的「托管形前置」段
|
|
66
|
+
* 把判据写成了给人读的清单。
|
|
67
|
+
*/
|
|
68
|
+
import { type ServiceConfigFlat } from "./config-types.js";
|
|
69
|
+
/** 托管/多租的**证据词表**(闭集;加词 ⇒ {@link hostedEvidenceNote} 的穷举 switch 编译红)。 */
|
|
70
|
+
export declare const HOSTED_EVIDENCE_WORDS: readonly ["require-principal", "principal-jwt", "auth-bridge-issuer"];
|
|
71
|
+
/** 一条证据(闭集三词)。 */
|
|
72
|
+
export type HostedEvidence = (typeof HOSTED_EVIDENCE_WORDS)[number];
|
|
73
|
+
/**
|
|
74
|
+
* 本判据读的配置键集 —— 取自平铺配置型(键名/类型永不与真配置漂)。
|
|
75
|
+
* `ServiceConfig` 与 `CrossInvariantView` 都**结构上**满足本形,所以 boot 腿、热路径腿与请求腿读同一只函数。
|
|
76
|
+
*/
|
|
77
|
+
export type HostedPostureInput = Readonly<Pick<ServiceConfigFlat, "requirePrincipal" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer">>;
|
|
78
|
+
/** 一台 worker 的托管姿态事实(值 + 证据)——纯数据。 */
|
|
79
|
+
export interface HostedPostureFacts {
|
|
80
|
+
/** 这台部署是不是托管 / 多租。 */
|
|
81
|
+
readonly hosted: boolean;
|
|
82
|
+
/** 判成托管的**全部**证据(按词表顺序;`hosted:false` ⇒ 空数组)。拒因逐字点它们,运维才知道改哪儿。 */
|
|
83
|
+
readonly evidence: readonly HostedEvidence[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 判定位的**唯一**铸点(纯数据 ⇒ `build*`)。
|
|
87
|
+
*
|
|
88
|
+
* 🔴 `evidence` 收**全部**命中而不是第一条:拒因要告诉运维「这台机器凭哪几件事被判成托管」,
|
|
89
|
+
* 只报一条会让他改掉那一条之后再撞一次同样的拒(两次拒启之间他学不到全貌)。
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildHostedPosture(c: HostedPostureInput): HostedPostureFacts;
|
|
92
|
+
/**
|
|
93
|
+
* 判据里**非定义式**那两条证据背后的 env 名 —— 即「运维显式点名了一个外部身份签发方」的全部拼法。
|
|
94
|
+
*
|
|
95
|
+
* 🔴 **单一属主**:`src/run-local.ts` 那条「强制单用户」的腿按本表清 env(W-新8)。它**结构上**兑现不了
|
|
96
|
+
* 这些键(它把 `REQUIRE_PRINCIPAL` 焊死成 false ⇒ principal 门根本不上场),留着它们只会让判据看见一个
|
|
97
|
+
* 假事实、然后拿一个**它自己会覆盖回去**的旋钮当拒因。名表抄第二份的那天,加一条证据 = run-local 静默漏清。
|
|
98
|
+
* ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(那是定义式那一条,run-local 自己显式写值)也**没有**
|
|
99
|
+
* `SEMA_REGISTRY_*`(fleet 管理轴,本判据根本不读 —— 本机 CLI 从组织下发面取配置是正当用法)。
|
|
100
|
+
*/
|
|
101
|
+
export declare const HOSTED_EVIDENCE_ENV_KEYS: readonly ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
|
|
102
|
+
/**
|
|
103
|
+
* 把上表那几根 env **清掉**(`run-local.ts` 的「强制单用户」腿调它)。
|
|
104
|
+
*
|
|
105
|
+
* 🔴 **为什么是四行字面 `delete` 而不是照表循环**:配置目录的 D1 对账门按文件数**动态 env 读点**并且是
|
|
106
|
+
* **只降不升的棘轮** —— `process.env[k]` 那种计算属性读它一个都不放过(理由是动态读让「这个文件读了哪些
|
|
107
|
+
* 键」变成机器看不见的事)。照表循环会给 `run-local.ts` 顶上第 3 处动态读、当场撞棘轮。⇒ 名字写成字面,
|
|
108
|
+
* **但只写这一处**;上面那张表仍是**声明面**的属主,两者的一致性由
|
|
109
|
+
* `test/hosted-posture.test.ts` 的行为格看着(逐个设上再调一次,断言全没了)——不是靠人眼比对。
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ 表里**没有** `REQUIRE_PRINCIPAL`(定义式那条,调用方自己显式写值)也没有 `SEMA_REGISTRY_*`
|
|
112
|
+
* (fleet 管理轴,本判据根本不读)。
|
|
113
|
+
*/
|
|
114
|
+
export declare function clearHostedEvidenceEnv(): void;
|
|
115
|
+
/** 一条证据的运维事实句 —— **穷举 switch,无缺省臂**(词表加员即编译红)。 */
|
|
116
|
+
export declare function hostedEvidenceNote(e: HostedEvidence): string;
|
|
117
|
+
/**
|
|
118
|
+
* **腿①(S-564)** —— 托管形部署必须显式开 `REQUIRE_PRINCIPAL`。返回拒因;`undefined` = 通过。
|
|
119
|
+
*
|
|
120
|
+
* 判据与文案的**唯一属主**:boot 期的拒启 throw(`config.ts` 的跨域不变量区)与
|
|
121
|
+
* `CONFIG_CROSS_INVARIANTS` 表(`config-invariants.ts`)**调同一只**,不各抄一份([ref] 的纪律 +
|
|
122
|
+
* [ref]「禁手抄投影」)。
|
|
123
|
+
*
|
|
124
|
+
* 🔴 **表里那一行今天是「预置纵深」,不是在岗执法**(主会话裁定⑤,撤回初版那句「热路径同样在岗」):
|
|
125
|
+
* `checkCrossInvariants` 的候选视图(`config-hot` 那条腿)今天只覆写 limits 那 14 员键,本判据读的
|
|
126
|
+
* 5 个键**一个都不在里面** ⇒ 那一行在热路径上**永不返回拒因**。它先在场是纵深(哪天候选视图覆盖到
|
|
127
|
+
* auth 域,它自己就活),不是现在有人在执法。**假覆盖比没覆盖更坏**(7.94.0 自己刚立的反面规矩)。
|
|
128
|
+
*
|
|
129
|
+
* 🔴 **为什么是拒启而不是 warn**:`principal` 缺席在本仓走 `"default"` 赦免 —— 一台没开这根旋钮的
|
|
130
|
+
* 多用户机器会把**全服无主会话**列进 `ListAgents`,并且把 hooks / shellEnv / cwd / 根围栏拓宽四条
|
|
131
|
+
* 能力授予面一并对每个调用方打开(四条面的谓词各不相同,逐条在模块头注 —— 别照「都一样」读)。
|
|
132
|
+
* 安全轴上的 fail-open 不许有安静的兜底臂([ref])。
|
|
133
|
+
*
|
|
134
|
+
* 🔴 **`settings.env` 那一条的射程要说准**(亲读 `plugins/remote-env-host.ts` 的 `mergeEnv`):适配器的
|
|
135
|
+
* `env` 是**并**到 `process.env` 上的逐键覆盖(不是替),而 `task-settings.ts` 的 `cleanEnvMap` 只判
|
|
136
|
+
* 类型/键长/值长/条数、**没有任何键名拒集** ⇒ `PATH` / `LD_PRELOAD` / `NODE_OPTIONS` 一视同仁。
|
|
137
|
+
* 那条面唯一的防线就是上面那道闸;闸一漂,面全开。
|
|
138
|
+
*/
|
|
139
|
+
export declare function hostedPrincipalRefusal(c: HostedPostureInput): string | undefined;
|
|
140
|
+
/** 腿② 读的键集 = 射程判据 + 天花板旋钮。
|
|
141
|
+
* 🔴 档位的**词表属主在 `config-types.ts`**(`TASK_WRITE_FACE_ESCALATIONS`)—— 本模块刻意**不留第二份**:
|
|
142
|
+
* 施工期真犯过一次(两处各写一份 `["allow","refuse"]`,加词的那天两边会分家),[ref] 第一排查点。 */
|
|
143
|
+
export type TaskWriteFaceInput = TaskHostPostureScope & Readonly<Pick<ServiceConfigFlat, "taskWriteFaceEscalation">>;
|
|
144
|
+
/** 任务层宿主姿态声明被拒时的 wire 子码(附录 A 的 `request.` 族)。 */
|
|
145
|
+
export declare const HOST_POSTURE_REFUSED_CODE = "request.host_posture_refused";
|
|
146
|
+
/** 射程判据读的键集(托管证据 + 车道)。 */
|
|
147
|
+
export type TaskHostPostureScope = HostedPostureInput & Readonly<Pick<ServiceConfigFlat, "remoteExec">>;
|
|
148
|
+
/**
|
|
149
|
+
* **腿③(clay 裁定① 2026-09-22)** —— `approverPosture` 在这台部署上兑现吗?`false` ⇒ 调用点
|
|
150
|
+
* **忽略 + 留声**(与 `task_hooks_ignored` / `task_cwd_ignored` / `task_shell_env_ignored` 同姿势)。
|
|
151
|
+
*
|
|
152
|
+
* 🔴 **为什么这一条不拒**(初版给了 422,当批撤回):这个键**零授权** —— 引擎只把它渲给模型,没有任何
|
|
153
|
+
* 裁决腿读它(`http/wire-types.ts` 的字段注逐字,来源是 core 的 `ApproverPosture` 头注)。而续跑腿
|
|
154
|
+
* **逐字重放盘上的体**,调用方**删不掉**它 ⇒ 一条 park 会被永久砖死;`resumePlanReview` 上更贵:
|
|
155
|
+
* nonce 先烧、拒在其后才抛,运维重签再撞同一个拒 = 无限循环。本仓对这一类的成文结论逐字是
|
|
156
|
+
* 「**砖死 resume 比降级糟**」(`boot/resolve-spec.ts` 顶部那段列了三处先例)。
|
|
157
|
+
* ⇒ 把本批最硬的处置给族里**危害最小**的成员,方向是错的。
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ 它**不是**没有危害:引擎据此把 bypass 指令渲给模型,而那段文字指示模型**改用 Bash 改文件**
|
|
160
|
+
* 而不是用文件工具 ⇒ 写动作从**文件带**(core 写门 / `sensitiveWritePatterns` / `writeProtectedPaths`)
|
|
161
|
+
* 挪到**shell 带**(`shellGate`)。所以它要被**忽略**,而不是原样过境;留声让运维看得见。
|
|
162
|
+
*/
|
|
163
|
+
export declare function taskApproverPostureHonored(c: TaskHostPostureScope): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* **腿②** —— 这条请求要掀开写围栏,这台部署给不给?返回拒因;`undefined` = 放行。
|
|
166
|
+
*
|
|
167
|
+
* 🔴 **响亮拒而不是静默钳**:一个明确要求「别拦我」的调用方撞上一道它掀不掉、也看不见的墙,
|
|
168
|
+
* 正是 [ref] 裁定要修掉的那个失败形。core 在同一条律上的姿势也是抛,不是钳;而本仓这一侧**不能**改用
|
|
169
|
+
* governance 的 `overrides.writeFace="roots"` 去压——实测 core 的 `tightenTaskSpec` 在
|
|
170
|
+
* base `open` × override `roots` 上**静默**取 `roots`(该函数零回调形参 ⇒ 没有留痕通道)。
|
|
171
|
+
*
|
|
172
|
+
* 🔴 **射程(含 device 车道那条对位臂)在共用谓词 {@link hostPostureDeclarationsHonored} 上** ——
|
|
173
|
+
* 两条腿同射程、不同处置,射程只有一处答案。
|
|
174
|
+
*/
|
|
175
|
+
export declare function taskWriteFaceRefusal(c: TaskWriteFaceInput): string | undefined;
|
|
176
|
+
//# sourceMappingURL=hosted-posture.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { TASK_WRITE_FACE_ESCALATIONS } from "./config-types.js";
|
|
2
|
+
import { deviceCwdHonored } from "./task-cwd.js";
|
|
3
|
+
export const HOSTED_EVIDENCE_WORDS = ["require-principal", "principal-jwt", "auth-bridge-issuer"];
|
|
4
|
+
export function buildHostedPosture(c) {
|
|
5
|
+
const evidence = [];
|
|
6
|
+
if (c.requirePrincipal === true)
|
|
7
|
+
evidence.push("require-principal");
|
|
8
|
+
if (c.principalJwtPubkeys.length > 0 || c.principalJwtIss !== undefined || c.principalJwtAud !== undefined)
|
|
9
|
+
evidence.push("principal-jwt");
|
|
10
|
+
if (c.authBridgeIssuer !== undefined)
|
|
11
|
+
evidence.push("auth-bridge-issuer");
|
|
12
|
+
return { hosted: evidence.length > 0, evidence };
|
|
13
|
+
}
|
|
14
|
+
export const HOSTED_EVIDENCE_ENV_KEYS = ["PRINCIPAL_JWT_PUBKEYS", "PRINCIPAL_JWT_ISS", "PRINCIPAL_JWT_AUD", "AUTH_BRIDGE_ISSUER"];
|
|
15
|
+
export function clearHostedEvidenceEnv() {
|
|
16
|
+
delete process.env.PRINCIPAL_JWT_PUBKEYS;
|
|
17
|
+
delete process.env.PRINCIPAL_JWT_ISS;
|
|
18
|
+
delete process.env.PRINCIPAL_JWT_AUD;
|
|
19
|
+
delete process.env.AUTH_BRIDGE_ISSUER;
|
|
20
|
+
}
|
|
21
|
+
export function hostedEvidenceNote(e) {
|
|
22
|
+
switch (e) {
|
|
23
|
+
case "require-principal":
|
|
24
|
+
return "REQUIRE_PRINCIPAL=true (this deployment declares itself multi-tenant)";
|
|
25
|
+
case "principal-jwt":
|
|
26
|
+
return "PRINCIPAL_JWT_PUBKEYS/ISS/AUD is configured (principal identities are verified against an external issuer — a single-user turnkey box has no issuer)";
|
|
27
|
+
case "auth-bridge-issuer":
|
|
28
|
+
return "AUTH_BRIDGE_ISSUER is set explicitly (a second external SSO-JWT issuer; the registry-URL fallback is deliberately NOT read here — that fact is fleet management, not tenancy)";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hostedBecause(facts) {
|
|
32
|
+
return facts.evidence.map(hostedEvidenceNote).join("; ");
|
|
33
|
+
}
|
|
34
|
+
export function hostedPrincipalRefusal(c) {
|
|
35
|
+
const facts = buildHostedPosture(c);
|
|
36
|
+
if (!facts.hosted || c.requirePrincipal === true)
|
|
37
|
+
return undefined;
|
|
38
|
+
return (`this deployment looks HOSTED/multi-tenant (${hostedBecause(facts)}) but REQUIRE_PRINCIPAL is not set — ` +
|
|
39
|
+
`an absent principal takes the single-user "default" amnesty, which would list every ownerless session to every caller ` +
|
|
40
|
+
`and open the single-user capability grants (settings.hooks / settings.env / cwd / additionalDirectories) to all of them. ` +
|
|
41
|
+
`Set REQUIRE_PRINCIPAL=true (and OPERATOR_PRINCIPALS for the operator-only routes), or remove the hosted-deployment configuration if this really is a single-user box.`);
|
|
42
|
+
}
|
|
43
|
+
export const HOST_POSTURE_REFUSED_CODE = "request.host_posture_refused";
|
|
44
|
+
function hostPostureDeclarationsHonored(c) {
|
|
45
|
+
if (deviceCwdHonored(c))
|
|
46
|
+
return true;
|
|
47
|
+
return !buildHostedPosture(c).hosted;
|
|
48
|
+
}
|
|
49
|
+
export function taskApproverPostureHonored(c) {
|
|
50
|
+
return hostPostureDeclarationsHonored(c);
|
|
51
|
+
}
|
|
52
|
+
export function taskWriteFaceRefusal(c) {
|
|
53
|
+
switch (c.taskWriteFaceEscalation) {
|
|
54
|
+
case "allow":
|
|
55
|
+
return undefined;
|
|
56
|
+
case "refuse":
|
|
57
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
58
|
+
`Refused because this deployment pins TASK_WRITE_FACE_ESCALATION=refuse (the operator's own declaration — it wins over any request, unconditionally). ` +
|
|
59
|
+
`Omit permissionMode:"bypassPermissions", or ask the operator for TASK_WRITE_FACE_ESCALATION=allow.`);
|
|
60
|
+
case undefined:
|
|
61
|
+
break;
|
|
62
|
+
default: {
|
|
63
|
+
const unhandled = c.taskWriteFaceEscalation;
|
|
64
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
65
|
+
`Refused because this deployment carries an UNRECOGNISED TASK_WRITE_FACE_ESCALATION word (${String(unhandled)}) — a knob this build cannot interpret is never read as permission. ` +
|
|
66
|
+
`Set TASK_WRITE_FACE_ESCALATION to one of ${TASK_WRITE_FACE_ESCALATIONS.join(" | ")}, or unset it to let the deployment posture decide.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (hostPostureDeclarationsHonored(c))
|
|
70
|
+
return undefined;
|
|
71
|
+
const facts = buildHostedPosture(c);
|
|
72
|
+
return (`permissionMode:"bypassPermissions" opens the WRITE containment face (TaskSpec.writeFace:"open") — the run could write outside the task root. ` +
|
|
73
|
+
`Refused because this deployment is HOSTED/multi-tenant (${hostedBecause(facts)}), so the task layer may only TIGHTEN the write face, never open it. ` +
|
|
74
|
+
`Set TASK_WRITE_FACE_ESCALATION=allow if the DEPLOYMENT permits the task layer to open it, or omit permissionMode:"bypassPermissions".`);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=hosted-posture.js.map
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -286,20 +286,10 @@ export function createResumeLegs(ctx) {
|
|
|
286
286
|
if (!ctx)
|
|
287
287
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
288
288
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
catch (e) {
|
|
295
|
-
if (e instanceof HttpError) {
|
|
296
|
-
return {
|
|
297
|
-
status: 409,
|
|
298
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
throw e;
|
|
302
|
-
}
|
|
289
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
290
|
+
if (!resolved.ok)
|
|
291
|
+
return resolved.answer;
|
|
292
|
+
const { spec, engineNotices } = resolved;
|
|
303
293
|
const pendingTool = cp.pendingAction.toolName;
|
|
304
294
|
if (decision === "approve" && pendingTool === ASK_USER_QUESTION_TOOL_NAME && !answer) {
|
|
305
295
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
|
|
@@ -333,6 +323,24 @@ export function createResumeLegs(ctx) {
|
|
|
333
323
|
const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
|
|
334
324
|
return { spec, engineNotices };
|
|
335
325
|
};
|
|
326
|
+
const resolveResumeSpecOrBlocked = async (body, req, auth) => {
|
|
327
|
+
try {
|
|
328
|
+
const { spec, engineNotices } = await resolveResumeSpec(body, req, auth);
|
|
329
|
+
return { ok: true, spec, engineNotices };
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
if (e instanceof HttpError) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
answer: {
|
|
336
|
+
status: 409,
|
|
337
|
+
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
throw e;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
336
344
|
async function driveResumeIntoRunLog(args) {
|
|
337
345
|
if (args.acceptEarly !== true)
|
|
338
346
|
return await driveResumeLeg(args);
|
|
@@ -1041,7 +1049,10 @@ export function createResumeLegs(ctx) {
|
|
|
1041
1049
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1042
1050
|
const ctx = ctxRead.ctx;
|
|
1043
1051
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1044
|
-
const
|
|
1052
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1053
|
+
if (!resolved.ok)
|
|
1054
|
+
return resolved.answer;
|
|
1055
|
+
const { spec, engineNotices } = resolved;
|
|
1045
1056
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1046
1057
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1047
1058
|
return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
@@ -1070,17 +1081,10 @@ export function createResumeLegs(ctx) {
|
|
|
1070
1081
|
if (windowDenial)
|
|
1071
1082
|
return { status: windowDenial.status, body: windowDenial.body };
|
|
1072
1083
|
const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
catch (e) {
|
|
1079
|
-
if (e instanceof HttpError) {
|
|
1080
|
-
return { status: 409, body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) } };
|
|
1081
|
-
}
|
|
1082
|
-
throw e;
|
|
1083
|
-
}
|
|
1084
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1085
|
+
if (!resolved.ok)
|
|
1086
|
+
return resolved.answer;
|
|
1087
|
+
const { spec, engineNotices } = resolved;
|
|
1084
1088
|
const hostSpec = { ...spec, objective, sessionId };
|
|
1085
1089
|
const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
|
|
1086
1090
|
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
@@ -1151,7 +1155,10 @@ export function createResumeLegs(ctx) {
|
|
|
1151
1155
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1152
1156
|
const ctx = ctxRead.ctx;
|
|
1153
1157
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1154
|
-
const
|
|
1158
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1159
|
+
if (!resolved.ok)
|
|
1160
|
+
return resolved.answer;
|
|
1161
|
+
const { spec, engineNotices } = resolved;
|
|
1155
1162
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1156
1163
|
return driveResumeIntoRunLog({
|
|
1157
1164
|
runner: runnerFor(spec),
|
|
@@ -1242,7 +1249,10 @@ export function createResumeLegs(ctx) {
|
|
|
1242
1249
|
}
|
|
1243
1250
|
const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1244
1251
|
const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
|
|
1245
|
-
const
|
|
1252
|
+
const resolved = await resolveResumeSpecOrBlocked(resumeBody, req, auth);
|
|
1253
|
+
if (!resolved.ok)
|
|
1254
|
+
return resolved.answer;
|
|
1255
|
+
const { spec, engineNotices } = resolved;
|
|
1246
1256
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1247
1257
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1248
1258
|
const outcome = {
|
|
@@ -3,6 +3,7 @@ import { isModelAllowlisted } from "../../model-select.js";
|
|
|
3
3
|
import { resolveModelApiKey } from "../../key-resolver.js";
|
|
4
4
|
import { isSealedKeyPoison } from "../../sealed-key.js";
|
|
5
5
|
import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
|
|
6
|
+
import { taskApproverPostureHonored, taskWriteFaceRefusal } from "../../hosted-posture.js";
|
|
6
7
|
import { DEVICE_PROTOCOL_VERSION, DEVICE_WS_PATH } from "../../device-ws-protocol.js";
|
|
7
8
|
import { a2aInjectionHonored } from "../../task-a2a.js";
|
|
8
9
|
import { mcpInjectionHonored } from "../../task-mcp.js";
|
|
@@ -135,6 +136,8 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
135
136
|
interactiveTools: true,
|
|
136
137
|
projectContext: cwdHonored(deps.config),
|
|
137
138
|
callerCwd: cwdHonored(deps.config) || deviceCwdHonored(deps.config),
|
|
139
|
+
taskWriteFaceOpen: taskWriteFaceRefusal(deps.config) === undefined,
|
|
140
|
+
taskApproverPosture: taskApproverPostureHonored(deps.config),
|
|
138
141
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
139
142
|
mcpInjection: mcpInjectionHonored(deps.config),
|
|
140
143
|
a2a: true,
|
|
@@ -326,10 +326,19 @@ export interface TaskRequestBody {
|
|
|
326
326
|
* (且只能收紧、deny-wins),前者只说「谁来答卡」。两者在语义上同族,声明它而不开 `bypassPermissions`
|
|
327
327
|
* 只是对模型说了一句与实际不符的话 —— 见契约文档那一段的「谁受伤」。
|
|
328
328
|
*
|
|
329
|
-
* 🔴
|
|
330
|
-
* (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server
|
|
331
|
-
*
|
|
332
|
-
*
|
|
329
|
+
* 🔴 **值域上本仓零判据**(`boot/resolve-spec.ts` 写点):词的拒绝逻辑属引擎
|
|
330
|
+
* (`refuseInvalidApproverPosture` 响亮拒一个不认识的词),server **不抄词表、不加第二道值域门** ——
|
|
331
|
+
* 在下游再判一次就是同一语义面的第二个判官(源头修复纪律 [ref]),而且本仓抄一份词表的那天 core 加
|
|
332
|
+
* 第二个词就会被静默 400 掉。型也从 core 的 `ApproverPosture` 派生(不抄字面联合)。
|
|
333
|
+
*
|
|
334
|
+
* 🔴 **但「原样过境」自 7.95.0(S-591 腿③)起不再成立 —— 它有一道**准入**闸**:本键是一句**宿主姿态
|
|
335
|
+
* 声明**(「这个 run 的审批卡由宿主自己自动答」),而在**托管 / 多租**部署上提交方**不是**宿主。
|
|
336
|
+
* 所以托管形上它被**忽略 + 留声**(`task_approver_posture_ignored`,`honored:false`),与同族的
|
|
337
|
+
* `settings.hooks` / `cwd` / `settings.env` 三条逐字同姿势;单用户 turnkey 与 device 车道照旧过境
|
|
338
|
+
* (射程判据单点 = `src/hosted-posture.ts` 的 `taskApproverPostureHonored`)。
|
|
339
|
+
* ⚠️ **刻意不是 400/422**:本键零授权,而续跑腿逐字重放盘上的体、调用方删不掉它 ⇒ 拒会把一条 parked
|
|
340
|
+
* 任务永久砖死(clay 裁定① 2026-09-22;本仓成文结论「砖死 resume 比降级糟」)。
|
|
341
|
+
* ⇒ 这一条与「值域零判据」**不矛盾**:值域仍归引擎,本仓判的是**这台部署认不认这句声明**。
|
|
333
342
|
*/
|
|
334
343
|
approverPosture?: ApproverPosture;
|
|
335
344
|
/** MF-30 memory PAUSE (shell-host contract, option B per-request — clay 2026-06-27): `false` makes THIS run
|
package/dist/run-local.js
CHANGED
|
@@ -18,6 +18,7 @@ import { taskWallClockSec } from "./task-workflow.js";
|
|
|
18
18
|
import { applyCatalogToSource } from "./capabilities/center-prompts.js";
|
|
19
19
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
|
|
20
20
|
import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
21
|
+
import { clearHostedEvidenceEnv } from "./hosted-posture.js";
|
|
21
22
|
import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
|
|
22
23
|
import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
|
|
23
24
|
import { createConfigProvider } from "./config-provider.js";
|
|
@@ -176,6 +177,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
176
177
|
process.env.CONFIG_PROVIDER = "local";
|
|
177
178
|
process.env.SESSION_BACKEND = "memory";
|
|
178
179
|
process.env.REQUIRE_PRINCIPAL = "false";
|
|
180
|
+
clearHostedEvidenceEnv();
|
|
179
181
|
const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
180
182
|
if (args.root)
|
|
181
183
|
process.env.CONFIG_LOCAL_DIR = args.root;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.95.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|