@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.
- package/USAGE.md +52 -0
- package/dist/boot/resolve-spec.js +14 -1
- package/dist/boot/stage-09-leader.js +1 -1
- package/dist/boot/stores.js +6 -3
- package/dist/boot/workflow-orchestration.js +5 -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-ctx-read.d.ts +40 -0
- package/dist/http/resume-ctx-read.js +25 -0
- package/dist/http/resume-legs.js +62 -42
- package/dist/http/routes/capabilities.js +3 -0
- package/dist/http/routes/images.js +12 -0
- package/dist/http/routes/runs.js +18 -4
- package/dist/http/routes/workflows.js +3 -2
- package/dist/http/wire-types.d.ts +13 -4
- package/dist/observability/corrupt-read-seat.d.ts +56 -0
- package/dist/observability/corrupt-read-seat.js +16 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/orchestration/workflow-journal-entry.d.ts +24 -6
- package/dist/orchestration/workflow-journal-entry.js +17 -10
- package/dist/plugins/approval-ask-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +16 -3
- package/dist/plugins/checkpoint-store-sql.js +7 -9
- package/dist/plugins/image-bake-store-sql.js +85 -13
- package/dist/plugins/image-index-sql.js +75 -12
- package/dist/plugins/local-checkpoint-store.d.ts +23 -2
- package/dist/plugins/local-checkpoint-store.js +40 -33
- package/dist/plugins/local-session-store.d.ts +14 -0
- package/dist/plugins/local-session-store.js +12 -7
- package/dist/plugins/local-sidecar-quarantine.d.ts +10 -0
- package/dist/plugins/local-sidecar-quarantine.js +15 -0
- package/dist/plugins/local-task-attachment-store.d.ts +1 -1
- package/dist/plugins/local-task-attachment-store.js +5 -2
- package/dist/plugins/mailbox-store-sql.d.ts +6 -3
- package/dist/plugins/mailbox-store-sql.js +45 -43
- package/dist/plugins/memory-embedder-fingerprint.js +8 -12
- package/dist/plugins/memory-engine-pg.js +34 -16
- package/dist/plugins/memory-engine-tidb.js +19 -12
- package/dist/plugins/memory-engine-vector-util.d.ts +26 -0
- package/dist/plugins/memory-engine-vector-util.js +3 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +6 -0
- package/dist/plugins/memory-sync-store-pg.js +14 -10
- package/dist/plugins/memory-sync-store-tidb.js +7 -11
- package/dist/plugins/pg-session-storage.js +26 -28
- package/dist/plugins/session-event-payload.d.ts +37 -0
- package/dist/plugins/session-event-payload.js +20 -0
- package/dist/plugins/sql-json-column.d.ts +124 -22
- package/dist/plugins/sql-json-column.js +22 -1
- package/dist/plugins/sql-row-helpers.d.ts +10 -16
- package/dist/plugins/sql-row-helpers.js +0 -29
- package/dist/plugins/store-backend.js +10 -12
- package/dist/plugins/store-contracts.d.ts +4 -6
- package/dist/plugins/store-contracts.js +0 -60
- package/dist/plugins/task-attachment-store.d.ts +21 -12
- package/dist/plugins/task-attachment-store.js +12 -0
- package/dist/plugins/tidb-session-storage.d.ts +2 -2
- package/dist/plugins/tidb-session-storage.js +12 -11
- package/dist/plugins/tidb-session-store.d.ts +2 -2
- package/dist/plugins/tidb-session-store.js +20 -22
- package/dist/plugins/usage-window-store-sql.d.ts +9 -1
- package/dist/plugins/usage-window-store-sql.js +23 -9
- package/dist/plugins/workflow-journal-store-sql.js +16 -4
- package/dist/run-local.js +4 -10
- package/dist/usage-analytics.d.ts +14 -1
- package/dist/usage-analytics.js +11 -17
- package/package.json +2 -2
|
@@ -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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **续跑上下文这一次读的结局**(S-577 第二半,7.94.0)—— 属主模块,所有消费腿共用这一只读口。
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **为什么必须有这只铸点**:此前各消费腿各写一句 `.catch(() => null)`(还有一处把异常文案拼上 wire),
|
|
5
|
+
* 于是店层在 S-577 里把坏体从「答缺席」改成「响亮拒」之后,那些 `catch` 会把那次拒**原样吞回缺席** ——
|
|
6
|
+
* 机制上「坏体不再与缺席同答」成立,**用户面上不成立**(用户拿到的还是同一个 409)。判据要钉用户实际
|
|
7
|
+
* 拿到的码,这只铸点就是让那句判据有地方落(test [ref] 的判据订正,当拍采纳)。
|
|
8
|
+
*
|
|
9
|
+
* 三态与各自的理由:
|
|
10
|
+
* · `ok` —— 读出来了。
|
|
11
|
+
* · `absent` —— 行不在(`reapCtx` 按设计在 run 终态后删掉它),**或**读腿抛了一个**不是坏体**的错
|
|
12
|
+
* (连接 / 超时 / 权限 / 店抖动):证不出它在就当它不在,**这是刻意保守**([ref] 方向锚:
|
|
13
|
+
* 拒绝可重试,搁浅不可)。
|
|
14
|
+
* · `corrupt` —— 读腿抛了**坏体类**错(属主谓词 `isJsonColumnBodyProblem`:`not-json` / `malformed`)。
|
|
15
|
+
* **不折成缺席**:两者的运维动作完全不同(前者去查那一行,后者什么都不用做)。
|
|
16
|
+
* ⚠️ `not-text`(投影漂 = 我方的码病)**刻意不进这一态**,原样上抛、响亮成 500 —— 不许伪装成
|
|
17
|
+
* 「数据坏了」,更不许折成缺席(那会让一次代码漂看起来像「这条会话没有上下文」)。
|
|
18
|
+
*/
|
|
19
|
+
import type { CheckpointCtx } from "./route-ctx.js";
|
|
20
|
+
export type ResumeCtxRead = {
|
|
21
|
+
readonly kind: "ok";
|
|
22
|
+
readonly ctx: CheckpointCtx;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "absent";
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "corrupt";
|
|
27
|
+
readonly where: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function readResumeCtx(load: Promise<CheckpointCtx | null>): Promise<ResumeCtxRead>;
|
|
30
|
+
/** 坏体的对外答复 —— 与会话策略面**同码**(`session-policy-wire.ts` 的 `corrupt` ⇒ 500),不铸新词。
|
|
31
|
+
* 文案只说「哪一行读不出来 + 该找谁」,不把店层 detail 拼上 wire(内部机件不上 wire,S-376 同律)。 */
|
|
32
|
+
export declare function resumeCtxCorruptAnswer(where: string): {
|
|
33
|
+
status: number;
|
|
34
|
+
body: {
|
|
35
|
+
error: string;
|
|
36
|
+
errorCode: string;
|
|
37
|
+
where: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=resume-ctx-read.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isJsonColumnBodyProblem, SqlJsonColumnError } from "../plugins/sql-json-column.js";
|
|
2
|
+
export async function readResumeCtx(load) {
|
|
3
|
+
try {
|
|
4
|
+
const ctx = await load;
|
|
5
|
+
return ctx == null ? { kind: "absent" } : { kind: "ok", ctx };
|
|
6
|
+
}
|
|
7
|
+
catch (e) {
|
|
8
|
+
if (isJsonColumnBodyProblem(e))
|
|
9
|
+
return { kind: "corrupt", where: e.where };
|
|
10
|
+
if (e instanceof SqlJsonColumnError)
|
|
11
|
+
throw e;
|
|
12
|
+
return { kind: "absent" };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function resumeCtxCorruptAnswer(where) {
|
|
16
|
+
return {
|
|
17
|
+
status: 500,
|
|
18
|
+
body: {
|
|
19
|
+
error: "the stored resume context for this session cannot be read — nothing was decided and nothing was resumed. Ask whoever operates this deployment to look at the row named in `where`: it has to be repaired or removed before this session can resume.",
|
|
20
|
+
errorCode: "corrupt",
|
|
21
|
+
where,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=resume-ctx-read.js.map
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -35,6 +35,7 @@ import { gatedPrincipal, explicitOperatorOk } from "./principal-gate.js";
|
|
|
35
35
|
import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
|
|
36
36
|
import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
|
|
37
37
|
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
38
|
+
import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
|
|
38
39
|
function isCheckpointReopenedFailure(r) {
|
|
39
40
|
const code = failureCodeOf(r.terminal);
|
|
40
41
|
return code === "resume.env_failed" || code === "resume.tool_unavailable" || code === "resume.tool_contract_mismatch";
|
|
@@ -278,30 +279,17 @@ export function createResumeLegs(ctx) {
|
|
|
278
279
|
: await startFreshHostTurn(plan.hostSessionId, plan.message, admittedPayer, req, parkedResume, plan.runId);
|
|
279
280
|
}
|
|
280
281
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
catch (e) {
|
|
286
|
-
return { status: 409, body: { error: `resume context load failed: ${e instanceof Error ? e.message : String(e)}`, errorCode: "conflict.resume_context_unavailable" } };
|
|
287
|
-
}
|
|
282
|
+
const ctxRead0 = await readResumeCtx(ctxPromise);
|
|
283
|
+
if (ctxRead0.kind === "corrupt")
|
|
284
|
+
return resumeCtxCorruptAnswer(ctxRead0.where);
|
|
285
|
+
const ctx = ctxRead0.kind === "ok" ? ctxRead0.ctx : null;
|
|
288
286
|
if (!ctx)
|
|
289
287
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
290
288
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
catch (e) {
|
|
297
|
-
if (e instanceof HttpError) {
|
|
298
|
-
return {
|
|
299
|
-
status: 409,
|
|
300
|
-
body: { error: `resume blocked by an admission policy change: ${e.message}`, errorCode: "resume_blocked_by_policy", ...(e.code ? { blockedBy: e.code } : {}), ...(e.extra ?? {}) },
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
throw e;
|
|
304
|
-
}
|
|
289
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
290
|
+
if (!resolved.ok)
|
|
291
|
+
return resolved.answer;
|
|
292
|
+
const { spec, engineNotices } = resolved;
|
|
305
293
|
const pendingTool = cp.pendingAction.toolName;
|
|
306
294
|
if (decision === "approve" && pendingTool === ASK_USER_QUESTION_TOOL_NAME && !answer) {
|
|
307
295
|
return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
|
|
@@ -335,6 +323,24 @@ export function createResumeLegs(ctx) {
|
|
|
335
323
|
const spec = await deps.resolveSpec({ ...body, ...ONE_SHOT_HISTORY_STRIP }, req, auth, { leg: "resume", onNotice: (n) => engineNotices.push(n) });
|
|
336
324
|
return { spec, engineNotices };
|
|
337
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
|
+
};
|
|
338
344
|
async function driveResumeIntoRunLog(args) {
|
|
339
345
|
if (args.acceptEarly !== true)
|
|
340
346
|
return await driveResumeLeg(args);
|
|
@@ -1036,11 +1042,17 @@ export function createResumeLegs(ctx) {
|
|
|
1036
1042
|
if (gateKind !== RESUMABLE_GATE_KIND) {
|
|
1037
1043
|
return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
|
|
1038
1044
|
}
|
|
1039
|
-
const
|
|
1040
|
-
if (
|
|
1045
|
+
const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
|
|
1046
|
+
if (ctxRead.kind === "corrupt")
|
|
1047
|
+
return resumeCtxCorruptAnswer(ctxRead.where);
|
|
1048
|
+
if (ctxRead.kind === "absent")
|
|
1041
1049
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1050
|
+
const ctx = ctxRead.ctx;
|
|
1042
1051
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1043
|
-
const
|
|
1052
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1053
|
+
if (!resolved.ok)
|
|
1054
|
+
return resolved.answer;
|
|
1055
|
+
const { spec, engineNotices } = resolved;
|
|
1044
1056
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1045
1057
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1046
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 } : {}) });
|
|
@@ -1050,7 +1062,10 @@ export function createResumeLegs(ctx) {
|
|
|
1050
1062
|
if (!deps.runStore) {
|
|
1051
1063
|
return { status: 501, body: { error: "delivering this decision requires a durable run store (DB_BACKEND=mysql|pg|local)", errorCode: "capability.run_store_required" } };
|
|
1052
1064
|
}
|
|
1053
|
-
const
|
|
1065
|
+
const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
|
|
1066
|
+
if (ctxRead.kind === "corrupt")
|
|
1067
|
+
return resumeCtxCorruptAnswer(ctxRead.where);
|
|
1068
|
+
const ctx = ctxRead.kind === "ok" ? ctxRead.ctx : null;
|
|
1054
1069
|
if (!ctx) {
|
|
1055
1070
|
return {
|
|
1056
1071
|
status: 409,
|
|
@@ -1066,17 +1081,10 @@ export function createResumeLegs(ctx) {
|
|
|
1066
1081
|
if (windowDenial)
|
|
1067
1082
|
return { status: windowDenial.status, body: windowDenial.body };
|
|
1068
1083
|
const auth = { sessionId, principal, memoryScope: ctx.memoryScope };
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
}
|
|
1074
|
-
catch (e) {
|
|
1075
|
-
if (e instanceof HttpError) {
|
|
1076
|
-
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 ?? {}) } };
|
|
1077
|
-
}
|
|
1078
|
-
throw e;
|
|
1079
|
-
}
|
|
1084
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1085
|
+
if (!resolved.ok)
|
|
1086
|
+
return resolved.answer;
|
|
1087
|
+
const { spec, engineNotices } = resolved;
|
|
1080
1088
|
const hostSpec = { ...spec, objective, sessionId };
|
|
1081
1089
|
const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
|
|
1082
1090
|
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
@@ -1140,11 +1148,17 @@ export function createResumeLegs(ctx) {
|
|
|
1140
1148
|
throw e;
|
|
1141
1149
|
}
|
|
1142
1150
|
}
|
|
1143
|
-
const
|
|
1144
|
-
if (
|
|
1151
|
+
const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
|
|
1152
|
+
if (ctxRead.kind === "corrupt")
|
|
1153
|
+
return resumeCtxCorruptAnswer(ctxRead.where);
|
|
1154
|
+
if (ctxRead.kind === "absent")
|
|
1145
1155
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1156
|
+
const ctx = ctxRead.ctx;
|
|
1146
1157
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1147
|
-
const
|
|
1158
|
+
const resolved = await resolveResumeSpecOrBlocked(ctx.body, req, auth);
|
|
1159
|
+
if (!resolved.ok)
|
|
1160
|
+
return resolved.answer;
|
|
1161
|
+
const { spec, engineNotices } = resolved;
|
|
1148
1162
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1149
1163
|
return driveResumeIntoRunLog({
|
|
1150
1164
|
runner: runnerFor(spec),
|
|
@@ -1222,9 +1236,12 @@ export function createResumeLegs(ctx) {
|
|
|
1222
1236
|
},
|
|
1223
1237
|
};
|
|
1224
1238
|
}
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1239
|
+
const ctxRead = await readResumeCtx(cs.getCtx(sessionId));
|
|
1240
|
+
if (ctxRead.kind === "corrupt")
|
|
1241
|
+
return resumeCtxCorruptAnswer(ctxRead.where);
|
|
1242
|
+
if (ctxRead.kind === "absent")
|
|
1227
1243
|
return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
|
|
1244
|
+
const ctx = ctxRead.ctx;
|
|
1228
1245
|
const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
|
|
1229
1246
|
const effMode = storedBodyPermissionMode(ctx.body);
|
|
1230
1247
|
if (permissionModeAfter !== undefined && effMode !== "plan") {
|
|
@@ -1232,7 +1249,10 @@ export function createResumeLegs(ctx) {
|
|
|
1232
1249
|
}
|
|
1233
1250
|
const approvedMode = decision === "approve" && effMode === "plan" ? (permissionModeAfter ?? "default") : undefined;
|
|
1234
1251
|
const resumeBody = approvedMode === undefined ? ctx.body : { ...ctx.body, permissionMode: approvedMode };
|
|
1235
|
-
const
|
|
1252
|
+
const resolved = await resolveResumeSpecOrBlocked(resumeBody, req, auth);
|
|
1253
|
+
if (!resolved.ok)
|
|
1254
|
+
return resolved.answer;
|
|
1255
|
+
const { spec, engineNotices } = resolved;
|
|
1236
1256
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1237
1257
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1238
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,
|
|
@@ -331,6 +331,18 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
331
331
|
sendError(res, 400, "request.field_invalid", "profile, repo, digest are required");
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
|
+
if (b.bands !== undefined && b.bands !== null && !Array.isArray(b.bands)) {
|
|
335
|
+
sendError(res, 400, "request.field_invalid", `bands must be an array when present`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const badObjectMeta = ["toolchainVersions", "capabilities", "podContract"].find((k) => {
|
|
339
|
+
const v = b[k];
|
|
340
|
+
return v !== undefined && v !== null && (typeof v !== "object" || Array.isArray(v));
|
|
341
|
+
});
|
|
342
|
+
if (badObjectMeta !== undefined) {
|
|
343
|
+
sendError(res, 400, "request.field_invalid", `${badObjectMeta} must be an object when present`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
334
346
|
const id = await idx.registerBuilding({
|
|
335
347
|
profile: b.profile,
|
|
336
348
|
bands: Array.isArray(b.bands) ? b.bands : [],
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -29,6 +29,7 @@ import { assertNever } from "../route-ctx.js";
|
|
|
29
29
|
import { mintAndDriveDurableRun } from "../durable-run-leg.js";
|
|
30
30
|
import { RUN_NOT_FOUND_MESSAGE, runNotFoundMessage } from "../route-ctx.js";
|
|
31
31
|
import { withPrincipal } from "../../observability/principal-context.js";
|
|
32
|
+
import { readResumeCtx } from "../resume-ctx-read.js";
|
|
32
33
|
const ASK_PARKING_RETRY_AFTER_SEC = 2;
|
|
33
34
|
const ASK_ACTOR_LABEL_MAX = 256;
|
|
34
35
|
const AskDecisionActorSchema = ActorAssertionWire.strip();
|
|
@@ -588,8 +589,12 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
588
589
|
const leafId = await Promise.resolve(deps.sessionStorage.getLeafId(run.sessionId)).catch(() => undefined);
|
|
589
590
|
if (leafId === undefined || leafId === null)
|
|
590
591
|
return;
|
|
591
|
-
const
|
|
592
|
-
if (
|
|
592
|
+
const resumeRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
|
|
593
|
+
if (resumeRead.kind === "corrupt") {
|
|
594
|
+
mintResult.corruptCtxWhere = resumeRead.where;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (resumeRead.kind === "absent") {
|
|
593
598
|
mintResult.noResumeCtx = true;
|
|
594
599
|
return;
|
|
595
600
|
}
|
|
@@ -607,8 +612,13 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
607
612
|
createdAt: Date.now(),
|
|
608
613
|
sourceTaskId: taskId,
|
|
609
614
|
});
|
|
610
|
-
const
|
|
611
|
-
if (
|
|
615
|
+
const stillRead = await readResumeCtx(Promise.resolve(cs.getCtx(run.sessionId)));
|
|
616
|
+
if (stillRead.kind === "corrupt") {
|
|
617
|
+
await cs.expire(wakeToken, cpScope).catch(() => undefined);
|
|
618
|
+
mintResult.corruptCtxWhere = stillRead.where;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (stillRead.kind !== "ok") {
|
|
612
622
|
await cs.expire(wakeToken, cpScope).catch(() => undefined);
|
|
613
623
|
mintResult.noResumeCtx = true;
|
|
614
624
|
return;
|
|
@@ -638,6 +648,10 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
638
648
|
sendDuplicateKey();
|
|
639
649
|
return;
|
|
640
650
|
}
|
|
651
|
+
if (mintResult.corruptCtxWhere !== undefined) {
|
|
652
|
+
sendError(res, 500, "corrupt", "run already ended and this session's stored resume context cannot be read — no wake card was minted and nothing was queued. Ask whoever operates this deployment to repair or remove the row named in `where` before steering this session again.", { where: mintResult.corruptCtxWhere });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
641
655
|
if (mintResult.noResumeCtx) {
|
|
642
656
|
sendError(res, 409, "conflict.resume_context_unavailable", "run already ended and this session's resume context is gone — a steer queued now could never be delivered by POST /v1/sessions/:id/wake; submit a new task instead");
|
|
643
657
|
return;
|
|
@@ -2,7 +2,8 @@ import { persistedPlaneOf } from "../../terminal.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
|
|
4
4
|
import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
|
|
5
|
-
import {
|
|
5
|
+
import { journalEntryOfBody, journalEntryPayload, journalRowPayloadOf, readJournalRowBody } from "../../orchestration/workflow-journal-entry.js";
|
|
6
|
+
import { decodeJsonColumn } from "../../plugins/sql-json-column.js";
|
|
6
7
|
import { redactSecrets } from "../../trace/redact.js";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { redactErrorMessage, redactHead } from "../../observability/run-terminal.js";
|
|
@@ -108,7 +109,7 @@ export async function handleWorkflows(req, res, match, ctx) {
|
|
|
108
109
|
return oversizeStub(row.callKey, row.resultBytes);
|
|
109
110
|
}
|
|
110
111
|
try {
|
|
111
|
-
const { payload, parked } = journalEntryPayload(
|
|
112
|
+
const { payload, parked } = journalEntryPayload(journalEntryOfBody(row.callKey, decodeJsonColumn(row.resultJson, readJournalRowBody, `workflow_journal.result for entry ${row.callKey}`)));
|
|
112
113
|
return projectEntry(row.callKey, payload, parked);
|
|
113
114
|
}
|
|
114
115
|
catch {
|
|
@@ -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
|