@sema-agent/server 7.121.0 → 7.123.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/MIGRATION.md +175 -0
- package/README.md +6 -1
- package/README.zh-CN.md +6 -1
- package/USAGE.md +57 -18
- package/dist/approval-reconciler.d.ts +8 -0
- package/dist/approval-reconciler.js +72 -52
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +52 -22
- package/dist/auto-mode-face.d.ts +16 -5
- package/dist/auto-mode-face.js +7 -4
- package/dist/bench/s1/live-deps.js +1 -0
- package/dist/boot/config-center.js +3 -0
- package/dist/boot/leader.js +1 -0
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +2 -1
- package/dist/boot/resolve-spec.js +1 -4
- package/dist/boot/shutdown.js +6 -1
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/capabilities/memory-notice.d.ts +1 -1
- package/dist/config-catalog.js +4 -4
- package/dist/config-center/apply-effective.d.ts +1 -1
- package/dist/config-center/apply-effective.js +5 -3
- package/dist/config-center/center-request.d.ts +6 -9
- package/dist/config-center/center-request.js +11 -27
- package/dist/config-center/http-client.js +3 -3
- package/dist/config-center/read-warnings.d.ts +56 -35
- package/dist/config-center/read-warnings.js +67 -19
- package/dist/config-center/types.d.ts +7 -8
- package/dist/config-lkg.d.ts +12 -9
- package/dist/config-lkg.js +38 -39
- package/dist/config-provider.d.ts +3 -2
- package/dist/config-provider.js +2 -2
- package/dist/config-types.d.ts +20 -9
- package/dist/config-types.js +1 -1
- package/dist/config.d.ts +10 -10
- package/dist/config.js +92 -127
- package/dist/fleet/drain-entry.d.ts +28 -3
- package/dist/fleet/drain-entry.js +3 -0
- package/dist/fleet/engine-signal.d.ts +54 -23
- package/dist/fleet/engine-signal.js +29 -5
- package/dist/fleet/serving-lease.js +13 -15
- package/dist/fleet-client.d.ts +8 -6
- package/dist/fleet-client.js +4 -3
- package/dist/governance-overrides.d.ts +7 -2
- package/dist/http/admission.js +7 -5
- package/dist/http/approval-sweeps.js +2 -2
- package/dist/http/checkpoint-ctx.d.ts +60 -11
- package/dist/http/checkpoint-ctx.js +23 -9
- package/dist/http/dispatch.js +22 -19
- package/dist/http/health-self-report.d.ts +3 -0
- package/dist/http/resume-legs.js +8 -8
- package/dist/http/route-ctx.d.ts +19 -3
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/admin-drain.d.ts +5 -2
- package/dist/http/routes/admin-drain.js +14 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -0
- package/dist/http/routes/approvals-assistant.js +6 -6
- package/dist/http/routes/capabilities.js +7 -5
- package/dist/http/routes/fleet.d.ts +1 -0
- package/dist/http/routes/fleet.js +1 -1
- package/dist/http/routes/images.d.ts +1 -0
- package/dist/http/routes/images.js +1 -1
- package/dist/http/routes/leader.js +6 -0
- package/dist/http/routes/runs.d.ts +2 -0
- package/dist/http/routes/runs.js +5 -4
- package/dist/http/routes/sessions.d.ts +1 -0
- package/dist/http/routes/sessions.js +2 -5
- package/dist/http/routes/trace-usage.d.ts +1 -0
- package/dist/http/routes/trace-usage.js +1 -1
- package/dist/http/routes/workflows.d.ts +1 -0
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/send.d.ts +13 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/stored-body-replay.d.ts +28 -0
- package/dist/http/stored-body-replay.js +22 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/leader/leader.d.ts +23 -1
- package/dist/leader/leader.js +6 -1
- package/dist/leader/repair-wire.d.ts +1 -1
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/run-entry.d.ts +27 -0
- package/dist/leader/run-entry.js +29 -0
- package/dist/leader/wire.d.ts +10 -0
- package/dist/leader/wire.js +15 -10
- package/dist/model-entry-refusal.d.ts +18 -9
- package/dist/model-entry-refusal.js +19 -14
- package/dist/model-route-endpoint.d.ts +21 -1
- package/dist/model-route-endpoint.js +9 -3
- package/dist/model-route-gap.d.ts +103 -0
- package/dist/model-route-gap.js +80 -0
- package/dist/model-route-url.d.ts +13 -20
- package/dist/model-route-url.js +28 -24
- package/dist/namespaced-rule-name.d.ts +16 -30
- package/dist/namespaced-rule-name.js +2 -20
- package/dist/observability/fail-open.d.ts +23 -10
- package/dist/observability/fail-open.js +8 -8
- package/dist/plugins/approval-ask-store-file.d.ts +1 -0
- package/dist/plugins/approval-ask-store-file.js +3 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
- package/dist/plugins/approval-ask-store-memory.js +6 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -0
- package/dist/plugins/one-time-migrations.js +40 -0
- package/dist/plugins/park-commit-watch.d.ts +19 -5
- package/dist/plugins/park-commit-watch.js +24 -17
- package/dist/plugins/session-policy-store-sql.js +1 -2
- package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
- package/dist/router/route-orchestration.d.ts +4 -2
- package/dist/router/route-orchestration.js +2 -1
- package/dist/rule-entry-list.d.ts +3 -13
- package/dist/rule-entry-list.js +1 -12
- package/dist/run-local.js +4 -2
- package/dist/runtime-governance.d.ts +67 -32
- package/dist/runtime-governance.js +28 -9
- package/dist/session-policy-wire.d.ts +47 -49
- package/dist/session-policy-wire.js +41 -36
- package/dist/session-sync.d.ts +5 -1
- package/dist/session-sync.js +2 -1
- package/dist/sighup-idle.d.ts +7 -1
- package/dist/sighup-idle.js +4 -4
- package/dist/spec-fields.d.ts +4 -4
- package/dist/spec-fields.js +1 -4
- package/dist/task-settings.d.ts +36 -14
- package/dist/task-settings.js +20 -23
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +13 -1
- package/dist/trace/redact.d.ts +3 -3
- package/dist/unattended-permission-mode.d.ts +17 -13
- package/dist/unattended-permission-mode.js +3 -1
- package/dist/url-shape.d.ts +31 -0
- package/dist/url-shape.js +41 -0
- package/package.json +3 -3
- package/scripts/drain-check-local.mjs +1 -1
|
@@ -23,15 +23,24 @@ export function mintEngineSignal(deps) {
|
|
|
23
23
|
const ds = deps.drainState;
|
|
24
24
|
const draining = ds === undefined ? undefined : drainingFactOf(ds);
|
|
25
25
|
const restart = deps.restartState?.();
|
|
26
|
-
const
|
|
26
|
+
const configApply = deps.configApplyState?.();
|
|
27
27
|
return {
|
|
28
28
|
verdict,
|
|
29
29
|
...(draining !== undefined ? { draining } : {}),
|
|
30
30
|
...(restart !== undefined ? { restart } : {}),
|
|
31
31
|
...(deps.config?.fleetGeneration !== undefined ? { generation: deps.config.fleetGeneration } : {}),
|
|
32
|
-
...(deps.instanceId !== undefined ? { instanceId: deps.instanceId } : {}),
|
|
33
32
|
lease: deps.servingLease?.() ?? LEASE_NONE,
|
|
34
|
-
...(
|
|
33
|
+
...(configApply !== undefined ? { configApply } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export const ENGINE_SIGNAL_KEY = "engineSignal";
|
|
37
|
+
export function buildEngineSignal(s) {
|
|
38
|
+
return {
|
|
39
|
+
ready: s.verdict.ready,
|
|
40
|
+
...(s.draining !== undefined ? { draining: s.draining } : {}),
|
|
41
|
+
...(s.restart !== undefined ? { restart: s.restart } : {}),
|
|
42
|
+
...fleetMembershipKeys(s),
|
|
43
|
+
...(s.configApply !== undefined ? { configTargetVersion: s.configApply.configTargetVersion, configAppliedVersion: s.configApply.configAppliedVersion } : {}),
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
46
|
export function fleetMembershipKeys(s) {
|
|
@@ -51,8 +60,23 @@ export function fleetMembershipKeys(s) {
|
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
|
-
export function
|
|
55
|
-
return
|
|
63
|
+
export function heartbeatReadinessKeys(s) {
|
|
64
|
+
return { ready: s.verdict.ready, draining: s.draining !== undefined };
|
|
65
|
+
}
|
|
66
|
+
export function healthDrainingKeys(s) {
|
|
67
|
+
return s.draining === undefined ? {} : { draining: true, drainingSince: s.draining.since };
|
|
68
|
+
}
|
|
69
|
+
export function healthConfigLedgerKeys(s) {
|
|
70
|
+
const g = s.configApply;
|
|
71
|
+
if (g === undefined)
|
|
72
|
+
return {};
|
|
73
|
+
return {
|
|
74
|
+
configTargetVersion: g.configTargetVersion,
|
|
75
|
+
configAppliedVersion: g.configAppliedVersion,
|
|
76
|
+
configTargetOrdinal: g.configTargetOrdinal,
|
|
77
|
+
configAppliedOrdinal: g.configAppliedOrdinal,
|
|
78
|
+
...(g.configApplyStaleMs !== undefined ? { configApplyStaleMs: g.configApplyStaleMs } : {}),
|
|
79
|
+
};
|
|
56
80
|
}
|
|
57
81
|
export function restartActionable(s) {
|
|
58
82
|
return s.restart !== undefined && s.restart.withheld !== true;
|
|
@@ -39,8 +39,8 @@ export function createServingLeaseClient(o) {
|
|
|
39
39
|
const epoch = o.clock?.epoch ?? Date.now;
|
|
40
40
|
const f = o.fetchImpl ?? fetch;
|
|
41
41
|
const stickyPath = o.dataRoot === undefined ? undefined : join(o.dataRoot, SERVING_LEASE_DENIED_FILE);
|
|
42
|
-
let
|
|
43
|
-
|
|
42
|
+
let capability;
|
|
43
|
+
const enabled = () => capability?.announced === true;
|
|
44
44
|
let denied = false;
|
|
45
45
|
let stopped = false;
|
|
46
46
|
let lease;
|
|
@@ -55,7 +55,7 @@ export function createServingLeaseClient(o) {
|
|
|
55
55
|
};
|
|
56
56
|
const schedule = (ms) => {
|
|
57
57
|
clear();
|
|
58
|
-
if (stopped || denied || !enabled)
|
|
58
|
+
if (stopped || denied || !enabled())
|
|
59
59
|
return;
|
|
60
60
|
timer = setTimeout(() => void renewNow(), Math.max(RENEW_MIN_INTERVAL_MS, ms));
|
|
61
61
|
timer.unref?.();
|
|
@@ -97,7 +97,7 @@ export function createServingLeaseClient(o) {
|
|
|
97
97
|
o.drain({ actionId: leaseDrainActionId(o.generation), deadlineAt: epoch() + o.drainWindowMs() });
|
|
98
98
|
};
|
|
99
99
|
const renewOnce = async () => {
|
|
100
|
-
if (stopped || denied || !enabled)
|
|
100
|
+
if (stopped || denied || !enabled())
|
|
101
101
|
return;
|
|
102
102
|
const sendMono = mono();
|
|
103
103
|
const sendEpoch = epoch();
|
|
@@ -132,7 +132,7 @@ export function createServingLeaseClient(o) {
|
|
|
132
132
|
failStreak = 0;
|
|
133
133
|
switch (reading.verdict) {
|
|
134
134
|
case "granted":
|
|
135
|
-
if (denied
|
|
135
|
+
if (denied)
|
|
136
136
|
return;
|
|
137
137
|
lease = { expiresAtMono: sendMono + reading.ttlMs, leaseUntil: sendEpoch + reading.ttlMs };
|
|
138
138
|
nextInMs = reading.ttlMs / 3;
|
|
@@ -161,29 +161,27 @@ export function createServingLeaseClient(o) {
|
|
|
161
161
|
return;
|
|
162
162
|
const on = caps?.servingLease === true;
|
|
163
163
|
if (!on)
|
|
164
|
-
recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled ? "enabled" : "none"}`);
|
|
165
|
-
if (on && !enabled) {
|
|
166
|
-
|
|
167
|
-
enabledAtMono = mono();
|
|
164
|
+
recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled() ? "enabled" : "none"}`);
|
|
165
|
+
if (on && !enabled()) {
|
|
166
|
+
capability = { announced: true, firstSeenAtMono: capability?.firstSeenAtMono ?? mono() };
|
|
168
167
|
o.logger?.info("fleet_serving_lease_enabled", { generation: o.generation, worker: o.center.worker });
|
|
169
168
|
void renewNow();
|
|
170
169
|
}
|
|
171
|
-
else if (!on &&
|
|
172
|
-
|
|
173
|
-
lease = undefined;
|
|
170
|
+
else if (!on && capability?.announced === true) {
|
|
171
|
+
capability = { announced: false, firstSeenAtMono: capability.firstSeenAtMono };
|
|
174
172
|
clear();
|
|
175
|
-
o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again" });
|
|
173
|
+
o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again; its outage clock is not reset (when the capability returns, the last grant and this process's first sighting of the capability still count)" });
|
|
176
174
|
}
|
|
177
175
|
},
|
|
178
176
|
view() {
|
|
179
177
|
if (denied)
|
|
180
178
|
return { state: "denied" };
|
|
181
|
-
if (!
|
|
179
|
+
if (capability === undefined || !capability.announced)
|
|
182
180
|
return { state: "none" };
|
|
183
181
|
const now = mono();
|
|
184
182
|
if (lease !== undefined && now < lease.expiresAtMono)
|
|
185
183
|
return { state: "granted", leaseUntil: lease.leaseUntil };
|
|
186
|
-
const expiredAt = lease?.expiresAtMono ??
|
|
184
|
+
const expiredAt = lease?.expiresAtMono ?? capability.firstSeenAtMono;
|
|
187
185
|
return { state: "expired", pastOutageMax: now - expiredAt >= o.outageMaxMs };
|
|
188
186
|
},
|
|
189
187
|
resumeStickyDenial() {
|
package/dist/fleet-client.d.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* 配置席 `fleetAdvertiseAddress`(env `FLEET_ADVERTISE_ADDRESS`,`loadConfig` 解析一次;可达性是运维的话,不自猜:k8s pod IP /
|
|
7
7
|
* docker alias / 127.0.0.1:port 只有部署环境自己知道)。不配中心的部署零行为变化;配了中心而不公告的引擎只报用量、不注册(7.105.0 及以前它连用量都不报)。
|
|
8
8
|
* - **announce=注册=心跳同形状幂等 upsert**:boot 后(listen 之后)POST 一次 + 每 heartbeatMs 续。
|
|
9
|
-
* `ready` / `draining`
|
|
10
|
-
* 与 `/health.ready`、`GET /readyz
|
|
11
|
-
* `draining
|
|
12
|
-
*
|
|
9
|
+
* `ready` / `draining` 两字段由引擎信号铸点的同一次读铸(S-740:`ready` = `http/readiness.ts` `readinessOf` 的判,
|
|
10
|
+
* 与 `/health.ready`、`GET /readyz`、计费提交门同源;= roster 已落 ∧ ¬draining),`draining` = 排空事实在场(与嵌套
|
|
11
|
+
* `engineSignal.draining` 同一个字段,`fleet/engine-signal.ts` 规则 5)。修前 `ready = !isDraining()`,不含 roster ⇒ roster 未落时
|
|
12
|
+
* 心跳说「就绪」而计费提交在 503。
|
|
13
|
+
* - **`engineSignal`(7.123.0)**:引擎态整只(契约 `EngineSignal`),与扁平键同一次读数;契约包 10.x 的心跳形没有它的座位(10.x 中心
|
|
14
|
+
* 默认 strip 解析丢掉它、照收心跳);扁平布尔 `draining` settings-schema 12.0.0 删、本引擎 7.124.0+ 删发。
|
|
13
15
|
* - **`restart?`(S-741)**:在场 ⇔ 本副本必须重启才能应用配置 —— 取自 `/health.restart` **同一只**取值口(装配层接
|
|
14
16
|
* `deployment.restartState`,config-center 的 `restartState()`),同一个对象,形 = settings-schema `RestartSignal`;
|
|
15
17
|
* 缺席即键缺席(中心按行整体替换,不合并 —— 一条不带它的心跳就是「不再需要」)。
|
|
@@ -80,8 +82,8 @@ export interface FleetClientOpts {
|
|
|
80
82
|
advertiseAddress?: string | undefined;
|
|
81
83
|
version: string;
|
|
82
84
|
/** 编排器片 2 件 1:引擎信号的**一只铸点**(装配层接 `mintEngineSignal(deployment)` —— `/health` 与 `/readyz` 读的同一只、同一个 deps 组)。
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
+
* 每条公告现铸**一次**,嵌套 `engineSignal` 与扁平 `ready` / `draining` / `restart` / 代际与租约三键都取自这一次(不会撕裂成 `ready:true` +
|
|
86
|
+
* `draining:true`,也不会出现心跳与 `/health` 各算一份重启或租约)。修前是 `readiness` + `restart` 两只取值口(S-740 / S-741),合进这一只。 */
|
|
85
87
|
signal: () => EngineSignalFacts;
|
|
86
88
|
/** S-768:模型拒因信号(装配层接 `/health` 同一只取值口 `deployment.modelRefusalsState`)。每条公告现读一次;`undefined` ⇒ 公告不带
|
|
87
89
|
* `modelRefusals` 键。必填:与 `restart` 同 —— 心跳面没有「不知道有没有拒」这一形(老引擎不发这个键,中心按 `version` 判「不可知」)。 */
|
package/dist/fleet-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS, readCapabilityReading } from "@sema-agent/settings-schema/fleet";
|
|
2
|
-
import {
|
|
2
|
+
import { ENGINE_SIGNAL_KEY, buildEngineSignal, fleetMembershipKeys, heartbeatReadinessKeys } from "./fleet/engine-signal.js";
|
|
3
3
|
import { centerFetch, centerLaneConfigured } from "./config-center/center-request.js";
|
|
4
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
5
5
|
export const FLEET_USAGE_KEY_CAP = 1024;
|
|
@@ -99,18 +99,19 @@ export function startFleetClient(opts) {
|
|
|
99
99
|
return;
|
|
100
100
|
try {
|
|
101
101
|
const signal = opts.signal();
|
|
102
|
+
const engineSignal = buildEngineSignal(signal);
|
|
102
103
|
const modelRefusals = opts.modelRefusals();
|
|
103
104
|
const body = {
|
|
104
105
|
instanceId: opts.instanceId,
|
|
105
106
|
worker: opts.center.worker,
|
|
106
107
|
address,
|
|
107
108
|
version: opts.version,
|
|
108
|
-
|
|
109
|
-
draining: drainingFlag(signal),
|
|
109
|
+
...heartbeatReadinessKeys(signal),
|
|
110
110
|
heartbeatMs,
|
|
111
111
|
...(signal.restart !== undefined ? { restart: signal.restart } : {}),
|
|
112
112
|
...(modelRefusals !== undefined ? { modelRefusals } : {}),
|
|
113
113
|
...fleetMembershipKeys(signal),
|
|
114
|
+
[ENGINE_SIGNAL_KEY]: engineSignal,
|
|
114
115
|
};
|
|
115
116
|
const { res } = await send("fleet-instances", "POST", body);
|
|
116
117
|
if (!res.ok)
|
|
@@ -7,13 +7,18 @@
|
|
|
7
7
|
* 塞 `allowedTools` 的调用点会静默改写请求的表。型上禁掉它(`allowedTools?: never`),写出来就是编译红 —— 比文本扫描的静态门(方括号写法、
|
|
8
8
|
* 展开形会漏)更硬。
|
|
9
9
|
*
|
|
10
|
+
* 🔴 入参型上同样**没有** `permissionMode`:一条 run 的模式词只有存储的提交体一个源(写者 = 每一个 spec 出生点的盖章 —— 新的持久化写都经 `http/checkpoint-ctx.ts` `buildCheckpointCtx` —— 与 `plan_review` 写回),
|
|
11
|
+
* 按段的收紧折叠写它就是第二个源 —— 一个被批准的计划会在下一次挂起→续跑被折回 plan。core 对模式词是钳(不会放宽),所以这里不另设运行期守卫:
|
|
12
|
+
* 型禁就是这条规则的全部(`permissionMode?: never`,写出来就是编译红)。
|
|
13
|
+
*
|
|
10
14
|
* 🔴 型禁有一个洞([ref] 合并复审 opus 腿):本仓没开 `exactOptionalPropertyTypes`,`{ allowedTools: undefined }` 过型检,而 core 的替换会把 base 上的表
|
|
11
15
|
* 抹成 `undefined`。所以折叠口再加一道**运行期**守卫:入参对象自带 `allowedTools` 键(任何值,含 `undefined`)⇒ 抛(fail-closed:宁多问一次)。
|
|
12
16
|
*/
|
|
13
17
|
import { type TaskSpec } from "@sema-agent/core";
|
|
14
|
-
/** 收紧折叠的入参:`TaskSpec` 的字段,`allowedTools` 除外(型上禁)。 */
|
|
15
|
-
export type GovernanceOverrides = Partial<Omit<TaskSpec, "allowedTools">> & {
|
|
18
|
+
/** 收紧折叠的入参:`TaskSpec` 的字段,`allowedTools` 与 `permissionMode` 除外(型上禁)。 */
|
|
19
|
+
export type GovernanceOverrides = Partial<Omit<TaskSpec, "allowedTools" | "permissionMode">> & {
|
|
16
20
|
readonly allowedTools?: never;
|
|
21
|
+
readonly permissionMode?: never;
|
|
17
22
|
};
|
|
18
23
|
/** 本仓唯一的收紧折叠口(deny-wins 合并;core 在放宽时抛)。 */
|
|
19
24
|
export declare function tightenGoverned(base: TaskSpec, overrides: GovernanceOverrides): TaskSpec;
|
package/dist/http/admission.js
CHANGED
|
@@ -2,7 +2,8 @@ import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isPermissionMode
|
|
|
2
2
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
|
|
3
3
|
import { modelUnavailableMessage } from "../model-entry-refusal.js";
|
|
4
4
|
import { HttpError } from "../security.js";
|
|
5
|
-
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
5
|
+
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, stampEntryPermissionMode, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
|
|
6
|
+
import { autonomyModeFloor } from "../runtime-governance.js";
|
|
6
7
|
import { taskBodyKeyIssue } from "../request-key-closure.js";
|
|
7
8
|
import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
8
9
|
import * as facts from "../capability-facts.js";
|
|
@@ -623,19 +624,20 @@ export function createAdmission(ctx) {
|
|
|
623
624
|
sendError(res, 400, "request.precondition_unmet", "cascade requested but no ladder configured — set MODEL_CASCADE_LADDER (catalog model names, cheapest→strongest)");
|
|
624
625
|
return null;
|
|
625
626
|
}
|
|
627
|
+
const admitted = stampEntryPermissionMode(body, autonomyModeFloor(deps.config.autonomy));
|
|
626
628
|
try {
|
|
627
|
-
const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
|
|
629
|
+
const auth = deps.authorize ? await deps.authorize({ req, body: admitted }) : undefined;
|
|
628
630
|
const engineNotices = [];
|
|
629
|
-
const spec = await deps.resolveSpec(
|
|
631
|
+
const spec = await deps.resolveSpec(admitted, req, auth, { leg: "fresh", ...(legFacts?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n) });
|
|
630
632
|
const bindingIntent = await resolveDevicePlacementIntent({
|
|
631
633
|
config: deps.config,
|
|
632
634
|
req,
|
|
633
635
|
rootSessionId: spec.sessionId,
|
|
634
|
-
deviceId:
|
|
636
|
+
deviceId: admitted.deviceId,
|
|
635
637
|
store: deps.deviceStore,
|
|
636
638
|
enrollment: deps.deviceEnrollment,
|
|
637
639
|
});
|
|
638
|
-
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId:
|
|
640
|
+
return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: admitted.jobId, body: admitted, ...(bindingIntent === undefined ? {} : { bindingIntent }), ...(engineNotices.length > 0 ? { engineNotices } : {}) };
|
|
639
641
|
}
|
|
640
642
|
catch (err) {
|
|
641
643
|
if (err instanceof HttpError) {
|
|
@@ -8,7 +8,7 @@ import { hasServiceAuth } from "./dispatch.js";
|
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
9
|
import { admissionRefusalOf, readinessOf } from "./readiness.js";
|
|
10
10
|
import { readResumeCtx } from "./resume-ctx-read.js";
|
|
11
|
-
import { storedBodyReplayRefusal } from "./
|
|
11
|
+
import { storedBodyReplayRefusal } from "./stored-body-replay.js";
|
|
12
12
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
13
13
|
function blockedScalars(body) {
|
|
14
14
|
const out = {};
|
|
@@ -72,7 +72,7 @@ export function createApprovalSweeps(ctx) {
|
|
|
72
72
|
return undefined;
|
|
73
73
|
if (read.kind === "absent")
|
|
74
74
|
return undefined;
|
|
75
|
-
return storedBodyReplayRefusal(read.ctx);
|
|
75
|
+
return storedBodyReplayRefusal(read.ctx, deps.config.autonomy);
|
|
76
76
|
};
|
|
77
77
|
let parkedSkipped = 0;
|
|
78
78
|
const replaySkipped = [];
|
|
@@ -20,8 +20,19 @@
|
|
|
20
20
|
* 同一面上的第二个「新提交会被拒、存量体重放不能放过」:提交体里技能(顶层 / 子代)的 `disallowedTools` 有引擎读成空的条目(`"Bash "` 这类 7.41 去白后有效、
|
|
21
21
|
* 7.42 什么也不拒的条目)⇒ 提交 400(`resolveSpec`,拒点);存量停驻体重放 ⇒ 409 `resume_blocked_by_policy`。判据函数 = `rule-entry-list.ts` `skillDenyInertEntryOf`。
|
|
22
22
|
*
|
|
23
|
-
* ##
|
|
24
|
-
*
|
|
23
|
+
* ## 第三问(7.122.0):体里模式词的读法记号
|
|
24
|
+
* 7.122.0 起一条 run 的模式词只有一个源 —— 存储的提交体顶层 `permissionMode`(提交入口把运维 `AUTONOMY=plan` 的地板盖进去一次;`plan_review` 写回改它),
|
|
25
|
+
* 治理层不再在每一次续跑重钳。更早版本写下的行体里是**请求的原词**,运维的 plan 由治理层每段重新折上(7.121.x 及以前:只读手 + 写面 roots)。
|
|
26
|
+
* 那种行在本版上按体里的原词重放 = 静默放宽(`acceptEdits` / `bypassPermissions` 原词直接生效;无词 ⇒ 引擎读 checkpoint 记下的词)。
|
|
27
|
+
* ⇒ 同一条规则:**写模式词的人盖读法记号** —— 模式词的写者:每一个出生点(新的持久化写都经 {@link buildCheckpointCtx})与 `plan_review` 写回(经 {@link withPermissionModeRewritten}),各自写词的同一拍在 ctx 上写记号
|
|
28
|
+
`permissionModeReading`(闭集只有当前一词 {@link PERMISSION_MODE_READINGS});续跑重放之前,**本部署此刻有入口地板**
|
|
29
|
+
* (`runtime-governance.ts` `autonomyModeFloor`)而行上没有当前记号、且体的生效词被地板钳会变 ⇒ 拒(行字节不动)。没有地板的部署、体的词本就在地板之下(`plan`)的行
|
|
30
|
+
* 不受影响:那两种行在新旧两种读法下答同一个词。出路:作为新任务重提(仍停驻的先取消),或在它们收尾之前把 `AUTONOMY` 设成 `read-only`(按段的只读围栏,
|
|
31
|
+
* 与 7.121.x 的 `AUTONOMY=plan` 同一个结局,且没有入口地板 ⇒ 不拒)。删除条件登记在 `plugins/one-time-migrations.ts` 的 M12 行。
|
|
32
|
+
*
|
|
33
|
+
* ## 一只判据、五个问点(`http/stored-body-replay.ts` 的 `storedBodyReplayRefusal` / 本模块的 {@link storedBodyReplayAnswer})
|
|
34
|
+
* 「这条停驻行的持久化体在本版上能否重放」只有**一个**读法:上面三问合在 `stored-body-replay.ts` 的 `storedBodyReplayRefusal` 一处(前两问的判据在本模块、第三问读请求设置
|
|
35
|
+
* 与治理两个属主 —— 本模块的值依赖要留在 node 内建 / 引擎包根之内,随包排空脚本经编译产物读它),答复合在 {@link storedBodyReplayAnswer} 一处。
|
|
25
36
|
* 问点五个,同调同答:续跑驱动段(`resume-legs.ts` `resolveResumeSpecOrBlocked`,另四条腿的唯一判点)· plan_review 的 prepare 段(读类拒,在 nonce 消费之前)·
|
|
26
37
|
* SLA 拒扫腿的跳过臂(`approval-sweeps.ts`)· 终态 run 的 steer 铸唤醒卡之前(`routes/runs.ts`:铸卡前先证明它赎得回,证不出 ⇒ 不铸、当场同一个拒)·
|
|
27
38
|
* `resolveSpec` 本身留作真正的拒点(提交腿 400;续跑腿在到它之前已被本判据答掉)。再开第六个问点必须调这两只,不许各判各的 —— 此前铸卡前只判了
|
|
@@ -32,13 +43,19 @@
|
|
|
32
43
|
* 一次性迁移登记表(本条那一行):所有旧读法下停驻的行都已结束或被收割(停驻行的终局 = 落盘起 `TERMINAL_BACKSTOP_MS`,见
|
|
33
44
|
* `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删。
|
|
34
45
|
*/
|
|
46
|
+
import type { PermissionMode } from "@sema-agent/core";
|
|
35
47
|
import type { CheckpointCtx } from "./route-ctx.js";
|
|
36
48
|
import type { TaskRequestBody } from "./wire-types.js";
|
|
37
|
-
import {
|
|
49
|
+
import type { SkillDenyInertEntry } from "../rule-entry-list.js";
|
|
38
50
|
/** 读法记号的闭集(一词 = 当前读法):这一行体里的 `settings.permissions.allow` 是按「交给引擎的 allow 层」写下、也该按它读的
|
|
39
51
|
* (与能力位 `settings.permissions.allowLayer` 同一个说法)。 */
|
|
40
52
|
export declare const PERMISSIONS_ALLOW_READINGS: readonly ["allow_layer"];
|
|
41
53
|
export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
|
|
54
|
+
/** 体里模式词的读法记号闭集(一词 = 当前读法):这一行体的顶层 `permissionMode` 是这条 run 模式词**唯一的源**(提交入口盖过运维地板、`plan_review` 写回改它),
|
|
55
|
+
* 续跑照它读、不再由治理层按段重钳。写点两个 = {@link buildCheckpointCtx}(每一次新的持久化写都写)与 {@link withPermissionModeRewritten}(`plan_review` 写回改词的那一拍写);
|
|
56
|
+
* **不改词**的续跑腿写回传原 ctx ⇒ 记号随行。 */
|
|
57
|
+
export declare const PERMISSION_MODE_READINGS: readonly ["entry_word"];
|
|
58
|
+
export type PermissionModeReading = (typeof PERMISSION_MODE_READINGS)[number];
|
|
42
59
|
/** 被拒的行是在哪种读法下写的(闭集;拒句按它穷举):`list` = 无记号或记号认不得(≤7.114.0 的名单读法;认不得的记号不是任何一代的证据,
|
|
43
60
|
* 按最早那一代说话);`statement` = 7.115.0–7.116.x 写下的表态读法。 */
|
|
44
61
|
export declare const ALLOW_LIST_READING_ERAS: readonly ["list", "statement"];
|
|
@@ -53,13 +70,44 @@ export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
|
|
|
53
70
|
*
|
|
54
71
|
* 调用方是「本进程刚按当前读法读过这份体」的写点:fresh 提交(受理门按当前读法读过 `allow`),或已经过了续跑拒臂的重放腿
|
|
55
72
|
* (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
|
|
56
|
-
*
|
|
73
|
+
* 续跑腿的**写回**不经本函数:**不改词**的写回传读进来的 ctx(记号随行,不重新判 —— 在写回口重新判会把一条没记号的旧行「洗」成新行);
|
|
74
|
+
* **改词**的写回(只有 `plan_review` 的批准 / 编辑)经 {@link withPermissionModeRewritten}。
|
|
57
75
|
*/
|
|
58
76
|
export declare function buildCheckpointCtx(fields: {
|
|
59
77
|
body: TaskRequestBody;
|
|
60
78
|
memoryScope?: string | undefined;
|
|
61
79
|
unattendedLeg?: true;
|
|
62
80
|
}): CheckpointCtx;
|
|
81
|
+
/**
|
|
82
|
+
* 模式词的**第二个写者** —— `plan_review` 的批准 / 编辑把这条 run 的体词从 `plan` 写成批准的档(`resume-legs.ts` `drivePlanReview`,调用点只有那一处;
|
|
83
|
+
* `test/allow-list-reading-park-refusal.test.ts` 枚举调用点,多一处即红)。与 {@link buildCheckpointCtx} **同一条规则**:按当前读法写下的词,盖当前读法记号。
|
|
84
|
+
*
|
|
85
|
+
* 为什么记号必须随词一起写,而不是留给读的一侧去豁免:第三问问的是「体的词是不是按当前读法写下的」,记号就是这句话的证据。改写之后体里的词是本版的 `plan_review`
|
|
86
|
+
* 刚写下的(一个人刚批准的档),它确实是 `entry_word` 读法的词;若只改体、记号缺席,重建段的同一问把它读成「7.121.x 的请求原词」,被地板钳回 `plan` ≠ 所写词 ⇒ 误拒
|
|
87
|
+
* 一条刚被批准的 run。判据侧(`stored-body-replay.ts`)**不为「plan_review 之后」另开特判臂** —— 读的一侧只有一个读法,写的一侧各自负责盖章。
|
|
88
|
+
*
|
|
89
|
+
* 为什么这不是在「洗」旧行:走到写回口的行,其**原体**已过 prepare 段的同一问(词被地板钳不变:词已是 `plan`,或本部署没有地板);原体的词会被地板钳的旧行
|
|
90
|
+
* (`acceptEdits` / `bypassPermissions` / 无词)在 prepare 段就被拒,到不了这里。入参 `ctx` 的其余位(memoryScope / 无人值守标 / allow 记号)原样随行,只换体与模式词记号。
|
|
91
|
+
*
|
|
92
|
+
* 本模块零值依赖的闭包纪律不变(随包排空脚本经编译产物 import;`test/drain-check-local.test.ts`):本函数只用本模块内的东西。
|
|
93
|
+
*/
|
|
94
|
+
export declare function withPermissionModeRewritten(ctx: CheckpointCtx, body: TaskRequestBody): CheckpointCtx;
|
|
95
|
+
/** 行上带的是不是当前的模式词读法记号(第三问的记号那一半;另一半 —— 体的生效词被地板钳会不会变 —— 读请求设置的解析层,住在 `http/stored-body-replay.ts`)。
|
|
96
|
+
* 记号缺席或认不得 ⇒ 不是「按当前读法写下」的证据,与 allow 记号同一条纪律。 */
|
|
97
|
+
export declare function permissionModeReadingStated(ctx: CheckpointCtx): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* 第三问的拒答复 —— 码与落点同 allow 记号那一问(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending,**不是**「原样重试会成功」)。
|
|
100
|
+
* 句子说原因(更早的版本在每次续跑重钳运维的 AUTONOMY、没把它记进任务;本版只在提交时记一次 ⇒ 续跑会按任务提交时的原词跑)与两条出路。
|
|
101
|
+
* 🔴 形是给冻结门看的:`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
|
|
102
|
+
*/
|
|
103
|
+
export declare function permissionModeReadingRefusal(): {
|
|
104
|
+
status: number;
|
|
105
|
+
body: {
|
|
106
|
+
error: string;
|
|
107
|
+
errorCode: string;
|
|
108
|
+
retriable: true;
|
|
109
|
+
};
|
|
110
|
+
};
|
|
63
111
|
/**
|
|
64
112
|
* 本行能否被按当前读法重放:体里 `allow` 在生效而行上的记号不是当前这一个词 ⇒ **不能**(`true` = 拒)。
|
|
65
113
|
* 记号缺席、是旧词(`"statement"`)或认不得(手改 / 将来的写者)⇒ 同样拒:只有当前这个词是「按当前读法写下」的证据。
|
|
@@ -89,12 +137,11 @@ export type StoredBodyReplayRefusal = {
|
|
|
89
137
|
} | {
|
|
90
138
|
readonly kind: "skill_deny_inert";
|
|
91
139
|
readonly entry: SkillDenyInertEntry;
|
|
140
|
+
} | {
|
|
141
|
+
readonly kind: "permission_mode_reading";
|
|
142
|
+
readonly stored: PermissionMode | undefined;
|
|
143
|
+
readonly floor: PermissionMode;
|
|
92
144
|
};
|
|
93
|
-
/**
|
|
94
|
-
* 「存量体能否重放」的**唯一**判据(文件头「一只判据、五个问点」):不能 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。
|
|
95
|
-
* 顺序:先 allow 读法记号,再技能拒侧读成空(两问都中时答第一问 —— 任一问中都不重放,句子只需一句)。
|
|
96
|
-
*/
|
|
97
|
-
export declare function storedBodyReplayRefusal(ctx: CheckpointCtx): StoredBodyReplayRefusal | undefined;
|
|
98
145
|
/** 续跑家族交回的拒答复形(`{ status, body }`,由 `sendResumeOutcome` / 各路由发送;`body` 至少 `error` + `errorCode`)。 */
|
|
99
146
|
export interface ResumeRefusalAnswer {
|
|
100
147
|
readonly status: number;
|
|
@@ -110,17 +157,19 @@ export interface ResumeRefusalAnswer {
|
|
|
110
157
|
*/
|
|
111
158
|
export declare function resumeBlockedByPolicyAnswer(message: string, blockedBy: string, extra?: Record<string, unknown> | undefined): ResumeRefusalAnswer;
|
|
112
159
|
/**
|
|
113
|
-
*
|
|
160
|
+
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的**日志字段**(三处问点同一形,各自只再加坐标键 `sessionId` / `taskId`):`kind` 判别,allow 臂带 `era`、技能臂带 `layer`,`note` 一句出路。
|
|
114
161
|
* 事件名三个(问点各一:`resume_refused_stored_body` / `steer_wake_park_refused_stored_body` / `deny_sweep_stored_body_skipped`),字段形只有这一只。
|
|
115
162
|
*/
|
|
116
163
|
export declare function storedBodyReplayLogFields(refusal: StoredBodyReplayRefusal): {
|
|
117
164
|
kind: StoredBodyReplayRefusal["kind"];
|
|
118
165
|
era?: AllowListReadingEra;
|
|
119
166
|
layer?: SkillDenyInertEntry["layer"];
|
|
167
|
+
storedMode?: PermissionMode | null;
|
|
168
|
+
floor?: PermissionMode;
|
|
120
169
|
note: string;
|
|
121
170
|
};
|
|
122
171
|
/**
|
|
123
|
-
*
|
|
172
|
+
* `stored-body-replay.ts` `storedBodyReplayRefusal` 的答复:每条规则一只既有铸点(allow 记号 ⇒ {@link allowListReadingRefusal};技能拒侧 ⇒ {@link resumeBlockedByPolicyAnswer},与提交腿的 400
|
|
124
173
|
* `request.field_invalid` 同码同句 —— 体上只有这一个问题时;存量体同时还踩中 `resolveSpec` 更早的拒〔三桶超帽 / 托管姿态〕时,提交侧答更早那条、续跑侧答本句,两边都是拒,指路不同)。
|
|
125
174
|
*/
|
|
126
175
|
export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal): ResumeRefusalAnswer;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { skillDenyInertEntryOf } from "../rule-entry-list.js";
|
|
2
1
|
export const PERMISSIONS_ALLOW_READINGS = ["allow_layer"];
|
|
2
|
+
export const PERMISSION_MODE_READINGS = ["entry_word"];
|
|
3
3
|
export const ALLOW_LIST_READING_ERAS = ["list", "statement"];
|
|
4
4
|
export function bodyCarriesAllowList(body) {
|
|
5
5
|
const settings = body?.settings;
|
|
@@ -17,6 +17,24 @@ export function buildCheckpointCtx(fields) {
|
|
|
17
17
|
...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
|
|
18
18
|
...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
19
19
|
...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "allow_layer" } : {}),
|
|
20
|
+
permissionModeReading: "entry_word",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function withPermissionModeRewritten(ctx, body) {
|
|
24
|
+
return { ...ctx, body, permissionModeReading: "entry_word" };
|
|
25
|
+
}
|
|
26
|
+
export function permissionModeReadingStated(ctx) {
|
|
27
|
+
const reading = ctx.permissionModeReading;
|
|
28
|
+
return typeof reading === "string" && PERMISSION_MODE_READINGS.includes(reading);
|
|
29
|
+
}
|
|
30
|
+
export function permissionModeReadingRefusal() {
|
|
31
|
+
return {
|
|
32
|
+
status: 409,
|
|
33
|
+
body: {
|
|
34
|
+
error: "this task was suspended by an earlier server version that re-applied this deployment's AUTONOMY on every resume instead of recording it in the task; this version records the operator's plan floor once, when a task is submitted, so continuing would run this task under the permission mode it was submitted with rather than the one it ran under. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel); to let it finish with read-only hands instead, set AUTONOMY=read-only until it does.",
|
|
35
|
+
errorCode: "checkpoint.unsupported_version",
|
|
36
|
+
retriable: true,
|
|
37
|
+
},
|
|
20
38
|
};
|
|
21
39
|
}
|
|
22
40
|
export function allowListReadingUnstated(ctx) {
|
|
@@ -55,14 +73,6 @@ export function allowListReadingRefusal(era) {
|
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
|
-
export function storedBodyReplayRefusal(ctx) {
|
|
59
|
-
if (allowListReadingUnstated(ctx))
|
|
60
|
-
return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx) };
|
|
61
|
-
const entry = skillDenyInertEntryOf(ctx.body);
|
|
62
|
-
if (entry !== undefined)
|
|
63
|
-
return { kind: "skill_deny_inert", entry };
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
76
|
export function resumeBlockedByPolicyAnswer(message, blockedBy, extra) {
|
|
67
77
|
return {
|
|
68
78
|
status: 409,
|
|
@@ -75,6 +85,8 @@ export function storedBodyReplayLogFields(refusal) {
|
|
|
75
85
|
return { kind: refusal.kind, era: refusal.era, note: "this session's stored request carries settings.permissions.allow written under an earlier reading (list era: a whitelist; statement era: a statement that cleared only the unstated question); it is not replayed under the current reading — the task stays suspended until someone cancels it" };
|
|
76
86
|
case "skill_deny_inert":
|
|
77
87
|
return { kind: refusal.kind, layer: refusal.entry.layer, note: "this session's stored request carries a skill disallowedTools entry the engine now reads as empty (an earlier engine trimmed it into a working deny); it is not replayed — the task stays suspended until someone cancels it and resubmits with the entry fixed" };
|
|
88
|
+
case "permission_mode_reading":
|
|
89
|
+
return { kind: refusal.kind, storedMode: refusal.stored ?? null, floor: refusal.floor, note: "this session's stored request was written before the operator's AUTONOMY plan floor was recorded in the task (earlier versions re-applied it on every resume); replaying it now would run under its submitted permission mode — the task stays suspended until someone cancels it, or AUTONOMY is set to read-only so it can finish with read-only hands" };
|
|
78
90
|
default: {
|
|
79
91
|
const unhandled = refusal;
|
|
80
92
|
throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no log fields`);
|
|
@@ -87,6 +99,8 @@ export function storedBodyReplayAnswer(refusal) {
|
|
|
87
99
|
return allowListReadingRefusal(refusal.era);
|
|
88
100
|
case "skill_deny_inert":
|
|
89
101
|
return resumeBlockedByPolicyAnswer(refusal.entry.message, "request.field_invalid");
|
|
102
|
+
case "permission_mode_reading":
|
|
103
|
+
return permissionModeReadingRefusal();
|
|
90
104
|
default: {
|
|
91
105
|
const unhandled = refusal;
|
|
92
106
|
throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no answer`);
|
package/dist/http/dispatch.js
CHANGED
|
@@ -3,10 +3,10 @@ import { setSsoPrincipal, ssoVerifiedPrincipal, setSsoScope, isDestructiveSessio
|
|
|
3
3
|
import { publicStoreProbeError } from "../store-live-probe.js";
|
|
4
4
|
import { looksLikeJwt } from "@sema-agent/settings-schema/api/auth-bridge";
|
|
5
5
|
import { ROUTE_METHOD_ORDER } from "./route-ctx.js";
|
|
6
|
-
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
|
|
7
|
-
import { sendJson, sendError, isSseResponse } from "./send.js";
|
|
8
|
-
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal } from "./readiness.js";
|
|
9
|
-
import { fleetMembershipKeys, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
|
|
6
|
+
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath, isSubscriptionRequest } from "./route-table.js";
|
|
7
|
+
import { sendJson, sendError, isSseResponse, endSseWithError } from "./send.js";
|
|
8
|
+
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal, billableRefusalOf } from "./readiness.js";
|
|
9
|
+
import { ENGINE_SIGNAL_KEY, buildEngineSignal, fleetMembershipKeys, healthConfigLedgerKeys, healthDrainingKeys, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
|
|
10
10
|
import { buildHealthSelfReport } from "./health-self-report.js";
|
|
11
11
|
import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized, verifiedIdentitySource } from "./principal-gate.js";
|
|
12
12
|
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
@@ -43,6 +43,7 @@ export function createDispatch(ctx) {
|
|
|
43
43
|
const { deps, noteActivity, routeCtxBase, counters } = ctx;
|
|
44
44
|
const openResponses = new Set();
|
|
45
45
|
let lastAttachEndedAt = 0;
|
|
46
|
+
const openSubscriptions = new Set();
|
|
46
47
|
if (deps.drainState) {
|
|
47
48
|
deps.drainState.attachedStreams = () => {
|
|
48
49
|
let n = 0;
|
|
@@ -52,6 +53,12 @@ export function createDispatch(ctx) {
|
|
|
52
53
|
return n;
|
|
53
54
|
};
|
|
54
55
|
deps.drainState.lastAttachEndedAt = () => lastAttachEndedAt;
|
|
56
|
+
deps.drainState.endSubscriptions = () => {
|
|
57
|
+
const { errorCode, message } = billableRefusalOf("draining").body;
|
|
58
|
+
for (const r of [...openSubscriptions])
|
|
59
|
+
if (isSseResponse(r))
|
|
60
|
+
endSseWithError(r, { errorCode, message });
|
|
61
|
+
};
|
|
55
62
|
}
|
|
56
63
|
async function dispatchSegment(rows, req, res, url, search, method, allow, ctx, admit) {
|
|
57
64
|
for (const row of rows) {
|
|
@@ -82,10 +89,14 @@ export function createDispatch(ctx) {
|
|
|
82
89
|
const allow = { pre: [], post: [] };
|
|
83
90
|
noteActivity(url);
|
|
84
91
|
const admitted = method !== "OPTIONS" && !isProbePath(url);
|
|
85
|
-
|
|
92
|
+
const subscription = admitted && isSubscriptionRequest(method, url);
|
|
93
|
+
const countedInflight = admitted && !subscription;
|
|
94
|
+
if (countedInflight)
|
|
86
95
|
counters.admittedInflight++;
|
|
87
96
|
if (admitted)
|
|
88
97
|
openResponses.add(res);
|
|
98
|
+
if (subscription)
|
|
99
|
+
openSubscriptions.add(res);
|
|
89
100
|
let logged = false;
|
|
90
101
|
const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
|
|
91
102
|
const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
|
|
@@ -93,8 +104,9 @@ export function createDispatch(ctx) {
|
|
|
93
104
|
if (logged)
|
|
94
105
|
return;
|
|
95
106
|
logged = true;
|
|
96
|
-
if (
|
|
107
|
+
if (countedInflight)
|
|
97
108
|
counters.admittedInflight--;
|
|
109
|
+
openSubscriptions.delete(res);
|
|
98
110
|
if (openResponses.delete(res) && isSseResponse(res))
|
|
99
111
|
lastAttachEndedAt = performance.now();
|
|
100
112
|
const route = routeLabel(method, url);
|
|
@@ -116,6 +128,7 @@ export function createDispatch(ctx) {
|
|
|
116
128
|
}
|
|
117
129
|
if (req.method === "GET" && url === "/health") {
|
|
118
130
|
const signal = mintEngineSignal(deps);
|
|
131
|
+
const engineSignal = buildEngineSignal(signal);
|
|
119
132
|
const configHash = createHash("sha256")
|
|
120
133
|
.update(JSON.stringify({ model: deps.config.model.id, sessionBackend: deps.config.sessionBackend, dbBackend: deps.config.dbBackend, dataRoot: deps.config.localDataRoot, configProvider: deps.config.configProvider, remoteExec: deps.config.remoteExec?.provider, port: deps.config.port }))
|
|
121
134
|
.digest("hex")
|
|
@@ -123,7 +136,7 @@ export function createDispatch(ctx) {
|
|
|
123
136
|
sendJson(res, 200, {
|
|
124
137
|
status: "ok",
|
|
125
138
|
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
126
|
-
...(
|
|
139
|
+
...healthDrainingKeys(signal),
|
|
127
140
|
...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
|
|
128
141
|
...(deps.storePosture?.degradedReason !== undefined ? { durable: false, storeDegraded: true } : {}),
|
|
129
142
|
...healthReadinessKeys(signal.verdict),
|
|
@@ -144,18 +157,7 @@ export function createDispatch(ctx) {
|
|
|
144
157
|
const configHandoff = deps.configHandoffState?.();
|
|
145
158
|
return configHandoff ? { configHandoff } : {};
|
|
146
159
|
})(),
|
|
147
|
-
...(
|
|
148
|
-
const gen = deps.configApplyState?.();
|
|
149
|
-
if (!gen)
|
|
150
|
-
return {};
|
|
151
|
-
return {
|
|
152
|
-
configTargetVersion: gen.configTargetVersion,
|
|
153
|
-
configAppliedVersion: gen.configAppliedVersion,
|
|
154
|
-
configTargetOrdinal: gen.configTargetOrdinal,
|
|
155
|
-
configAppliedOrdinal: gen.configAppliedOrdinal,
|
|
156
|
-
...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
|
|
157
|
-
};
|
|
158
|
-
})(),
|
|
160
|
+
...healthConfigLedgerKeys(signal),
|
|
159
161
|
...(() => {
|
|
160
162
|
const credential = deps.centerCredentialState?.();
|
|
161
163
|
return credential ? { centerCredential: credential } : {};
|
|
@@ -173,6 +175,7 @@ export function createDispatch(ctx) {
|
|
|
173
175
|
storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
|
|
174
176
|
};
|
|
175
177
|
})(),
|
|
178
|
+
[ENGINE_SIGNAL_KEY]: engineSignal,
|
|
176
179
|
});
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
* `/health` 排在凭据门之前、缺省全网卡监听:本模块只出闭集词、布尔与运维要发布的 URL。两只 URL 值都过 display-safe 律
|
|
24
24
|
* (`buildDisplaySafeUrl`,所有展示面 URL 的同一个铸点):userinfo 落地、query 值成记号、尾斜杠折掉 —— issuer 的来源
|
|
25
25
|
* (`AUTH_BRIDGE_ISSUER` / `SEMA_REGISTRY_URL`)起服时不判凭据形,这一道是它上无鉴权面的唯一闸。令牌、名单、worker 名、凭证一概不上。
|
|
26
|
+
* 同一张面上本模块之外还有一个**调用方自取**的串(如实,7.123.0 起;`model` / `dataRoot` / `version` 等其余非闭集的名是部署自己的事实,不是调用方放进来的):排空中的动作号 `engineSignal.draining.actionId`(`fleet/engine-signal.ts` 铸,契约 §0.4)——
|
|
27
|
+
* 引擎自起的排空是引擎的闭集词,经排空口(operator-only)进的排空是调用方自取的幂等键**原样**(契约包今天只要求非空串,推荐 `manual:<uuid>`),
|
|
28
|
+
* 所以那一键不是闭集词:调用方不要往里放秘密或人名。
|
|
26
29
|
*
|
|
27
30
|
* ## 在场律
|
|
28
31
|
* 三键**恒在场**;缺席 = 老 worker(≤7.120.x)。`center.issuer` / `center.webBaseUrl` 缺席即缺席(没有鉴权桥 / 没配旋钮),不拼不猜。
|