@sema-agent/server 7.120.0 → 7.122.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 +173 -0
- package/README.md +1 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +97 -29
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
- package/deploy/sema-up/chart/templates/server.yaml +2 -2
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
- package/dist/approval-reconciler.d.ts +8 -0
- package/dist/approval-reconciler.js +72 -52
- package/dist/auto-mode-face.d.ts +9 -2
- package/dist/auto-mode-face.js +5 -3
- package/dist/bench/s1/live-deps.js +1 -0
- package/dist/boot/config-center.d.ts +5 -7
- package/dist/boot/config-center.js +33 -9
- package/dist/boot/engine-lease.d.ts +1 -1
- 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 +2 -4
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +59 -16
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -9
- package/dist/boot/stage-10-http-server.d.ts +1 -9
- package/dist/boot/stage-10-http-server.js +31 -8
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/memory-notice.d.ts +1 -1
- package/dist/config-catalog.js +12 -6
- package/dist/config-center/center-request.d.ts +13 -1
- package/dist/config-center/center-request.js +25 -10
- package/dist/config-types.d.ts +44 -5
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +35 -6
- package/dist/config.js +113 -33
- package/dist/fleet/drain-entry.d.ts +112 -0
- package/dist/fleet/drain-entry.js +38 -0
- package/dist/fleet/engine-signal.d.ts +144 -0
- package/dist/fleet/engine-signal.js +60 -0
- package/dist/fleet/serving-lease.d.ts +47 -0
- package/dist/fleet/serving-lease.js +233 -0
- package/dist/fleet-client.d.ts +35 -19
- package/dist/fleet-client.js +49 -25
- package/dist/governance-overrides.d.ts +7 -2
- package/dist/http/admission.js +7 -5
- package/dist/http/approval-sweeps.js +5 -7
- package/dist/http/checkpoint-ctx.d.ts +60 -11
- package/dist/http/checkpoint-ctx.js +23 -9
- package/dist/http/dispatch.js +33 -16
- package/dist/http/health-self-report.d.ts +60 -0
- package/dist/http/health-self-report.js +53 -0
- package/dist/http/principal-gate.d.ts +8 -1
- package/dist/http/principal-gate.js +1 -0
- package/dist/http/readiness.d.ts +25 -5
- package/dist/http/readiness.js +31 -6
- package/dist/http/resume-legs.js +9 -9
- package/dist/http/route-ctx.d.ts +25 -7
- package/dist/http/route-table.d.ts +6 -0
- package/dist/http/route-table.js +8 -1
- package/dist/http/routes/a2a-serve.d.ts +1 -1
- package/dist/http/routes/a2a-serve.js +5 -5
- package/dist/http/routes/admin-drain.d.ts +15 -31
- package/dist/http/routes/admin-drain.js +28 -10
- package/dist/http/routes/approvals-assistant.d.ts +4 -3
- package/dist/http/routes/approvals-assistant.js +9 -9
- package/dist/http/routes/capabilities.js +7 -5
- package/dist/http/routes/devices.js +3 -2
- 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.d.ts +1 -1
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/memory-consolidation.d.ts +1 -1
- package/dist/http/routes/memory-consolidation.js +1 -1
- package/dist/http/routes/notify-wake.d.ts +1 -1
- package/dist/http/routes/notify-wake.js +1 -1
- package/dist/http/routes/rules.js +4 -3
- package/dist/http/routes/runs.d.ts +4 -2
- package/dist/http/routes/runs.js +7 -6
- package/dist/http/routes/sessions.d.ts +1 -0
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/side-query.d.ts +1 -1
- package/dist/http/routes/side-query.js +1 -1
- package/dist/http/routes/tasks.d.ts +2 -2
- package/dist/http/routes/tasks.js +3 -3
- 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 +10 -21
- package/dist/http/server.js +20 -1
- package/dist/http/stored-body-replay.d.ts +27 -0
- package/dist/http/stored-body-replay.js +23 -0
- package/dist/http/wire-types.d.ts +7 -1
- package/dist/leader/leader.d.ts +22 -1
- package/dist/leader/leader.js +4 -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-reach-scope.d.ts +15 -3
- package/dist/model-reach-scope.js +3 -2
- package/dist/model-route-url.d.ts +13 -20
- package/dist/model-route-url.js +28 -24
- package/dist/model-unreachable-notice.d.ts +2 -1
- package/dist/model-unreachable-notice.js +1 -0
- package/dist/observability/fail-open.d.ts +35 -2
- package/dist/observability/fail-open.js +20 -0
- 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 +14 -1
- package/dist/plugins/park-commit-watch.d.ts +19 -5
- package/dist/plugins/park-commit-watch.js +24 -17
- package/dist/plugins/permission-rule-store-sql.d.ts +2 -1
- package/dist/plugins/remote-env-device.js +0 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/router/route-orchestration.d.ts +4 -2
- package/dist/router/route-orchestration.js +2 -1
- package/dist/rules-consent.d.ts +6 -2
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.js +3 -1
- package/dist/runs.js +1 -1
- package/dist/runtime-governance.d.ts +44 -32
- package/dist/runtime-governance.js +23 -9
- package/dist/sighup-idle.d.ts +7 -1
- package/dist/sighup-idle.js +4 -4
- package/dist/task-settings.d.ts +37 -14
- package/dist/task-settings.js +20 -20
- package/dist/tool-approval.d.ts +13 -0
- package/dist/tool-approval.js +17 -2
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/redact.d.ts +3 -3
- package/dist/trace/self-minted-wire.d.ts +64 -0
- package/dist/trace/self-minted-wire.js +30 -0
- 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
package/dist/fleet-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS } from "@sema-agent/settings-schema/fleet";
|
|
2
|
-
import {
|
|
1
|
+
import { InstanceAnnouncement, UsageReport, DEFAULT_HEARTBEAT_MS, readCapabilityReading } from "@sema-agent/settings-schema/fleet";
|
|
2
|
+
import { drainingFlag, fleetMembershipKeys } from "./fleet/engine-signal.js";
|
|
3
3
|
import { centerFetch, centerLaneConfigured } from "./config-center/center-request.js";
|
|
4
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
4
5
|
export const FLEET_USAGE_KEY_CAP = 1024;
|
|
5
6
|
export const FLEET_CLIENT_FETCH_TIMEOUT_MS = 5000;
|
|
6
7
|
export class FleetUsageAccumulator {
|
|
@@ -74,6 +75,16 @@ async function rejectionSnippet(res) {
|
|
|
74
75
|
return "";
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
async function capabilityReadingOf(res) {
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = await res.json();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return { kind: "unreadable", why: "body", detail: err instanceof Error ? err.message : String(err) };
|
|
85
|
+
}
|
|
86
|
+
return readCapabilityReading(body);
|
|
87
|
+
}
|
|
77
88
|
export function startFleetClient(opts) {
|
|
78
89
|
const f = opts.fetchImpl ?? fetch;
|
|
79
90
|
const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
@@ -81,28 +92,55 @@ export function startFleetClient(opts) {
|
|
|
81
92
|
const send = (route, method, json) => centerFetch(opts.center, { kind: route }, { method, json, timeoutMs: FLEET_CLIENT_FETCH_TIMEOUT_MS, fetchImpl: f });
|
|
82
93
|
let stopped = false;
|
|
83
94
|
let failStreak = 0;
|
|
95
|
+
let unreadableStreak = 0;
|
|
84
96
|
const address = opts.advertiseAddress;
|
|
85
97
|
const announceOnce = async () => {
|
|
86
98
|
if (stopped || address === undefined)
|
|
87
99
|
return;
|
|
88
100
|
try {
|
|
89
|
-
const
|
|
90
|
-
const restart = opts.restart();
|
|
101
|
+
const signal = opts.signal();
|
|
91
102
|
const modelRefusals = opts.modelRefusals();
|
|
92
103
|
const body = {
|
|
93
104
|
instanceId: opts.instanceId,
|
|
94
105
|
worker: opts.center.worker,
|
|
95
106
|
address,
|
|
96
107
|
version: opts.version,
|
|
97
|
-
ready: verdict.ready,
|
|
98
|
-
draining:
|
|
108
|
+
ready: signal.verdict.ready,
|
|
109
|
+
draining: drainingFlag(signal),
|
|
99
110
|
heartbeatMs,
|
|
100
|
-
...(restart !== undefined ? { restart } : {}),
|
|
111
|
+
...(signal.restart !== undefined ? { restart: signal.restart } : {}),
|
|
101
112
|
...(modelRefusals !== undefined ? { modelRefusals } : {}),
|
|
113
|
+
...fleetMembershipKeys(signal),
|
|
102
114
|
};
|
|
103
115
|
const { res } = await send("fleet-instances", "POST", body);
|
|
104
116
|
if (!res.ok)
|
|
105
117
|
throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
|
|
118
|
+
if (opts.onCenterCapabilities !== undefined) {
|
|
119
|
+
const reading = await capabilityReadingOf(res);
|
|
120
|
+
switch (reading.kind) {
|
|
121
|
+
case "declared":
|
|
122
|
+
case "undeclared":
|
|
123
|
+
unreadableStreak = 0;
|
|
124
|
+
opts.onCenterCapabilities(reading.kind === "declared" ? reading.capabilities : undefined);
|
|
125
|
+
break;
|
|
126
|
+
case "unreadable":
|
|
127
|
+
recordFailOpen("server.fleet.announce-reply-unreadable", `why=${reading.why} leaseState=${signal.lease.state}`);
|
|
128
|
+
unreadableStreak += 1;
|
|
129
|
+
if (unreadableStreak === 1) {
|
|
130
|
+
opts.logger?.warn("fleet_announce_reply_unreadable", {
|
|
131
|
+
status: res.status,
|
|
132
|
+
why: reading.why,
|
|
133
|
+
...(reading.detail !== undefined ? { error: reading.detail } : {}),
|
|
134
|
+
note: "the fleet center's heartbeat reply is not a capability reading (unreadable body, not an object, or a malformed capabilities value) — no reading taken; the last one stands (the serving lease keeps its state and outage clock)",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
default: {
|
|
139
|
+
const unhandled = reading;
|
|
140
|
+
throw new Error(`heartbeat reply: unknown capability reading ${JSON.stringify(unhandled)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
106
144
|
if (failStreak > 0)
|
|
107
145
|
opts.logger?.info("fleet_announce_recovered", { after: failStreak });
|
|
108
146
|
failStreak = 0;
|
|
@@ -156,7 +194,7 @@ export function startFleetClient(opts) {
|
|
|
156
194
|
};
|
|
157
195
|
const announcing = address !== undefined;
|
|
158
196
|
if (announcing)
|
|
159
|
-
void announceNow();
|
|
197
|
+
queueMicrotask(() => void announceNow());
|
|
160
198
|
const hb = announcing ? setInterval(() => void announceNow(), heartbeatMs) : undefined;
|
|
161
199
|
hb?.unref?.();
|
|
162
200
|
const uw = opts.usage ? setInterval(() => void flushUsage(), usageWindowMs) : undefined;
|
|
@@ -190,29 +228,15 @@ export function startFleetClient(opts) {
|
|
|
190
228
|
};
|
|
191
229
|
return { announceNow, stopRouting, flushFinalUsage };
|
|
192
230
|
}
|
|
193
|
-
export function
|
|
231
|
+
export function startFleetClientFromConfig(cfg, opts) {
|
|
194
232
|
const cc = cfg.configCenter;
|
|
195
|
-
const
|
|
196
|
-
const address = raw === undefined || raw === "" ? undefined : raw;
|
|
197
|
-
if (address !== undefined && cc?.baseUrl && cc.credential.declared) {
|
|
198
|
-
let parsed;
|
|
199
|
-
try {
|
|
200
|
-
parsed = new URL(address);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
parsed = undefined;
|
|
204
|
-
}
|
|
205
|
-
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
206
|
-
throw new Error(`FLEET_ADVERTISE_ADDRESS must be a parseable http(s) base URL (e.g. "http://10.0.0.5:8090"); got "${buildDisplaySafeUrl(address)}" — the fleet center rejects any other shape on every announce`);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
233
|
+
const address = cfg.fleetAdvertiseAddress;
|
|
209
234
|
if (!centerLaneConfigured(cfg)) {
|
|
210
235
|
if (address !== undefined && cc?.baseUrl && cc.credential.declared) {
|
|
211
236
|
opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
|
|
212
237
|
}
|
|
213
238
|
return undefined;
|
|
214
239
|
}
|
|
215
|
-
|
|
216
|
-
return startFleetClient({ ...opts, center: { baseUrl: lane.baseUrl, credential: lane.credential, worker: lane.worker }, advertiseAddress: address });
|
|
240
|
+
return startFleetClient({ ...opts, center: cfg.configCenter, advertiseAddress: address });
|
|
217
241
|
}
|
|
218
242
|
//# sourceMappingURL=fleet-client.js.map
|
|
@@ -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) {
|
|
@@ -6,9 +6,9 @@ import { APPROVAL_GATE_KINDS } from "../tool-approval.js";
|
|
|
6
6
|
import { createWallClockJumpGuard } from "../wall-clock-jump-guard.js";
|
|
7
7
|
import { hasServiceAuth } from "./dispatch.js";
|
|
8
8
|
import { gatedPrincipal } from "./principal-gate.js";
|
|
9
|
-
import {
|
|
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 = [];
|
|
@@ -107,11 +107,9 @@ export function createApprovalSweeps(ctx) {
|
|
|
107
107
|
if (!hasServiceAuth(deps.config) && !deps.config.allowUnauthedWrites) {
|
|
108
108
|
return { status: 503, body: { error: "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions", errorCode: "auth.service_token_required" } };
|
|
109
109
|
}
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
const refusal = billableRefusalOf(readiness, deps.drainState?.reason);
|
|
110
|
+
const refusal = admissionRefusalOf(readinessOf(deps), deps.servingLease?.(), "hitl-answer");
|
|
111
|
+
if (refusal !== undefined)
|
|
113
112
|
return { status: refusal.status, body: refusal.body };
|
|
114
|
-
}
|
|
115
113
|
if (deps.costQuota && req.httpReq) {
|
|
116
114
|
const quotaPrincipal = gatedPrincipal(req.httpReq, deps.config);
|
|
117
115
|
if (quotaPrincipal !== undefined) {
|
|
@@ -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,9 +3,11 @@ 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, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
|
|
7
|
-
import { sendJson, sendError, isSseResponse } from "./send.js";
|
|
8
|
-
import { readinessOf, healthReadinessKeys, sendReadyz,
|
|
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 { fleetMembershipKeys, mintEngineSignal, restartActionable } from "../fleet/engine-signal.js";
|
|
10
|
+
import { buildHealthSelfReport } from "./health-self-report.js";
|
|
9
11
|
import { authorized, systemFor, gatedPrincipal, sendPrincipalRefusal, sendUnauthorized, verifiedIdentitySource } from "./principal-gate.js";
|
|
10
12
|
const PROCESS_STARTED_AT_MS = Date.now() - Math.round(process.uptime() * 1000);
|
|
11
13
|
export function hasServiceAuth(config) {
|
|
@@ -41,6 +43,7 @@ export function createDispatch(ctx) {
|
|
|
41
43
|
const { deps, noteActivity, routeCtxBase, counters } = ctx;
|
|
42
44
|
const openResponses = new Set();
|
|
43
45
|
let lastAttachEndedAt = 0;
|
|
46
|
+
const openSubscriptions = new Set();
|
|
44
47
|
if (deps.drainState) {
|
|
45
48
|
deps.drainState.attachedStreams = () => {
|
|
46
49
|
let n = 0;
|
|
@@ -50,6 +53,12 @@ export function createDispatch(ctx) {
|
|
|
50
53
|
return n;
|
|
51
54
|
};
|
|
52
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
|
+
};
|
|
53
62
|
}
|
|
54
63
|
async function dispatchSegment(rows, req, res, url, search, method, allow, ctx, admit) {
|
|
55
64
|
for (const row of rows) {
|
|
@@ -80,10 +89,14 @@ export function createDispatch(ctx) {
|
|
|
80
89
|
const allow = { pre: [], post: [] };
|
|
81
90
|
noteActivity(url);
|
|
82
91
|
const admitted = method !== "OPTIONS" && !isProbePath(url);
|
|
83
|
-
|
|
92
|
+
const subscription = admitted && isSubscriptionRequest(method, url);
|
|
93
|
+
const countedInflight = admitted && !subscription;
|
|
94
|
+
if (countedInflight)
|
|
84
95
|
counters.admittedInflight++;
|
|
85
96
|
if (admitted)
|
|
86
97
|
openResponses.add(res);
|
|
98
|
+
if (subscription)
|
|
99
|
+
openSubscriptions.add(res);
|
|
87
100
|
let logged = false;
|
|
88
101
|
const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
|
|
89
102
|
const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
|
|
@@ -91,8 +104,9 @@ export function createDispatch(ctx) {
|
|
|
91
104
|
if (logged)
|
|
92
105
|
return;
|
|
93
106
|
logged = true;
|
|
94
|
-
if (
|
|
107
|
+
if (countedInflight)
|
|
95
108
|
counters.admittedInflight--;
|
|
109
|
+
openSubscriptions.delete(res);
|
|
96
110
|
if (openResponses.delete(res) && isSseResponse(res))
|
|
97
111
|
lastAttachEndedAt = performance.now();
|
|
98
112
|
const route = routeLabel(method, url);
|
|
@@ -113,7 +127,7 @@ export function createDispatch(ctx) {
|
|
|
113
127
|
return;
|
|
114
128
|
}
|
|
115
129
|
if (req.method === "GET" && url === "/health") {
|
|
116
|
-
const
|
|
130
|
+
const signal = mintEngineSignal(deps);
|
|
117
131
|
const configHash = createHash("sha256")
|
|
118
132
|
.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 }))
|
|
119
133
|
.digest("hex")
|
|
@@ -121,24 +135,26 @@ export function createDispatch(ctx) {
|
|
|
121
135
|
sendJson(res, 200, {
|
|
122
136
|
status: "ok",
|
|
123
137
|
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
124
|
-
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {})
|
|
138
|
+
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}) } : {}),
|
|
125
139
|
...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
|
|
126
140
|
...(deps.storePosture?.degradedReason !== undefined ? { durable: false, storeDegraded: true } : {}),
|
|
127
|
-
...healthReadinessKeys(
|
|
141
|
+
...healthReadinessKeys(signal.verdict),
|
|
128
142
|
model: deps.config.model.id,
|
|
129
143
|
sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
|
|
130
144
|
sessions: deps.runner.sessions.size,
|
|
131
145
|
...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
|
|
146
|
+
...fleetMembershipKeys(signal),
|
|
132
147
|
...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
|
|
133
148
|
configHash,
|
|
134
149
|
pid: process.pid,
|
|
135
150
|
startedAt: PROCESS_STARTED_AT_MS,
|
|
136
151
|
port: deps.config.port,
|
|
137
152
|
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
138
|
-
...(
|
|
153
|
+
...buildHealthSelfReport(deps, hasServiceAuth(deps.config)),
|
|
154
|
+
...(signal.restart ? { ...(restartActionable(signal) ? { restartRequired: true } : {}), restart: signal.restart } : {}),
|
|
139
155
|
...(() => {
|
|
140
|
-
const
|
|
141
|
-
return
|
|
156
|
+
const configHandoff = deps.configHandoffState?.();
|
|
157
|
+
return configHandoff ? { configHandoff } : {};
|
|
142
158
|
})(),
|
|
143
159
|
...(() => {
|
|
144
160
|
const gen = deps.configApplyState?.();
|
|
@@ -173,7 +189,7 @@ export function createDispatch(ctx) {
|
|
|
173
189
|
return;
|
|
174
190
|
}
|
|
175
191
|
if (req.method === "GET" && url === "/readyz") {
|
|
176
|
-
sendReadyz(res,
|
|
192
|
+
sendReadyz(res, mintEngineSignal(deps).verdict);
|
|
177
193
|
return;
|
|
178
194
|
}
|
|
179
195
|
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
@@ -244,10 +260,11 @@ export function createDispatch(ctx) {
|
|
|
244
260
|
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
|
|
245
261
|
return;
|
|
246
262
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
const billableKind = req.method === "POST" && !isMethodDispatchedSubmitPath(url) ? billableKindOf(url) : undefined;
|
|
264
|
+
if (billableKind !== undefined) {
|
|
265
|
+
const refusal = admissionRefusalOf(readinessOf(deps), deps.servingLease?.(), billableKind);
|
|
266
|
+
if (refusal !== undefined) {
|
|
267
|
+
sendBillableRefusal(res, refusal);
|
|
251
268
|
return;
|
|
252
269
|
}
|
|
253
270
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /health` 的**自报位**(7.121.0)—— `auth` / `operator` / `center` 三键:壳在**带凭据之前**要知道的三件部署事实
|
|
3
|
+
* (这台引擎要不要凭据、认哪几类、能不能附着身份;有没有运维方可找;接着哪个中心、会话会不会出现在那个中心的 web 面)。
|
|
4
|
+
*
|
|
5
|
+
* ## 规则(一条):自报位是**投影**,不是第二份规则
|
|
6
|
+
* 每一位读的是**门本身读的那只谓词 / 那只取值口**,本模块不判任何东西:
|
|
7
|
+
* · `auth.required` = 全局凭据门的武装谓词(`hasServiceAuth`,由 `dispatch.ts` 交进来 —— 门与本位读同一次求值的同一只函数);
|
|
8
|
+
* · `auth.kinds` = {@link IDENTITY_SOURCES}(`verifiedIdentitySource` 的回值闭集)里,这台部署的装配**能**回的那几个词:
|
|
9
|
+
* `service_token` ⇐ 全局门武装(`systemFor` 只在配了 service token 时回得出值)· `sso` ⇐ 鉴权桥装了(入口预认证只在
|
|
10
|
+
* `registryJwtVerifier` 在场时挂身份)· `direct_door_jwt` ⇐ 直连门开着(`verifiedIdentitySource` 那一臂的第一个合取项);
|
|
11
|
+
* · `auth.identityAttach` = kinds 里有没有**带调用方身份**的一类({@link carriesCallerIdentity},穷举):`service_token` 认的是系统
|
|
12
|
+
* (`systemFor` 回系统名或 null),不是人;
|
|
13
|
+
* · `operator` = 能力事实 `operatorLane`(operator 面族的 501 门读的同一只;名单本身不上 wire);
|
|
14
|
+
* · `center.issuer` = 鉴权桥 issuer 的唯一取值 `authBridgeIssuerOf`(装配 `boot/stage-09-leader.ts` 与 caps `centerWiring.issuer` 同读);
|
|
15
|
+
* · `center.webBaseUrl` = 运维旋钮 `SEMA_REGISTRY_WEB_URL`(`config.configCenter.webBaseUrl`,接了中心才挂;起服时已判成固定基址);
|
|
16
|
+
* · `center.sessionsVisible` = 中心的 web 面能不能列出本引擎的会话 —— 四件接线事实全在才可能:中心车道配齐(`centerLaneConfigured`,
|
|
17
|
+
* 租约 / 用量 / 公告同一只谓词)∧ 公告地址在(配置席 `fleetAdvertiseAddress`:web 面只拨中心登记过的实例地址)∧ 鉴权桥在(web 面带的是
|
|
18
|
+
* 中心铸的中继令牌,不带 service token)∧ 会话枚举面在(事实 `sessionList`,web 面中继 `GET /v1/sessions`);缺任一 ⇒ `false`。
|
|
19
|
+
* 四件齐时:多租户(`REQUIRE_PRINCIPAL`)每条会话都落着验过的属主 ⇒ `true`;单用户形下一条会话归谁取决于提交它的那次请求带没带
|
|
20
|
+
* 身份(web 面只列调用人自己的会话)—— 部署级答不了 ⇒ `"unknown"`。
|
|
21
|
+
*
|
|
22
|
+
* ## 无鉴权面
|
|
23
|
+
* `/health` 排在凭据门之前、缺省全网卡监听:本模块只出闭集词、布尔与运维要发布的 URL。两只 URL 值都过 display-safe 律
|
|
24
|
+
* (`buildDisplaySafeUrl`,所有展示面 URL 的同一个铸点):userinfo 落地、query 值成记号、尾斜杠折掉 —— issuer 的来源
|
|
25
|
+
* (`AUTH_BRIDGE_ISSUER` / `SEMA_REGISTRY_URL`)起服时不判凭据形,这一道是它上无鉴权面的唯一闸。令牌、名单、worker 名、凭证一概不上。
|
|
26
|
+
*
|
|
27
|
+
* ## 在场律
|
|
28
|
+
* 三键**恒在场**;缺席 = 老 worker(≤7.120.x)。`center.issuer` / `center.webBaseUrl` 缺席即缺席(没有鉴权桥 / 没配旋钮),不拼不猜。
|
|
29
|
+
*/
|
|
30
|
+
import type { FlatServiceDeps } from "./server.js";
|
|
31
|
+
import { type IdentitySource } from "./principal-gate.js";
|
|
32
|
+
/** `auth.identityAttach` 的词表。 */
|
|
33
|
+
export declare const IDENTITY_ATTACH_WORDS: readonly ["available", "unavailable"];
|
|
34
|
+
export type IdentityAttach = (typeof IDENTITY_ATTACH_WORDS)[number];
|
|
35
|
+
/** `operator` 的词表。 */
|
|
36
|
+
export declare const OPERATOR_PRESENCE_WORDS: readonly ["present", "absent"];
|
|
37
|
+
export type OperatorPresence = (typeof OPERATOR_PRESENCE_WORDS)[number];
|
|
38
|
+
/** `center.sessionsVisible` 的值域(两个布尔 + 一个词;`"unknown"` = 接线齐了,但会话归谁要看提交它的那次请求)。 */
|
|
39
|
+
export declare const SESSIONS_VISIBLE_VALUES: readonly [true, false, "unknown"];
|
|
40
|
+
export type SessionsVisible = (typeof SESSIONS_VISIBLE_VALUES)[number];
|
|
41
|
+
/** 三键的 wire 形(纯数据)。 */
|
|
42
|
+
export interface HealthSelfReport {
|
|
43
|
+
readonly auth: {
|
|
44
|
+
readonly required: boolean;
|
|
45
|
+
readonly kinds: readonly IdentitySource[];
|
|
46
|
+
readonly identityAttach: IdentityAttach;
|
|
47
|
+
};
|
|
48
|
+
readonly operator: OperatorPresence;
|
|
49
|
+
readonly center: {
|
|
50
|
+
readonly issuer?: string;
|
|
51
|
+
readonly webBaseUrl?: string;
|
|
52
|
+
readonly sessionsVisible: SessionsVisible;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 三键的**唯一铸点**(`build*`:纯数据)。`credentialGateArmed` 由 `dispatch.ts` 交进来 —— 它是全局凭据门自己的那一句
|
|
57
|
+
* (`hasServiceAuth`),本模块不另求一遍(另求就是第二个写者)。
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildHealthSelfReport(deps: Pick<FlatServiceDeps, "config" | "registryJwtVerifier" | "sessionStorage" | "runStore">, credentialGateArmed: boolean): HealthSelfReport;
|
|
60
|
+
//# sourceMappingURL=health-self-report.d.ts.map
|