@sema-agent/server 7.119.0 → 7.121.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 +129 -0
- package/USAGE.md +72 -23
- 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/boot/config-center.d.ts +5 -7
- package/dist/boot/config-center.js +37 -13
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.js +3 -2
- package/dist/boot/fatal-exit.d.ts +7 -1
- package/dist/boot/fatal-exit.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/runner-deps.d.ts +4 -3
- package/dist/boot/runner-deps.js +7 -3
- package/dist/boot/shutdown.d.ts +9 -10
- package/dist/boot/shutdown.js +54 -16
- package/dist/boot/stage-01-config.js +2 -0
- 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/builtin-tools.d.ts +31 -8
- package/dist/capabilities/builtin-tools.js +22 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/capabilities/scenarios.d.ts +8 -6
- package/dist/capabilities/scenarios.js +3 -3
- package/dist/capabilities/send-user-file-tool.d.ts +6 -3
- package/dist/capabilities/send-user-file-tool.js +1 -1
- package/dist/config-catalog.js +13 -7
- package/dist/config-center/center-request.d.ts +16 -1
- package/dist/config-center/center-request.js +36 -5
- package/dist/config-lkg.d.ts +33 -5
- package/dist/config-lkg.js +55 -11
- package/dist/config-types.d.ts +46 -3
- package/dist/config-types.js +6 -0
- package/dist/config.d.ts +33 -4
- package/dist/config.js +107 -15
- package/dist/deployment-governance.d.ts +3 -1
- package/dist/deployment-governance.js +3 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +4 -0
- package/dist/device-ws-hub.js +17 -9
- package/dist/device-ws-protocol.d.ts +15 -0
- package/dist/fleet/drain-entry.d.ts +87 -0
- package/dist/fleet/drain-entry.js +35 -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 +235 -0
- package/dist/fleet-client.d.ts +35 -19
- package/dist/fleet-client.js +49 -25
- package/dist/http/approval-sweeps.js +3 -5
- package/dist/http/dispatch.js +18 -13
- 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 +5 -1
- package/dist/http/route-ctx.d.ts +6 -4
- package/dist/http/route-table.d.ts +3 -0
- package/dist/http/route-table.js +4 -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 +12 -31
- package/dist/http/routes/admin-drain.js +15 -9
- package/dist/http/routes/approvals-assistant.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +3 -3
- package/dist/http/routes/capabilities.js +3 -1
- package/dist/http/routes/devices.js +6 -5
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/leader.js +1 -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 +14 -6
- package/dist/http/routes/runs.d.ts +2 -2
- package/dist/http/routes/runs.js +2 -2
- 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/server.d.ts +7 -21
- package/dist/http/server.js +20 -1
- package/dist/http/wire-types.d.ts +7 -1
- package/dist/list-entry-display.d.ts +14 -0
- package/dist/list-entry-display.js +14 -0
- package/dist/model-reach-scope.d.ts +15 -3
- package/dist/model-reach-scope.js +3 -2
- package/dist/model-unreachable-notice.d.ts +2 -1
- package/dist/model-unreachable-notice.js +1 -0
- package/dist/namespaced-rule-name.js +2 -6
- package/dist/observability/fail-open.d.ts +20 -0
- package/dist/observability/fail-open.js +20 -0
- package/dist/operator-ask.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +8 -0
- package/dist/plugins/one-time-migrations.js +72 -3
- package/dist/plugins/permission-rule-store-file.js +2 -6
- package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
- package/dist/plugins/permission-rule-store-sql.js +166 -59
- package/dist/plugins/remote-env-device.d.ts +12 -1
- package/dist/plugins/remote-env-device.js +13 -6
- package/dist/plugins/scheduler-support.d.ts +8 -4
- package/dist/plugins/scheduler-support.js +5 -5
- package/dist/request-key-closure.js +1 -1
- package/dist/rule-entry-list.d.ts +0 -1
- package/dist/rule-entry-list.js +4 -9
- package/dist/rules-consent.d.ts +16 -3
- package/dist/rules-consent.js +8 -4
- package/dist/run-local.js +2 -0
- package/dist/runs.js +1 -1
- package/dist/sema-config-dir.d.ts +19 -0
- package/dist/sema-config-dir.js +33 -0
- package/dist/session-policy-wire.js +5 -4
- package/dist/spec-fields.d.ts +5 -4
- package/dist/task-settings.d.ts +3 -3
- package/dist/tool-approval.d.ts +7 -0
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +16 -9
- package/dist/trace/self-minted-wire.d.ts +78 -0
- package/dist/trace/self-minted-wire.js +32 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
- package/package.json +3 -3
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
|
|
@@ -6,7 +6,7 @@ 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
11
|
import { storedBodyReplayRefusal } from "./checkpoint-ctx.js";
|
|
12
12
|
const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
|
|
@@ -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) {
|
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";
|
|
6
|
+
import { READ_CREDENTIAL_ROUTES, PUBLIC_ROUTES, POST_CREDENTIAL_ROUTES, routeLabel, isBillableSubmitPath, billableKindOf, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, lookupRoute, isProbePath } from "./route-table.js";
|
|
7
7
|
import { sendJson, sendError, isSseResponse } from "./send.js";
|
|
8
|
-
import { readinessOf, healthReadinessKeys, sendReadyz,
|
|
8
|
+
import { admissionRefusalOf, readinessOf, healthReadinessKeys, sendReadyz, sendBillableRefusal } 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) {
|
|
@@ -113,7 +115,7 @@ export function createDispatch(ctx) {
|
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
if (req.method === "GET" && url === "/health") {
|
|
116
|
-
const
|
|
118
|
+
const signal = mintEngineSignal(deps);
|
|
117
119
|
const configHash = createHash("sha256")
|
|
118
120
|
.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
121
|
.digest("hex")
|
|
@@ -121,24 +123,26 @@ export function createDispatch(ctx) {
|
|
|
121
123
|
sendJson(res, 200, {
|
|
122
124
|
status: "ok",
|
|
123
125
|
...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
|
|
124
|
-
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {})
|
|
126
|
+
...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}) } : {}),
|
|
125
127
|
...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
|
|
126
128
|
...(deps.storePosture?.degradedReason !== undefined ? { durable: false, storeDegraded: true } : {}),
|
|
127
|
-
...healthReadinessKeys(
|
|
129
|
+
...healthReadinessKeys(signal.verdict),
|
|
128
130
|
model: deps.config.model.id,
|
|
129
131
|
sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
|
|
130
132
|
sessions: deps.runner.sessions.size,
|
|
131
133
|
...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
|
|
134
|
+
...fleetMembershipKeys(signal),
|
|
132
135
|
...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
|
|
133
136
|
configHash,
|
|
134
137
|
pid: process.pid,
|
|
135
138
|
startedAt: PROCESS_STARTED_AT_MS,
|
|
136
139
|
port: deps.config.port,
|
|
137
140
|
...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
|
|
138
|
-
...(
|
|
141
|
+
...buildHealthSelfReport(deps, hasServiceAuth(deps.config)),
|
|
142
|
+
...(signal.restart ? { ...(restartActionable(signal) ? { restartRequired: true } : {}), restart: signal.restart } : {}),
|
|
139
143
|
...(() => {
|
|
140
|
-
const
|
|
141
|
-
return
|
|
144
|
+
const configHandoff = deps.configHandoffState?.();
|
|
145
|
+
return configHandoff ? { configHandoff } : {};
|
|
142
146
|
})(),
|
|
143
147
|
...(() => {
|
|
144
148
|
const gen = deps.configApplyState?.();
|
|
@@ -173,7 +177,7 @@ export function createDispatch(ctx) {
|
|
|
173
177
|
return;
|
|
174
178
|
}
|
|
175
179
|
if (req.method === "GET" && url === "/readyz") {
|
|
176
|
-
sendReadyz(res,
|
|
180
|
+
sendReadyz(res, mintEngineSignal(deps).verdict);
|
|
177
181
|
return;
|
|
178
182
|
}
|
|
179
183
|
if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
|
|
@@ -244,10 +248,11 @@ export function createDispatch(ctx) {
|
|
|
244
248
|
sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
|
|
245
249
|
return;
|
|
246
250
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
const billableKind = req.method === "POST" && !isMethodDispatchedSubmitPath(url) ? billableKindOf(url) : undefined;
|
|
252
|
+
if (billableKind !== undefined) {
|
|
253
|
+
const refusal = admissionRefusalOf(readinessOf(deps), deps.servingLease?.(), billableKind);
|
|
254
|
+
if (refusal !== undefined) {
|
|
255
|
+
sendBillableRefusal(res, refusal);
|
|
251
256
|
return;
|
|
252
257
|
}
|
|
253
258
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { IDENTITY_SOURCES } from "./principal-gate.js";
|
|
2
|
+
import { operatorLane, sessionList } from "../capability-facts.js";
|
|
3
|
+
import { authBridgeIssuerOf } from "../auth-bridge.js";
|
|
4
|
+
import { centerLaneConfigured } from "../config-center/center-request.js";
|
|
5
|
+
import { buildDisplaySafeUrl } from "../trace/redact.js";
|
|
6
|
+
export const IDENTITY_ATTACH_WORDS = ["available", "unavailable"];
|
|
7
|
+
export const OPERATOR_PRESENCE_WORDS = ["present", "absent"];
|
|
8
|
+
export const SESSIONS_VISIBLE_VALUES = [true, false, "unknown"];
|
|
9
|
+
function carriesCallerIdentity(kind) {
|
|
10
|
+
switch (kind) {
|
|
11
|
+
case "service_token":
|
|
12
|
+
return false;
|
|
13
|
+
case "sso":
|
|
14
|
+
case "direct_door_jwt":
|
|
15
|
+
return true;
|
|
16
|
+
default: {
|
|
17
|
+
const unknownKind = kind;
|
|
18
|
+
throw new Error(`health self-report: unknown identity source ${String(unknownKind)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function identitySourceWired(kind, a) {
|
|
23
|
+
switch (kind) {
|
|
24
|
+
case "service_token":
|
|
25
|
+
return a.credentialGateArmed;
|
|
26
|
+
case "sso":
|
|
27
|
+
return a.bridgeWired;
|
|
28
|
+
case "direct_door_jwt":
|
|
29
|
+
return a.directDoorActive;
|
|
30
|
+
default: {
|
|
31
|
+
const unknownKind = kind;
|
|
32
|
+
throw new Error(`health self-report: unknown identity source ${String(unknownKind)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function buildHealthSelfReport(deps, credentialGateArmed) {
|
|
37
|
+
const { config } = deps;
|
|
38
|
+
const bridgeWired = deps.registryJwtVerifier !== undefined;
|
|
39
|
+
const kinds = IDENTITY_SOURCES.filter((k) => identitySourceWired(k, { credentialGateArmed, bridgeWired, directDoorActive: config.directDoorActive }));
|
|
40
|
+
const issuer = authBridgeIssuerOf(config)?.issuer;
|
|
41
|
+
const webBaseUrl = config.configCenter?.webBaseUrl;
|
|
42
|
+
const sessionsVisible = centerLaneConfigured(config) && config.fleetAdvertiseAddress !== undefined && bridgeWired && sessionList(deps).available ? (config.requirePrincipal ? true : "unknown") : false;
|
|
43
|
+
return {
|
|
44
|
+
auth: { required: credentialGateArmed, kinds, identityAttach: kinds.some(carriesCallerIdentity) ? "available" : "unavailable" },
|
|
45
|
+
operator: operatorLane(deps).available ? "present" : "absent",
|
|
46
|
+
center: {
|
|
47
|
+
...(issuer !== undefined ? { issuer: buildDisplaySafeUrl(issuer) } : {}),
|
|
48
|
+
...(webBaseUrl !== undefined ? { webBaseUrl: buildDisplaySafeUrl(webBaseUrl) } : {}),
|
|
49
|
+
sessionsVisible,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=health-self-report.js.map
|
|
@@ -13,13 +13,20 @@ export declare function systemFor(req: IncomingMessage, config: ServiceConfig):
|
|
|
13
13
|
export declare function bearerPresentedButUnverified(req: IncomingMessage, config: ServiceConfig): boolean;
|
|
14
14
|
/** loopback 三形(IPv4 / IPv6 / IPv4-mapped)—— 一处定义,diagnostics 门与任何「本机自用」判据同吃。缺席 ⇒ false(fail-closed)。 */
|
|
15
15
|
export declare function isLoopbackAddress(addr: string | undefined): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* 验过的身份来源的**闭集**(序 = `/health.auth.kinds` 的出现序)。{@link verifiedIdentitySource} 的回值就是这张表的词;
|
|
18
|
+
* `/health` 的自报位(`http/health-self-report.ts`)读同一张表,答「这台部署的装配**能**回哪几个词」—— 加一个来源 = 加一个词,
|
|
19
|
+
* 两边各自的穷举 `switch` 不接就编译红。
|
|
20
|
+
*/
|
|
21
|
+
export declare const IDENTITY_SOURCES: readonly ["service_token", "sso", "direct_door_jwt"];
|
|
22
|
+
export type IdentitySource = (typeof IDENTITY_SOURCES)[number];
|
|
16
23
|
/**
|
|
17
24
|
* 这次请求的身份是不是**密码学验过**的(而不是调用方自述的 header):`sso` = registry JWT(入口预认证的签名真源)/
|
|
18
25
|
* `direct_door_jwt` = direct-door principal JWT 验过 / `service_token` = 出示了单一或 per-system service token。
|
|
19
26
|
* 缺席 = 只有自述头或什么都没有。一处判定(7.63.0 codex [high]+[medium]):off-box 读 operator 面的门用它——名单只在
|
|
20
27
|
* 有可验身份时才是身份,零凭据下它只是谁都能写的字符串;而 JWT 身份不依赖静态 token 的在场。
|
|
21
28
|
*/
|
|
22
|
-
export declare function verifiedIdentitySource(req: IncomingMessage, config: ServiceConfig):
|
|
29
|
+
export declare function verifiedIdentitySource(req: IncomingMessage, config: ServiceConfig): IdentitySource | undefined;
|
|
23
30
|
export declare function gatedPrincipal(req: IncomingMessage, config: ServiceConfig): string | undefined;
|
|
24
31
|
/**
|
|
25
32
|
* 「**出示了凭证但验不过** ≠ **没出示**」在 **direct-door principal JWT** 面上的形 —— 本仓
|
|
@@ -37,6 +37,7 @@ export function bearerPresentedButUnverified(req, config) {
|
|
|
37
37
|
export function isLoopbackAddress(addr) {
|
|
38
38
|
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
|
|
39
39
|
}
|
|
40
|
+
export const IDENTITY_SOURCES = ["service_token", "sso", "direct_door_jwt"];
|
|
40
41
|
export function verifiedIdentitySource(req, config) {
|
|
41
42
|
if (ssoVerifiedPrincipal(req))
|
|
42
43
|
return "sso";
|
package/dist/http/readiness.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* 模块形:纯叶(只值 import `send.ts`)—— `fleet-client.ts` / `boot/` 都会读它,不许长出回环。
|
|
23
23
|
*/
|
|
24
24
|
import type { ServerResponse } from "node:http";
|
|
25
|
+
import type { ServingLeaseView } from "../fleet/engine-signal.js";
|
|
25
26
|
/**
|
|
26
27
|
* 就绪原因闭集,**数组序 = 优先序**:`/health.readyReason` 取首条的句、计费门取首条的拒绝码。
|
|
27
28
|
* draining 在前 —— 一台正在下线的副本「去找替补」(Retry-After 15)比「稍后重试本机」(5)更对。
|
|
@@ -61,6 +62,26 @@ export declare function healthReadinessKeys(v: ReadinessVerdict): Record<string,
|
|
|
61
62
|
/** `/readyz` 两形:200 `{ready:true}` / 503 `{ready:false, reasons}` + `Retry-After: 5`。体只这两键(不带身份字段:
|
|
62
63
|
* 就绪探针给 k8s readinessProbe / Traefik / LB 摘流用,不是 `/health` 的替身;`drainReason` 等运维自由文本也不上这一面)。 */
|
|
63
64
|
export declare function sendReadyz(res: ServerResponse, v: ReadinessVerdict): void;
|
|
65
|
+
/**
|
|
66
|
+
* 编排器片 2 件 2 —— 计费面请求的**种类**(闭集;路由声明表 `billable` 列的取值,`http/route-ctx.ts`):
|
|
67
|
+
* · `submit` —— 起一轮**新**工作(新任务 / 新 run / leader / side-query / wake / 记忆整理 / A2A `message/send`);
|
|
68
|
+
* · `hitl-answer` —— 对一张已经在等人的卡作答(耐久 `/decide`、计划审阅、PARKED 停驻卡的赎回);
|
|
69
|
+
* · `resume` —— 续跑一条已经存在的 run(助手 resume、子代 resume)。
|
|
70
|
+
*/
|
|
71
|
+
export declare const BILLABLE_KINDS: readonly ["submit", "hitl-answer", "resume"];
|
|
72
|
+
export type BillableKind = (typeof BILLABLE_KINDS)[number];
|
|
73
|
+
/**
|
|
74
|
+
* 计费面的拒因(**数组序 = 优先序**):就绪原因({@link READINESS_REASONS})∪ `lease_expired`(服务租约过期且中心不可达满 `T_max`)。
|
|
75
|
+
* `lease_expired` 刻意不是就绪原因:它不翻 `/readyz`(中心不可达不是本实例的病,摘它出路由只会把取消 / HITL / 读面一起摘掉)。
|
|
76
|
+
*/
|
|
77
|
+
export declare const ADMISSION_REFUSALS: readonly ["draining", "model_roster_pending", "lease_expired"];
|
|
78
|
+
export type AdmissionRefusal = (typeof ADMISSION_REFUSALS)[number];
|
|
79
|
+
/**
|
|
80
|
+
* 计费面请求的**唯一**准入判(dispatch 的计费门、a2a `message/send`、PARKED 赎回席同调):按 {@link ADMISSION_REFUSALS} 序找第一条
|
|
81
|
+
* 成立且按 {@link ADMISSION_TABLE} 拒这一种的拒因;没有 ⇒ `undefined`(放行)。入参是**判决**而不是 deps:就绪判由调用点经唯一谓词
|
|
82
|
+
* {@link readinessOf} 现读(模块边界上那一只 —— 谓词单源钉注入它,任何读点自己读 `drainState` 都会当场红),租约读数取 `deps.servingLease`。
|
|
83
|
+
*/
|
|
84
|
+
export declare function admissionRefusalOf(verdict: ReadinessVerdict, lease: ServingLeaseView | undefined, kind: BillableKind): BillableRefusal | undefined;
|
|
64
85
|
/** 计费提交门的拒绝(首条 reason 定码)。`retryAfterSec` 由发送形(`sendBillableRefusal`)镜像成头;交回
|
|
65
86
|
* `{status, body}` 的赎回席只取体(它的路由刻意不带头,见 `approval-sweeps.ts` 该臂注)。 */
|
|
66
87
|
export interface BillableRefusal {
|
|
@@ -69,17 +90,16 @@ export interface BillableRefusal {
|
|
|
69
90
|
readonly error: string;
|
|
70
91
|
readonly errorCode: string;
|
|
71
92
|
readonly message: string;
|
|
72
|
-
readonly reason?: string;
|
|
73
93
|
};
|
|
74
|
-
readonly retryAfterSec: 15 | 5;
|
|
94
|
+
readonly retryAfterSec: 15 | 5 | 60;
|
|
75
95
|
}
|
|
76
96
|
/**
|
|
77
97
|
* 计费提交门的拒绝体 —— **唯一铸点**(`error` 字面 `"draining"` / `"model_roster_pending"` 与 `message` 是冻结的
|
|
78
98
|
* wire 契约:SDK / 壳的 pre-stream 重试按它们判型;冻结在 `test/api-error-text-freeze.test.ts` 的
|
|
79
|
-
* `FROZEN_RESULT_BODY` 与 `test/api-notice-text-freeze.test.ts` §2)
|
|
80
|
-
*
|
|
99
|
+
* `FROZEN_RESULT_BODY` 与 `test/api-notice-text-freeze.test.ts` §2)。编排器片 2:排空体的 `reason` 键随
|
|
100
|
+
* 停机因由声明口一起退役(排空口的体改成契约 `DrainRequest`,没有因由字段);`lease_expired` 一臂新增。
|
|
81
101
|
*/
|
|
82
|
-
export declare function billableRefusalOf(
|
|
102
|
+
export declare function billableRefusalOf(reason: AdmissionRefusal): BillableRefusal;
|
|
83
103
|
/** 发送形:`Retry-After` + 体(`dispatch.ts` 的 billable 门与 a2a `message/send` 同调)。走 `sendError` —— errorCode
|
|
84
104
|
* 是它的必填位置参(对抗复审 #3:修前这里是 `sendJson(res, r.status, r.body)`,与 leader 口的豁免行逐字同文,
|
|
85
105
|
* 在 error-code-key-gate 上借了别人的豁免过门);体键序与修前逐字同(`error, errorCode, message, reason?`)。 */
|
package/dist/http/readiness.js
CHANGED
|
@@ -33,16 +33,41 @@ export function sendReadyz(res, v) {
|
|
|
33
33
|
res.setHeader("retry-after", "5");
|
|
34
34
|
sendJson(res, 503, { ready: false, reasons: [...v.reasons] });
|
|
35
35
|
}
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
export const BILLABLE_KINDS = ["submit", "hitl-answer", "resume"];
|
|
37
|
+
export const ADMISSION_REFUSALS = [...READINESS_REASONS, "lease_expired"];
|
|
38
|
+
const ADMISSION_TABLE = {
|
|
39
|
+
draining: { submit: true, "hitl-answer": false, resume: false },
|
|
40
|
+
model_roster_pending: { submit: true, "hitl-answer": true, resume: true },
|
|
41
|
+
lease_expired: { submit: true, "hitl-answer": false, resume: false },
|
|
42
|
+
};
|
|
43
|
+
export function admissionRefusalOf(verdict, lease, kind) {
|
|
44
|
+
const holdsNow = (r) => {
|
|
45
|
+
switch (r) {
|
|
46
|
+
case "draining":
|
|
47
|
+
case "model_roster_pending":
|
|
48
|
+
return !verdict.ready && verdict.reasons.includes(r);
|
|
49
|
+
case "lease_expired":
|
|
50
|
+
return lease?.state === "expired" && lease.pastOutageMax;
|
|
51
|
+
default: {
|
|
52
|
+
const unhandled = r;
|
|
53
|
+
throw new Error(`admission refusal without a predicate arm: ${String(unhandled)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const refusal = ADMISSION_REFUSALS.find((r) => holdsNow(r) && ADMISSION_TABLE[r][kind]);
|
|
58
|
+
return refusal === undefined ? undefined : billableRefusalOf(refusal);
|
|
59
|
+
}
|
|
60
|
+
export function billableRefusalOf(reason) {
|
|
61
|
+
switch (reason) {
|
|
39
62
|
case "draining":
|
|
40
|
-
return { status: 503, body: { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance"
|
|
63
|
+
return { status: 503, body: { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" }, retryAfterSec: 15 };
|
|
41
64
|
case "model_roster_pending":
|
|
42
65
|
return { status: 503, body: { error: "model_roster_pending", errorCode: "state.model_roster_pending", message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" }, retryAfterSec: 5 };
|
|
66
|
+
case "lease_expired":
|
|
67
|
+
return { status: 503, body: { error: "lease_expired", errorCode: "state.lease_expired", message: "this instance has not renewed its serving lease with the fleet center for longer than its outage limit — it takes no new work; cancel, approvals, resume and reads stay open here — retry new work against another instance" }, retryAfterSec: 60 };
|
|
43
68
|
default: {
|
|
44
|
-
const unhandled =
|
|
45
|
-
throw new Error(`
|
|
69
|
+
const unhandled = reason;
|
|
70
|
+
throw new Error(`admission refusal without a billable refusal: ${String(unhandled)}`);
|
|
46
71
|
}
|
|
47
72
|
}
|
|
48
73
|
}
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -69,6 +69,10 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
69
69
|
write_face_damaged: false,
|
|
70
70
|
skill_grants_damaged: false,
|
|
71
71
|
constraint_chain_missing: false,
|
|
72
|
+
hands_coordinates_damaged: false,
|
|
73
|
+
installation_record_absent: true,
|
|
74
|
+
installation_record_misaligned: false,
|
|
75
|
+
installation_record_invalid: false,
|
|
72
76
|
};
|
|
73
77
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
74
78
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
@@ -578,7 +582,7 @@ export function createResumeLegs(ctx) {
|
|
|
578
582
|
let unregisterEngineNotice;
|
|
579
583
|
try {
|
|
580
584
|
const instructionLeg = createInstructionSelectionLeg();
|
|
581
|
-
result = await instructionLeg.run(sessionId, () => withModelReachScope(sessionId, () => withPrincipal(principal, async () => {
|
|
585
|
+
result = await instructionLeg.run(sessionId, () => withModelReachScope({ sessionId, taskId }, () => withPrincipal(principal, async () => {
|
|
582
586
|
const resumeNotifiedKeys = new NotifiedKeys();
|
|
583
587
|
if (deps.runStore && taskId) {
|
|
584
588
|
const rs0 = deps.runStore;
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -569,10 +569,12 @@ export type RouteMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
|
569
569
|
* `satisfies` 让它与 {@link RouteMethod} 双向闭合:漏一个动词 / 多一个都是编译红。 */
|
|
570
570
|
export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "DELETE"];
|
|
571
571
|
export interface RouteDeclFacts {
|
|
572
|
-
/** 「这条 url **烧模型**」—— drain / model-roster-pending / 无 service-token
|
|
573
|
-
* 没有这一列 = 不烧模型(
|
|
574
|
-
* 少一道门 ——
|
|
575
|
-
|
|
572
|
+
/** 「这条 url **烧模型**」—— drain / model-roster-pending / 租约过期 / 无 service-token 四道 503 的共同判别。
|
|
573
|
+
* 没有这一列 = 不烧模型(零默认值:要它在场必须显式写出来,漏写的方向是**少收一笔账单**而不是
|
|
574
|
+
* 少一道门 —— 反了才是灾难,所以门读的是在场这一侧)。
|
|
575
|
+
* 编排器片 2 件 2:值 = 这一种计费请求的**种类**(`submit` 新工作 / `hitl-answer` 对等人的卡作答 / `resume` 续跑;闭集属主
|
|
576
|
+
* `http/readiness.ts` 的 `BILLABLE_KINDS`)—— 排空与租约过期只拒 `submit`(放行闭集表 `ADMISSION_TABLE`),无凭证门不看种类。 */
|
|
577
|
+
readonly billable?: import("./readiness.js").BillableKind;
|
|
576
578
|
/** 「本条 url 在**没有任何 service credential** 的部署形下,这几个方法必须 fail-closed」。
|
|
577
579
|
* 判据(两个合取项)= 「授权的唯一输入是 principal 头」∧「持久改写,或爆炸半径跨租户的治理读」;
|
|
578
580
|
* 逐口论证在各 `routes/*.ts` 的头注里,声明行只记结论。 */
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
import type { FlatServiceDeps } from "./server.js";
|
|
55
55
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
56
56
|
import type { RouteDecl, RouteDispatchDecl, RouteMatch, RouteCtx, RouteMethod } from "./route-ctx.js";
|
|
57
|
+
import type { BillableKind } from "./readiness.js";
|
|
57
58
|
/** 一只域 handler 相对**全局服务凭据门**的位置。门前 = 未鉴权可达(每一只都要单独论证),门后 = 先过凭据。 */
|
|
58
59
|
/**
|
|
59
60
|
* 一行路由在全局服务凭据门的哪一侧、吃哪道门(闭集,三员;S-672 起由两员拆成三员):
|
|
@@ -133,6 +134,8 @@ export declare function matchDecl(d: RouteDispatchDecl, url: string): Readonly<R
|
|
|
133
134
|
export declare function routeLabel(_method: string, url: string): string;
|
|
134
135
|
/** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them. */
|
|
135
136
|
export declare function isBillableSubmitPath(url: string): boolean;
|
|
137
|
+
/** 编排器片 2 件 2:这条 url 是哪一**种**计费请求(`undefined` = 不在计费面)。声明序第一条命中的行定种类(今天每条 url 至多命中一行)。 */
|
|
138
|
+
export declare function billableKindOf(url: string): BillableKind | undefined;
|
|
136
139
|
/** [ref]([ref] 件5)—— **改写门**:不烧模型、但在没有任何 service credential 的部署形下必须
|
|
137
140
|
* 与 billable / bake 两族一样 fail-closed 的写门。与 `isBillableSubmitPath` **刻意分开**:这几扇门
|
|
138
141
|
* `billable=false`(零模型工作),塞进那张名单会让它们连带吃 drain 与 roster-pending 两道 503 ——
|
package/dist/http/route-table.js
CHANGED
|
@@ -109,7 +109,7 @@ export function matchDecl(d, url) {
|
|
|
109
109
|
const NO_PARAMS = Object.freeze({});
|
|
110
110
|
const LABEL_BY_PATH = new Map(ROUTE_DECLARATIONS.flatMap((d) => ("path" in d ? [[d.path, d.path]] : [])));
|
|
111
111
|
const SHAPED_LABEL_DECLS = ROUTE_DECLARATIONS.filter((d) => !("path" in d) && typeof d.label === "string");
|
|
112
|
-
const BILLABLE_DECLS = ROUTE_DECLARATIONS.filter((d) => d.billable
|
|
112
|
+
const BILLABLE_DECLS = ROUTE_DECLARATIONS.filter((d) => d.billable !== undefined);
|
|
113
113
|
const METHOD_DISPATCHED_DECLS = ROUTE_DECLARATIONS.filter((d) => d.methodDispatched === true);
|
|
114
114
|
const CREDENTIAL_GATED_BY_METHOD = (() => {
|
|
115
115
|
const m = new Map();
|
|
@@ -135,6 +135,9 @@ export function routeLabel(_method, url) {
|
|
|
135
135
|
export function isBillableSubmitPath(url) {
|
|
136
136
|
return BILLABLE_DECLS.some((d) => declMatches(d, url));
|
|
137
137
|
}
|
|
138
|
+
export function billableKindOf(url) {
|
|
139
|
+
return BILLABLE_DECLS.find((d) => declMatches(d, url))?.billable;
|
|
140
|
+
}
|
|
138
141
|
export function isCredentialGatedRewrite(method, url) {
|
|
139
142
|
const rows = CREDENTIAL_GATED_BY_METHOD.get(method.toUpperCase());
|
|
140
143
|
return rows !== undefined && rows.some((d) => declMatches(d, url));
|
|
@@ -79,7 +79,7 @@ export type A2aServeRouteId = RouteIdsOf<typeof A2A_SERVE_ROUTES>;
|
|
|
79
79
|
export declare const A2A_RPC_ROUTES: readonly [{
|
|
80
80
|
readonly id: "a2a-rpc";
|
|
81
81
|
readonly path: "/v1/a2a";
|
|
82
|
-
readonly billable:
|
|
82
|
+
readonly billable: "submit";
|
|
83
83
|
readonly methodDispatched: true;
|
|
84
84
|
readonly methods: readonly ["POST"];
|
|
85
85
|
}];
|
|
@@ -5,7 +5,7 @@ import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-cont
|
|
|
5
5
|
import { HttpError } from "../../security.js";
|
|
6
6
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
|
-
import {
|
|
8
|
+
import { admissionRefusalOf, readinessOf, sendBillableRefusal } from "../readiness.js";
|
|
9
9
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
10
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
11
|
import * as facts from "../../capability-facts.js";
|
|
@@ -168,10 +168,10 @@ function rpcErrorFromTypedFailure(err, skillId) {
|
|
|
168
168
|
}
|
|
169
169
|
function submitUnavailable(res, ctx) {
|
|
170
170
|
const { deps } = ctx;
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
171
|
+
const refusal = admissionRefusalOf(readinessOf(deps), deps.servingLease?.(), "submit");
|
|
172
|
+
if (refusal === undefined)
|
|
173
173
|
return false;
|
|
174
|
-
sendBillableRefusal(res,
|
|
174
|
+
sendBillableRefusal(res, refusal);
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
177
|
function readBlocking(params) {
|
|
@@ -392,6 +392,6 @@ export const A2A_SERVE_ROUTES = [
|
|
|
392
392
|
{ id: "a2a-agent-card", path: "/.well-known/agent-card.json", methods: ["GET"] },
|
|
393
393
|
];
|
|
394
394
|
export const A2A_RPC_ROUTES = [
|
|
395
|
-
{ id: "a2a-rpc", path: "/v1/a2a", billable:
|
|
395
|
+
{ id: "a2a-rpc", path: "/v1/a2a", billable: "submit", methodDispatched: true, methods: ["POST"] },
|
|
396
396
|
];
|
|
397
397
|
//# sourceMappingURL=a2a-serve.js.map
|