@sema-agent/server 7.53.0 → 7.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +5 -0
- package/dist/approval-ask-audit-store.d.ts +129 -0
- package/dist/approval-ask-audit-store.js +284 -0
- package/dist/approval-card.d.ts +18 -6
- package/dist/approval-card.js +2 -2
- package/dist/approval-reconciler.d.ts +2 -1
- package/dist/approval-reconciler.js +1 -1
- package/dist/boot/config-center.js +2 -2
- package/dist/boot/coordinators.d.ts +2 -0
- package/dist/boot/coordinators.js +18 -1
- package/dist/boot/leader.js +2 -0
- package/dist/boot/reapers.d.ts +2 -1
- package/dist/boot/resolve-spec.js +1 -3
- package/dist/boot/runner-deps.d.ts +20 -0
- package/dist/boot/runner-deps.js +14 -8
- package/dist/boot/shutdown.js +1 -1
- package/dist/boot/stores.js +8 -1
- package/dist/capabilities/team.d.ts +10 -9
- package/dist/config-center/apply-effective.d.ts +25 -26
- package/dist/config-center/apply-effective.js +28 -66
- package/dist/config-center/effective-keys.d.ts +30 -0
- package/dist/config-center/effective-keys.js +49 -0
- package/dist/config-center/facade.d.ts +3 -3
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.js +3 -9
- package/dist/config-center/types.d.ts +19 -40
- package/dist/config-lkg.js +7 -1
- package/dist/config-provider.js +1 -1
- package/dist/device-enrollment.d.ts +5 -3
- package/dist/device-store.d.ts +81 -3
- package/dist/device-store.js +37 -0
- package/dist/device-ws-hub.d.ts +15 -2
- package/dist/device-ws-hub.js +31 -5
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/devices.d.ts +64 -0
- package/dist/http/routes/devices.js +173 -0
- package/dist/http/server.d.ts +14 -0
- package/dist/http/server.js +32 -2
- package/dist/http/wire-types.d.ts +19 -0
- package/dist/leader/wire.d.ts +12 -0
- package/dist/leader/wire.js +6 -5
- package/dist/main.js +3 -1
- package/dist/observability/fail-open.d.ts +20 -0
- package/dist/observability/fail-open.js +20 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +11 -1
- package/dist/plugins/approval-ask-store-memory.js +20 -3
- package/dist/plugins/approval-ask-store-sql.d.ts +82 -0
- package/dist/plugins/approval-ask-store-sql.js +41 -10
- package/dist/plugins/device-store-sql.d.ts +38 -1
- package/dist/plugins/device-store-sql.js +82 -2
- package/dist/plugins/pg-pool.d.ts +29 -2
- package/dist/plugins/pg-pool.js +45 -2
- package/dist/plugins/remote-env-device.d.ts +4 -2
- package/dist/plugins/remote-env-device.js +12 -2
- package/dist/plugins/roster-store-sql.d.ts +1 -3
- package/dist/plugins/sql-errors.d.ts +12 -0
- package/dist/plugins/sql-errors.js +10 -0
- package/dist/plugins/store-backend.js +1 -1
- package/dist/plugins/tidb-pool.d.ts +13 -0
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +4 -0
- package/dist/runtime-governance.js +23 -1
- package/dist/tool-approval.d.ts +63 -39
- package/dist/tool-approval.js +322 -109
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/injection-tier.d.ts +11 -21
- package/dist/trace/injection-tier.js +3 -4
- package/dist/trace/ledger-events.d.ts +9 -0
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config-center wire types — the config-center `/api/config/effective` payload shapes (`CenterModel`,
|
|
3
|
-
* `
|
|
3
|
+
* `CenterSkillManifest`, `CenterMcpServer`, `CenterA2aPeer`, `EffectiveConfig`) plus the per-principal
|
|
4
4
|
* execution/session-mirror ruling shapes riding the caps view (`ExecutionRuling`/`SessionMirrorRuling`).
|
|
5
5
|
* Pure type/interface declarations — no runtime logic. Split out of `facade.ts` ([ref] A13,
|
|
6
6
|
* internal-lossless: the facade re-exports every symbol below unchanged).
|
|
@@ -71,24 +71,6 @@ export type CenterRoleTarget = {
|
|
|
71
71
|
} | {
|
|
72
72
|
select: unknown;
|
|
73
73
|
};
|
|
74
|
-
export interface CenterTeam {
|
|
75
|
-
name: string;
|
|
76
|
-
members: {
|
|
77
|
-
role: string;
|
|
78
|
-
modelRole?: string;
|
|
79
|
-
model?: string;
|
|
80
|
-
systemPrompt?: string;
|
|
81
|
-
}[];
|
|
82
|
-
rounds: number;
|
|
83
|
-
synthesizer?: {
|
|
84
|
-
role: string;
|
|
85
|
-
modelRole?: string;
|
|
86
|
-
systemPrompt?: string;
|
|
87
|
-
};
|
|
88
|
-
scenario?: string;
|
|
89
|
-
maxTranscriptTokens?: number;
|
|
90
|
-
enabled?: boolean;
|
|
91
|
-
}
|
|
92
74
|
/** Skill MANIFEST entry (content is NOT inline — fetched by hash; see `fetchSkillContent`). */
|
|
93
75
|
export interface CenterSkillManifest {
|
|
94
76
|
name: string;
|
|
@@ -250,14 +232,8 @@ export interface EffectiveConfig {
|
|
|
250
232
|
tierGroups?: unknown;
|
|
251
233
|
activeTierGroup?: string;
|
|
252
234
|
};
|
|
253
|
-
/**
|
|
254
|
-
*
|
|
255
|
-
* permanent builtin base). */
|
|
256
|
-
teams?: {
|
|
257
|
-
teams: CenterTeam[];
|
|
258
|
-
};
|
|
259
|
-
/** registry-core 0.7 collab domain (teams' successor) — consumed by the slice-① read-only
|
|
260
|
-
* projection (projectCollabToTeams → registerTeams). Both lanes carry it: the remote worker /effective view
|
|
235
|
+
/** registry-core 0.7 collab domain (teams' successor) — consumed by the slice-1.5 projection
|
|
236
|
+
* (projectCollabToWorkflows → registerCollabWorkflows). Both lanes carry it: the remote worker /effective view
|
|
261
237
|
* ships collab, and local FileConfigStore reads config.d/collab.json (PORTABLE_DOMAINS includes collab). */
|
|
262
238
|
collab?: {
|
|
263
239
|
templates: CollabTemplateWire[];
|
|
@@ -285,22 +261,23 @@ export interface EffectiveConfig {
|
|
|
285
261
|
enabled?: boolean;
|
|
286
262
|
}>;
|
|
287
263
|
};
|
|
288
|
-
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
264
|
+
/** `runtime` 域 = **限额残余五键**,与 settings-schema `RuntimeConfig` 的键集逐字相等([ref] 件3 对账门
|
|
265
|
+
* test/settings-schema-consumption-gates.test.ts 读 dist `RuntimeConfig.shape` 钉集合等式)。全部 center
|
|
266
|
+
* `.optional()`(center `f95438a`):缺席 = 不管 → 保 env;在场(含显式 0 = "publish OFF")= 覆盖。
|
|
267
|
+
* [ref] 批1 起五键全是**热**键(属主 stage-limits,boot+refresh 双腿);`limits` 域是它们的继任发布位
|
|
268
|
+
* (两处同现 ⇒ limits 赢,见 {@link EffectiveConfig.limits})。
|
|
269
|
+
*
|
|
270
|
+
* 🔴 治理三件(autonomy / commandPolicy / approvalRequire)**不在这里**([ref] 件3 退役键清除):它们自
|
|
271
|
+
* settings-schema 0.6 起住 `governance` 域,0.7.1 撤了 runtime 位的双写镜像;此后 `RuntimeConfig` 是 strip
|
|
272
|
+
* 的 zod object,两条腿(远端 `readEffectiveWire` / 本地 FileConfigStore→`buildEffective`)都把旧位 strip
|
|
273
|
+
* 掉、由 `legacyGovernanceFromRuntime` 抬进 governance ⇒ 本仓在 runtime 位上读它们的每一行都是死码。
|
|
274
|
+
* 旧类型面上留着这三键,让 `runtime-gates` 重启片一直读着一个生产上恒缺席的位(审批闸收紧静默不生效)。 */
|
|
291
275
|
runtime?: {
|
|
292
276
|
rateLimitPerMin?: number;
|
|
293
|
-
approvalRequire?: string[];
|
|
294
277
|
maxTaskCostUsd?: number;
|
|
295
278
|
maxTaskTokens?: number;
|
|
296
279
|
maxPrincipalCostUsd?: number;
|
|
297
280
|
costQuotaWindowSec?: number;
|
|
298
|
-
/** Runtime governance second baton (center §10): autonomy mode + per-command rules. UNLIKE the 6 gates above,
|
|
299
|
-
* these are per-request HOT (read live in resolveSpec, not baked into boot middleware) → applied on boot AND
|
|
300
|
-
* refresh by `applyRuntimeHot` (NOT in RUNTIME_GATE_KEYS / restart-to-apply). Same presence rule (undefined =
|
|
301
|
-
* unmanaged → keep). */
|
|
302
|
-
autonomy?: Autonomy;
|
|
303
|
-
commandPolicy?: CommandRule[];
|
|
304
281
|
};
|
|
305
282
|
/**
|
|
306
283
|
* [ref] 批1 —— **limitsHttp 组的热更新发布域**([ref] §6 批1)。
|
|
@@ -330,9 +307,11 @@ export interface EffectiveConfig {
|
|
|
330
307
|
projects?: {
|
|
331
308
|
projects?: Record<string, unknown>;
|
|
332
309
|
};
|
|
333
|
-
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
310
|
+
/** settings-schema 0.6 起治理三件(autonomy/commandPolicy/approvalRequire)的**唯一**承运域(0.7.1 撤
|
|
311
|
+
* runtime 位双写镜像;顺序契约当年已走完:消费端先切、center 再撤)。键集与 schema `GovernanceConfig.shape`
|
|
312
|
+
* 逐字相等([ref] 件3 对账门钉)。消费:autonomy/commandPolicy 逐请求热(`stageRuntimeHot`,boot+refresh);
|
|
313
|
+
* approvalRequire boot-baked(`stageApprovalGate`,refresh 不写)⇒ 变更由 `runtime-gates` 重启片通告。
|
|
314
|
+
* 同一 presence 规则(undefined = 不管 → 保 env / 回基线)。 */
|
|
336
315
|
governance?: {
|
|
337
316
|
autonomy?: Autonomy;
|
|
338
317
|
commandPolicy?: CommandRule[];
|
package/dist/config-lkg.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function loadLkg(path, worker) {
|
|
|
47
47
|
const eff = p.effective;
|
|
48
48
|
if (typeof eff.version !== "number")
|
|
49
49
|
return { reason: "bad effective.version" };
|
|
50
|
-
for (const [domain, arrayKey] of [["models", "models"], ["
|
|
50
|
+
for (const [domain, arrayKey] of [["models", "models"], ["skills", "skills"], ["mcp", "servers"], ["scenarios", "scenarios"], ["runtime", null]]) {
|
|
51
51
|
const v = eff[domain];
|
|
52
52
|
if (v === undefined)
|
|
53
53
|
continue;
|
|
@@ -59,6 +59,12 @@ export async function loadLkg(path, worker) {
|
|
|
59
59
|
return { reason: `bad effective.${domain}.${arrayKey}` };
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
{
|
|
63
|
+
const rt = eff.runtime;
|
|
64
|
+
if (rt && (rt.autonomy !== undefined || rt.commandPolicy !== undefined || rt.approvalRequire !== undefined)) {
|
|
65
|
+
return { reason: "legacy runtime governance keys (pre-0.7.1 mirror shape; no contract producer since LKG shipped) — refusing rather than silently dropping governance" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
62
68
|
const promptsRaw = p.effective.prompts;
|
|
63
69
|
if (promptsRaw !== undefined && !validatePromptsDomain(promptsRaw).ok)
|
|
64
70
|
return { reason: "prompts face fails re-validation" };
|
package/dist/config-provider.js
CHANGED
|
@@ -126,8 +126,8 @@ export function mapToServiceEffective(eff, version) {
|
|
|
126
126
|
...(eff.models.tierGroups !== undefined ? { tierGroups: eff.models.tierGroups } : {}),
|
|
127
127
|
...(eff.models.activeTierGroup !== undefined ? { activeTierGroup: eff.models.activeTierGroup } : {}),
|
|
128
128
|
},
|
|
129
|
-
teams: { teams: [] },
|
|
130
129
|
...(eff.collab?.templates?.length ? { collab: { templates: eff.collab.templates } } : {}),
|
|
130
|
+
...(eff.plugins?.plugins?.length ? { plugins: { plugins: eff.plugins.plugins } } : {}),
|
|
131
131
|
skills: {
|
|
132
132
|
skills: (eff.skills.skills ?? []).map((s) => ({
|
|
133
133
|
name: s.name,
|
|
@@ -23,13 +23,15 @@
|
|
|
23
23
|
* 判定);②吊销同事务把连接租约作废并把 `conn_generation` 推进一格 ⇒ 在途连接的每一次带世代谓词的写
|
|
24
24
|
* 都会影响 0 行,设备侧因此立刻失权(§4.6 步 2);③重新 enrollment = 新 deviceId,不复活旧行。
|
|
25
25
|
*/
|
|
26
|
-
import {
|
|
26
|
+
import { type DeviceOwner, type DeviceRejectCode, type DeviceRow, type DeviceSessionRow, type DeviceStore, type NewDeviceInput } from "./device-store.js";
|
|
27
27
|
/** typed 拒绝 —— 端点层只负责把它翻成 HTTP(status 已在体内,不许调用方另算)。 */
|
|
28
28
|
export interface DeviceRejection {
|
|
29
|
-
|
|
29
|
+
/** [ref]:含两个借用位(`limit.rate_exceeded` 429 / `draining` 503);enrollment 本文件只铸前者,
|
|
30
|
+
* `draining` 的唯一铸造点在 device-ws-hub 的排空/停机两臂。 */
|
|
31
|
+
code: DeviceRejectCode;
|
|
30
32
|
status: number;
|
|
31
33
|
message: string;
|
|
32
|
-
/**
|
|
34
|
+
/** 限流/排空专用:`Retry-After` 的秒值(附录 A 的 429/503 族纪律)。 */
|
|
33
35
|
retryAfterSec?: number;
|
|
34
36
|
}
|
|
35
37
|
export type DeviceOutcome<T> = {
|
package/dist/device-store.d.ts
CHANGED
|
@@ -28,6 +28,13 @@ export declare const DEVICE_ERROR_CODES: {
|
|
|
28
28
|
readonly ENROLLMENT_INVALID: "device.enrollment_invalid";
|
|
29
29
|
/** 反枚举 404:设备不存在 **与** 设备不是你的,逐字节同形。 */
|
|
30
30
|
readonly NOT_FOUND: "not_found.device";
|
|
31
|
+
/** O4 换绑动词(2026-08-30 v2 稿 §2 步 3):前态 CAS 输 —— 体带当前 rev(`currentRev`,[ref]
|
|
32
|
+
* currentPending 哲学,壳一跳重定位)。**唯一铸造点 = `POST /v1/devices/sessions/:rootSessionId/rebind`**
|
|
33
|
+
* (管理面,`http/routes/devices.ts`);adapter/准入链结构上不产它。 */
|
|
34
|
+
readonly REBIND_REV_CONFLICT: "device.rebind_rev_conflict";
|
|
35
|
+
/** O4 换绑动词(v2 稿 §2 步 4):该根会话名下有非终态 run(durable 判据 = run store 的 session
|
|
36
|
+
* claim;hub 内存在途计数**不作门**——跨副本不可见)—— 体带 `activeTaskId`。铸造点同上一条。 */
|
|
37
|
+
readonly REBIND_ACTIVE_RUN: "device.rebind_active_run";
|
|
31
38
|
};
|
|
32
39
|
/** {@link DEVICE_ERROR_CODES} 的值联合 —— 闭集(未知码 = 编译错误,[ref] 词表纪律)。 */
|
|
33
40
|
export type DeviceErrorCode = (typeof DEVICE_ERROR_CODES)[keyof typeof DEVICE_ERROR_CODES];
|
|
@@ -39,16 +46,34 @@ export type DeviceErrorCode = (typeof DEVICE_ERROR_CODES)[keyof typeof DEVICE_ER
|
|
|
39
46
|
* 同义码 = 消费端要认两套。`device.busy`(429)留给**在途指令数**上限,不是这里。
|
|
40
47
|
*/
|
|
41
48
|
export declare const DEVICE_RATE_LIMIT_CODE: "limit.rate_exceeded";
|
|
49
|
+
/**
|
|
50
|
+
* 排空/停机拒绝复用**既有**的通用码(`draining`,`docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A 已收录,503)。
|
|
51
|
+
*
|
|
52
|
+
* 🔴 刻意不铸 `device.draining`([ref]/[ref]):§4.8 的 device 闭集**冻结**且无 draining 成员,而排空
|
|
53
|
+
* 语义面全部署早有属主词 —— HTTP 计费提交拒臂(`sendError(res, 503, "draining", …)`,SDK spec 逐字引用
|
|
54
|
+
* 的冻结字面)、drain 期新 upgrade 拒(503)、WS hello 拒帧 / bye 帧的 `draining` 分支键。为同一语义再铸
|
|
55
|
+
* `device.*` 同义码 = 消费端要认两套(上面 rate-limit 条的同一判据);且 `device.draining` 在 HTTP 层
|
|
56
|
+
* 零发送点,写进附录 A 即幽灵行(`error-code-catalog-live` 的「文档多码」臂当场红)。
|
|
57
|
+
* 与 `device.busy` 的分工自此可判:`device.busy`(429)= 在途满,稍后重试**同设备**;`draining`(503)=
|
|
58
|
+
* **本副本**在退,重试同副本不会好 —— 该去替换副本/等设备重连。
|
|
59
|
+
*/
|
|
60
|
+
export declare const DEVICE_DRAINING_CODE: "draining";
|
|
61
|
+
/** device 拒绝面的码全集 = §4.8 闭集 + 两个借用位(限流 429 / 排空 503)。 */
|
|
62
|
+
export type DeviceRejectCode = DeviceErrorCode | typeof DEVICE_RATE_LIMIT_CODE | typeof DEVICE_DRAINING_CODE;
|
|
42
63
|
/** 拒绝码 → HTTP 状态。**穷举 map**(缺一个成员 = 编译错误),不是运行时 switch 的默认臂。 */
|
|
43
|
-
export declare const DEVICE_ERROR_STATUS: Readonly<Record<
|
|
64
|
+
export declare const DEVICE_ERROR_STATUS: Readonly<Record<DeviceRejectCode, number>>;
|
|
44
65
|
/**
|
|
45
66
|
* `device_audit.event` 的闭集(v2 §6 DDL 的 CHECK 清单 + §4.8「标审计行者」,两处逐字对齐)。
|
|
46
67
|
*
|
|
47
68
|
* ⚠️ §4.8 的**遥测**清单比这张表长(`device_connected`/`device_disconnected`/`device_heartbeat_lost`/
|
|
48
69
|
* `device_instruction_dispatched`/`device_outcome_unknown`)—— 那些是**计数器/日志**面,不落审计表。
|
|
49
|
-
* 只有下面这
|
|
70
|
+
* 只有下面这 9 个是「安全轴事件,必须可追溯查询」。加成员 = 同时改这里 + 两侧 DDL 的 CHECK
|
|
71
|
+
* (`AUDIT_EVENT_CHECK` 由本数组生成,DDL 自动跟);**且**存量表的旧 CHECK 不会被
|
|
72
|
+
* `CREATE TABLE IF NOT EXISTS` 更新 —— 升级探针见 `device-store-sql.ts` 的
|
|
73
|
+
* `assertDeviceAuditRebindEventSchema`(能力探测:PG 强制 CHECK、TiDB 默认不执行,探的是**行为**
|
|
74
|
+
* 不是元数据 —— memory `capability-detection-not-impl-names` 同判)。
|
|
50
75
|
*/
|
|
51
|
-
export declare const DEVICE_AUDIT_EVENTS: readonly ["device_enrolled", "device_revoked", "device_token_rotated", "device_enroll_token_reuse", "device_frame_unexpected", "device_identity_mismatch", "device_superseded", "device_attached_elsewhere"];
|
|
76
|
+
export declare const DEVICE_AUDIT_EVENTS: readonly ["device_enrolled", "device_revoked", "device_token_rotated", "device_enroll_token_reuse", "device_frame_unexpected", "device_identity_mismatch", "device_superseded", "device_attached_elsewhere", "session_rebound"];
|
|
52
77
|
export type DeviceAuditEvent = (typeof DEVICE_AUDIT_EVENTS)[number];
|
|
53
78
|
/** v1 平台闭集(AR-R1-8)。win32 = follow-on(§10 车A-4 的 flavor 注)。 */
|
|
54
79
|
export declare const DEVICE_PLATFORM_OS: readonly ["darwin", "linux"];
|
|
@@ -144,6 +169,10 @@ export declare function assertDevicePubkeyShape(pubkey: string): void;
|
|
|
144
169
|
export declare function assertNewDeviceShape(input: NewDeviceInput): void;
|
|
145
170
|
export declare function assertIssueTokenShape(input: IssueEnrollTokenInput): void;
|
|
146
171
|
export declare function assertRevokeShape(input: RevokeDeviceInput): void;
|
|
172
|
+
/** O4 换绑入参的整形门(三形共用)。`expectedRev` 收窄为非负整数:它要进 SQL 等值谓词,小数/负数/NaN
|
|
173
|
+
* 在两方言的 BIGINT 比对下不是「不匹配」而是各自的隐式转换歧义面 —— 在门口拒,方言分歧面消失。
|
|
174
|
+
* ⚠️ HTTP 层对坏形先答 400(业务拒绝);这里的抛是**编程/装配错误**的兜底,不是 wire 分支。 */
|
|
175
|
+
export declare function assertRebindShape(input: RebindSessionInput): void;
|
|
147
176
|
export declare function assertAcquireShape(input: AcquireConnectionInput): void;
|
|
148
177
|
/** token 摘要必须是**规范形**(小写 sha256 hex):大小写混形在 VARBINARY/COLLATE "C" 下是两个键,
|
|
149
178
|
* 于是同一个明文 token 能被消费两次 —— 这是 §7-T5 ③「一次性 CAS」的直接跳过面。 */
|
|
@@ -259,6 +288,47 @@ export type BindSessionResult = {
|
|
|
259
288
|
} | {
|
|
260
289
|
outcome: "device_revoked";
|
|
261
290
|
};
|
|
291
|
+
/**
|
|
292
|
+
* O4 显式换绑(2026-08-30 v2 稿 §2)。`owner` = **绑定行的**复合身份(HTTP 层已裁决:属主同域或
|
|
293
|
+
* explicit operator;operator 代操作时传的也是行的 owner,不是操作员自己)——店内所有谓词恒带它,
|
|
294
|
+
* 判别位在店里就消失(反枚举,文件头注)。`expectedRev` = 壳从 S-4 读面拿到的前态(强制显式,
|
|
295
|
+
* 防两操作员并发换绑互踩)。
|
|
296
|
+
*/
|
|
297
|
+
export interface RebindSessionInput {
|
|
298
|
+
rootSessionId: string;
|
|
299
|
+
toDeviceId: string;
|
|
300
|
+
expectedRev: number;
|
|
301
|
+
owner: DeviceOwner;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* O4 换绑的判别式结果(门序 = v2 稿 §2:目标设备在场 → 幂等短路 → 前态 CAS → 审计;活跃 run 门在
|
|
305
|
+
* **HTTP 层**、CAS 之前判 —— run store 是另一只店,CAS 语句不兼职)。
|
|
306
|
+
*/
|
|
307
|
+
export type RebindSessionResult =
|
|
308
|
+
/** CAS 赢家:`rev+1` + `bound_at` 刷新 + `session_rebound` 审计行**同一事务**提交。 */
|
|
309
|
+
{
|
|
310
|
+
outcome: "rebound";
|
|
311
|
+
row: DeviceSessionRow;
|
|
312
|
+
}
|
|
313
|
+
/** 真幂等:`toDeviceId` ≡ 当前绑定 ∧ `expectedRev` ≡ 当前 rev ⇒ 回显不加 rev、不写审计。 */
|
|
314
|
+
| {
|
|
315
|
+
outcome: "idempotent";
|
|
316
|
+
row: DeviceSessionRow;
|
|
317
|
+
}
|
|
318
|
+
/** 前态输(rev 不符,含「toDeviceId 同但 rev 错」——前态错就是错)⇒ 携当前行供壳一跳重定位。 */
|
|
319
|
+
| {
|
|
320
|
+
outcome: "rev_conflict";
|
|
321
|
+
row: DeviceSessionRow;
|
|
322
|
+
}
|
|
323
|
+
/** 绑定行不在**或**不是该 owner 的 —— 反枚举同形(HTTP 层折 404 同串)。 */
|
|
324
|
+
| {
|
|
325
|
+
outcome: "binding_not_found";
|
|
326
|
+
}
|
|
327
|
+
/** 目标设备不在 / 不是该 owner 的 / **已吊销** —— v2 稿 §2 步 2:三臂同折 404(设备 id 也是
|
|
328
|
+
* 不可枚举面);吊销不单列 = 判别位在店里就消失,消费层想泄也泄不出来。 */
|
|
329
|
+
| {
|
|
330
|
+
outcome: "device_not_found";
|
|
331
|
+
};
|
|
262
332
|
export interface RevokeDeviceInput {
|
|
263
333
|
deviceId: string;
|
|
264
334
|
owner: DeviceOwner;
|
|
@@ -330,12 +400,18 @@ export interface DeviceStore {
|
|
|
330
400
|
/** 管理面/测试用的无 owner 谓词读口 —— **不得**接到任何 principal-scoped 的 wire 面上。 */
|
|
331
401
|
getDeviceUnscoped(deviceId: string): Promise<DeviceRow | null>;
|
|
332
402
|
listDevicesByOwner(owner: DeviceOwner): Promise<DeviceRow[]>;
|
|
403
|
+
/** O4/S-4:全量列表(operator 面专用;`getDeviceUnscoped` 同一条纪律 —— 不得接到任何
|
|
404
|
+
* principal-scoped 的 wire 面上,消费点必须先过 explicit operator 门)。 */
|
|
405
|
+
listDevicesUnscoped(): Promise<DeviceRow[]>;
|
|
333
406
|
revokeDevice(input: RevokeDeviceInput): Promise<RevokeDeviceResult>;
|
|
334
407
|
acquireConnection(input: AcquireConnectionInput): Promise<AcquireConnectionResult>;
|
|
335
408
|
/** 世代谓词命中 = true;0 行 = 已失权。 */
|
|
336
409
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
337
410
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
338
411
|
bindSession(input: BindSessionInput): Promise<BindSessionResult>;
|
|
412
|
+
/** O4 显式换绑(v2 稿 §2 步 2/3/5):目标设备门 + 前态 CAS + `session_rebound` 审计行同一事务。
|
|
413
|
+
* 活跃 run 门**不在这里**(run store 是另一只店)—— 由 HTTP 层在调用前判(v2 稿 §2 步 4)。 */
|
|
414
|
+
rebindSession(input: RebindSessionInput): Promise<RebindSessionResult>;
|
|
339
415
|
getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
|
|
340
416
|
/** purge coordinator 座(§6 注②:core 的会话删除是**逐 store 显式调用**,不是 DB 自动扫;
|
|
341
417
|
* 只建表不接 purge = 删会话后绑定残留 ⇒ sessionId 重用致旧设备粘连/claim conflict/隐私残留)。 */
|
|
@@ -366,11 +442,13 @@ export declare class InMemoryDeviceStore implements DeviceStore {
|
|
|
366
442
|
readDeviceForAdmission(deviceId: string, owner: DeviceOwner): Promise<DeviceAdmissionView | null>;
|
|
367
443
|
getDeviceUnscoped(deviceId: string): Promise<DeviceRow | null>;
|
|
368
444
|
listDevicesByOwner(owner: DeviceOwner): Promise<DeviceRow[]>;
|
|
445
|
+
listDevicesUnscoped(): Promise<DeviceRow[]>;
|
|
369
446
|
revokeDevice(input: RevokeDeviceInput): Promise<RevokeDeviceResult>;
|
|
370
447
|
acquireConnection(input: AcquireConnectionInput): Promise<AcquireConnectionResult>;
|
|
371
448
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
372
449
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
373
450
|
bindSession(input: BindSessionInput): Promise<BindSessionResult>;
|
|
451
|
+
rebindSession(input: RebindSessionInput): Promise<RebindSessionResult>;
|
|
374
452
|
getSessionBinding(rootSessionId: string): Promise<DeviceSessionRow | null>;
|
|
375
453
|
deleteByRootSession(rootSessionId: string): Promise<number>;
|
|
376
454
|
appendAudit(input: AppendAuditInput): Promise<void>;
|
package/dist/device-store.js
CHANGED
|
@@ -10,8 +10,11 @@ export const DEVICE_ERROR_CODES = {
|
|
|
10
10
|
BUSY: "device.busy",
|
|
11
11
|
ENROLLMENT_INVALID: "device.enrollment_invalid",
|
|
12
12
|
NOT_FOUND: "not_found.device",
|
|
13
|
+
REBIND_REV_CONFLICT: "device.rebind_rev_conflict",
|
|
14
|
+
REBIND_ACTIVE_RUN: "device.rebind_active_run",
|
|
13
15
|
};
|
|
14
16
|
export const DEVICE_RATE_LIMIT_CODE = "limit.rate_exceeded";
|
|
17
|
+
export const DEVICE_DRAINING_CODE = "draining";
|
|
15
18
|
export const DEVICE_ERROR_STATUS = {
|
|
16
19
|
"device.not_bound": 409,
|
|
17
20
|
"device.offline": 409,
|
|
@@ -23,7 +26,10 @@ export const DEVICE_ERROR_STATUS = {
|
|
|
23
26
|
"device.busy": 429,
|
|
24
27
|
"device.enrollment_invalid": 403,
|
|
25
28
|
"not_found.device": 404,
|
|
29
|
+
"device.rebind_rev_conflict": 409,
|
|
30
|
+
"device.rebind_active_run": 409,
|
|
26
31
|
"limit.rate_exceeded": 429,
|
|
32
|
+
"draining": 503,
|
|
27
33
|
};
|
|
28
34
|
export const DEVICE_AUDIT_EVENTS = [
|
|
29
35
|
"device_enrolled",
|
|
@@ -34,6 +40,7 @@ export const DEVICE_AUDIT_EVENTS = [
|
|
|
34
40
|
"device_identity_mismatch",
|
|
35
41
|
"device_superseded",
|
|
36
42
|
"device_attached_elsewhere",
|
|
43
|
+
"session_rebound",
|
|
37
44
|
];
|
|
38
45
|
export const DEVICE_PLATFORM_OS = ["darwin", "linux"];
|
|
39
46
|
export const DEVICE_STATUSES = ["active", "revoked"];
|
|
@@ -112,6 +119,14 @@ export function assertRevokeShape(input) {
|
|
|
112
119
|
assertBoundedText("revokedBy", input.revokedBy, DEVICE_TEXT_LIMITS.actor);
|
|
113
120
|
assertBoundedText("revokeReason", input.reason ?? "", DEVICE_TEXT_LIMITS.revokeReason);
|
|
114
121
|
}
|
|
122
|
+
export function assertRebindShape(input) {
|
|
123
|
+
assertRootSessionIdShape(input.rootSessionId);
|
|
124
|
+
assertDeviceIdShape(input.toDeviceId);
|
|
125
|
+
assertDeviceOwnerShape(input.owner);
|
|
126
|
+
if (!Number.isInteger(input.expectedRev) || input.expectedRev < 0) {
|
|
127
|
+
throw new Error(`device-store: expectedRev must be a non-negative integer (got ${input.expectedRev}) — it enters a byte-exact BIGINT equality predicate`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
115
130
|
export function assertAcquireShape(input) {
|
|
116
131
|
assertDeviceIdShape(input.deviceId);
|
|
117
132
|
assertDeviceOwnerShape(input.owner);
|
|
@@ -282,6 +297,9 @@ export class InMemoryDeviceStore {
|
|
|
282
297
|
assertDeviceOwnerShape(owner);
|
|
283
298
|
return [...this.devices.values()].filter((d) => sameOwner(d.owner, owner)).sort((a, b) => (a.deviceId < b.deviceId ? -1 : 1)).map((d) => this.snapshotDevice(d));
|
|
284
299
|
}
|
|
300
|
+
async listDevicesUnscoped() {
|
|
301
|
+
return [...this.devices.values()].sort((a, b) => (a.deviceId < b.deviceId ? -1 : 1)).map((d) => this.snapshotDevice(d));
|
|
302
|
+
}
|
|
285
303
|
async revokeDevice(input) {
|
|
286
304
|
assertRevokeShape(input);
|
|
287
305
|
const row = this.devices.get(input.deviceId);
|
|
@@ -363,6 +381,25 @@ export class InMemoryDeviceStore {
|
|
|
363
381
|
this.sessions.set(row.rootSessionId, row);
|
|
364
382
|
return { outcome: "bound", row: this.snapshotSession(row) };
|
|
365
383
|
}
|
|
384
|
+
async rebindSession(input) {
|
|
385
|
+
assertRebindShape(input);
|
|
386
|
+
const device = this.devices.get(input.toDeviceId);
|
|
387
|
+
if (!device || !sameOwner(device.owner, input.owner) || device.status !== "active")
|
|
388
|
+
return { outcome: "device_not_found" };
|
|
389
|
+
const row = this.sessions.get(input.rootSessionId);
|
|
390
|
+
if (!row || !sameOwner(row.owner, input.owner))
|
|
391
|
+
return { outcome: "binding_not_found" };
|
|
392
|
+
if (row.deviceId === input.toDeviceId && row.rev === input.expectedRev)
|
|
393
|
+
return { outcome: "idempotent", row: this.snapshotSession(row) };
|
|
394
|
+
if (row.rev !== input.expectedRev)
|
|
395
|
+
return { outcome: "rev_conflict", row: this.snapshotSession(row) };
|
|
396
|
+
const from = row.deviceId;
|
|
397
|
+
row.deviceId = input.toDeviceId;
|
|
398
|
+
row.boundAtMs = this.now();
|
|
399
|
+
row.rev += 1;
|
|
400
|
+
this.pushAudit({ event: "session_rebound", deviceId: input.toDeviceId, owner: row.owner, detail: { from, to: input.toDeviceId, rev: row.rev } });
|
|
401
|
+
return { outcome: "rebound", row: this.snapshotSession(row) };
|
|
402
|
+
}
|
|
366
403
|
async getSessionBinding(rootSessionId) {
|
|
367
404
|
assertRootSessionIdShape(rootSessionId);
|
|
368
405
|
const row = this.sessions.get(rootSessionId);
|
package/dist/device-ws-hub.d.ts
CHANGED
|
@@ -56,6 +56,14 @@ export type DeviceDispatchInput = {
|
|
|
56
56
|
[K in DeviceInstructionKind]: {
|
|
57
57
|
rootSessionId: string;
|
|
58
58
|
requester: DeviceOwner;
|
|
59
|
+
/**
|
|
60
|
+
* O4 §3 投递门**身份重断言**(codex 轮2 R2-[high],红先 R4b 实证):调用方 env **铸造时**钉住的
|
|
61
|
+
* 设备。准入在 dispatch 内解出的是**当前**绑定 —— 显式换绑与在飞 turn 竞态时两者可以不同,而
|
|
62
|
+
* adapter 的 connect 身份检查罩不住已连接 env 的后续调用(它们不再过 connect)。差异 ⇒ 恒拒
|
|
63
|
+
* `device.identity_mismatch`(cwd/能力面是从铸造设备读的,静默改道 = 在错误机器的工作区上执行)。
|
|
64
|
+
* **必填**:可选会让漏传静默([ref] `originTaskId` 同判)。
|
|
65
|
+
*/
|
|
66
|
+
expectedDeviceId: string;
|
|
59
67
|
kind: K;
|
|
60
68
|
args: DeviceInstructionArgsByKind[K];
|
|
61
69
|
cwd?: string;
|
|
@@ -162,8 +170,13 @@ export interface DeviceWsHub {
|
|
|
162
170
|
dispatch(input: DeviceDispatchInput): Promise<DeviceDispatchResult>;
|
|
163
171
|
/** 幂等:重复 cancel 同一指令 = no-op(§5.3)。 */
|
|
164
172
|
cancel(instructionId: string): Promise<void>;
|
|
165
|
-
/** §8-R7:停发新指令 + 拒新 upgrade,但**保持既有 WS**,在途结果帧照收。
|
|
166
|
-
|
|
173
|
+
/** §8-R7:停发新指令 + 拒新 upgrade,但**保持既有 WS**,在途结果帧照收。
|
|
174
|
+
* [ref]:装配层可带 `graceMs`(= shutdown 的 drainGraceMs 上界)——排空拒体借它给出「drain 窗最迟
|
|
175
|
+
* 何时收窗」的指路;getter 形每次活读(drainGraceMs 是热改键,一次性快照会在窗中热调后失真,
|
|
176
|
+
* codex-S09-F2);不带 = 拒体不造数(测试宿主/嵌入形没有这个事实)。 */
|
|
177
|
+
beginDrain(opts?: {
|
|
178
|
+
graceMs?: number | (() => number);
|
|
179
|
+
}): void;
|
|
167
180
|
isDraining(): boolean;
|
|
168
181
|
connectedDeviceIds(): string[];
|
|
169
182
|
/**
|
package/dist/device-ws-hub.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { WebSocketServer } from "ws";
|
|
4
|
-
import { DEVICE_ERROR_CODES, DEVICE_ERROR_STATUS, assertDeviceIdShape, } from "./device-store.js";
|
|
4
|
+
import { DEVICE_DRAINING_CODE, DEVICE_ERROR_CODES, DEVICE_ERROR_STATUS, assertDeviceIdShape, } from "./device-store.js";
|
|
5
5
|
import { DEVICE_MIN_PROTOCOL_VERSION, DEVICE_WS_LIMITS, DEVICE_WS_PATH, DEVICE_PROTOCOL_VERSION, asInstructionKind, buildHelloSignaturePayload, formatGeneration, mintChallengeNonce, mintInstructionId, parseDeviceFrameText, verifyHelloSignature, } from "./device-ws-protocol.js";
|
|
6
6
|
const OUTCOME_UNKNOWN_SENTENCE = "outcome unknown — the command may have already executed on the device; verify its effect before re-running";
|
|
7
7
|
const SETTLED_MEMO_MAX = 512;
|
|
8
|
+
const DRAIN_RETRY_AFTER_SEC = 15;
|
|
8
9
|
function rejection(code, message, retryAfterSec) {
|
|
9
10
|
return { code, status: DEVICE_ERROR_STATUS[code], message, ...(retryAfterSec === undefined ? {} : { retryAfterSec }) };
|
|
10
11
|
}
|
|
@@ -73,6 +74,8 @@ export function createDeviceWsHub(options) {
|
|
|
73
74
|
const settledMemo = new Map();
|
|
74
75
|
let draining = false;
|
|
75
76
|
let closed = false;
|
|
77
|
+
let drainGraceMs;
|
|
78
|
+
let drainStartMono;
|
|
76
79
|
function auditFrame(deviceId, owner, reason, detail) {
|
|
77
80
|
metrics?.inc("device_frame_unexpected_total", { reason });
|
|
78
81
|
void store
|
|
@@ -775,6 +778,12 @@ export function createDeviceWsHub(options) {
|
|
|
775
778
|
}
|
|
776
779
|
sendServerFrame(conn, { t: "payload", gen: conn.genStr, instructionId, part: "commit", totalLen: body.length, sha256 });
|
|
777
780
|
}
|
|
781
|
+
function drainingRejectResult() {
|
|
782
|
+
const graceNow = typeof drainGraceMs === "function" ? drainGraceMs() : drainGraceMs;
|
|
783
|
+
const remainMs = graceNow === undefined || drainStartMono === undefined ? undefined : Math.max(0, graceNow - (performance.now() - drainStartMono));
|
|
784
|
+
const drainHint = remainMs === undefined ? "" : `; the drain window ends within ~${Math.ceil(remainMs / 1000)}s`;
|
|
785
|
+
return { kind: "rejected", reject: rejection(DEVICE_DRAINING_CODE, "this replica is draining for shutdown/upgrade and no longer issues new device instructions — the instruction was never dispatched (safe to retry once the device reconnects on a replacement replica)" + drainHint, DRAIN_RETRY_AFTER_SEC) };
|
|
786
|
+
}
|
|
778
787
|
async function dispatch(input) {
|
|
779
788
|
if (input.payloadPartBytes !== undefined) {
|
|
780
789
|
const n = input.payloadPartBytes;
|
|
@@ -793,15 +802,21 @@ export function createDeviceWsHub(options) {
|
|
|
793
802
|
if (abortedNow())
|
|
794
803
|
return abortedBeforeCommit();
|
|
795
804
|
if (closed)
|
|
796
|
-
return { kind: "rejected", reject: rejection(
|
|
805
|
+
return { kind: "rejected", reject: rejection(DEVICE_DRAINING_CODE, "this device hub is closed for shutdown — the instruction was never dispatched (safe to retry once the device reconnects on a replacement replica)", DRAIN_RETRY_AFTER_SEC) };
|
|
797
806
|
if (draining)
|
|
798
|
-
return
|
|
807
|
+
return drainingRejectResult();
|
|
799
808
|
const admitted = await enrollment.admitSession({ rootSessionId: input.rootSessionId, requester: input.requester });
|
|
800
809
|
if (!admitted.ok)
|
|
801
810
|
return { kind: "rejected", reject: admitted.reject };
|
|
802
811
|
if (abortedNow())
|
|
803
812
|
return abortedBeforeCommit();
|
|
804
813
|
const { deviceId, connGeneration } = admitted.value;
|
|
814
|
+
if (deviceId !== input.expectedDeviceId) {
|
|
815
|
+
return {
|
|
816
|
+
kind: "rejected",
|
|
817
|
+
reject: rejection(DEVICE_ERROR_CODES.IDENTITY_MISMATCH, `this session is now bound to a different device (${deviceId}) than the one this instruction's environment was minted for — refusing to silently re-target; re-initiate the work on the currently bound device (O4 v2 §3)`),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
805
820
|
const conn = conns.get(deviceId);
|
|
806
821
|
if (!conn || conn.phase !== "active" || conn.goodbye) {
|
|
807
822
|
return { kind: "rejected", reject: rejection(DEVICE_ERROR_CODES.OFFLINE, "the bound device has no dispatchable connection on this replica") };
|
|
@@ -820,12 +835,18 @@ export function createDeviceWsHub(options) {
|
|
|
820
835
|
}
|
|
821
836
|
const granted = await acquireSlot(deviceId);
|
|
822
837
|
if (!granted) {
|
|
838
|
+
if (closed || draining)
|
|
839
|
+
return drainingRejectResult();
|
|
823
840
|
return { kind: "rejected", reject: rejection(DEVICE_ERROR_CODES.BUSY, `the device already has ${maxInflightPerDevice} instructions in flight`, 1) };
|
|
824
841
|
}
|
|
825
842
|
if (abortedNow()) {
|
|
826
843
|
releaseSlot(deviceId);
|
|
827
844
|
return abortedBeforeCommit();
|
|
828
845
|
}
|
|
846
|
+
if (closed) {
|
|
847
|
+
releaseSlot(deviceId);
|
|
848
|
+
return drainingRejectResult();
|
|
849
|
+
}
|
|
829
850
|
{
|
|
830
851
|
const drift = driftNow();
|
|
831
852
|
if (drift.length > 0) {
|
|
@@ -975,9 +996,14 @@ export function createDeviceWsHub(options) {
|
|
|
975
996
|
deliverCancel(p);
|
|
976
997
|
return Promise.resolve();
|
|
977
998
|
}
|
|
978
|
-
function beginDrain() {
|
|
999
|
+
function beginDrain(opts) {
|
|
1000
|
+
if (!draining) {
|
|
1001
|
+
drainStartMono = performance.now();
|
|
1002
|
+
drainGraceMs = opts?.graceMs;
|
|
1003
|
+
}
|
|
979
1004
|
draining = true;
|
|
980
|
-
|
|
1005
|
+
const graceNow = typeof drainGraceMs === "function" ? drainGraceMs() : drainGraceMs;
|
|
1006
|
+
logger?.info?.("device_hub_draining", { connections: conns.size, pending: pending.size, ...(graceNow === undefined ? {} : { graceMs: graceNow }) });
|
|
981
1007
|
}
|
|
982
1008
|
async function close() {
|
|
983
1009
|
closed = true;
|
|
@@ -108,6 +108,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
108
108
|
sendJson(res, 200, {
|
|
109
109
|
pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
|
|
110
110
|
...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
|
|
111
|
+
...(deps.approvalAskAudit ? { crashConverged: deps.approvalAskAudit.listCrashConverged(scope) } : {}),
|
|
111
112
|
});
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O4 设备换绑 wire + S-4 additive 投影(DEBTS [ref];设计真源 = sema-internal
|
|
3
|
+
* `server/designs/2026-08-30-o4-device-rebind-wire-v2.md`,冻结稿)—— `/v1/devices/*` 管理面的
|
|
4
|
+
* **首批三动词**(capabilities.ts 那条「车C 的 `/v1/devices/*`」预留注所指的族,本文件开族):
|
|
5
|
+
*
|
|
6
|
+
* · `GET /v1/devices` —— 设备列表(owner 域过滤;explicit operator 全量)。行 =
|
|
7
|
+
* `{ deviceId, name?, status: "online"|"offline", lastSeenAt? }`(v2 稿 §4 冻结形;`status` 由
|
|
8
|
+
* **hub 在场性**判 —— v1 部署形 = 单副本(基线稿 §4.6),汇聚端的活连接集就是本副本的 presence
|
|
9
|
+
* 真源;`name` = displayName,空串诚实缺席;`lastSeenAt` ISO-8601)。**吊销行不列**:revoked 是
|
|
10
|
+
* 终态(重注册 = 新 deviceId),列出来只会被壳当换绑目标然后撞 404。
|
|
11
|
+
* · `GET /v1/devices/sessions/:rootSessionId` —— S-4 绑定读面(壳换绑 UX 的全部材料):
|
|
12
|
+
* `{ rootSessionId, boundDevice: { deviceId, rev, boundAt } }`。稿 §4 允许「并入 session 读面」或
|
|
13
|
+
* 「独立读面」两形,选**独立读面**:sessions 的 GET 面承诺「无参请求字节不变」(routes/sessions.ts
|
|
14
|
+
* 的 additive contract 注),且 audit 投影由 boot/session-faces 组装、不持 device 店 —— 并入即跨
|
|
15
|
+
* 装配缝加一次店读;独立读面把 device wire 族收在一个文件里,户口与门序自洽。
|
|
16
|
+
* · `POST /v1/devices/sessions/:rootSessionId/rebind` —— 显式换绑动词(v2 稿 §2 门序五步,逐字):
|
|
17
|
+
* ① gatedPrincipal + owner 门(非属主 404 反枚举同串,[ref] 纪律)
|
|
18
|
+
* ② 目标设备在场门(不存在 / 不是该域的 / 已吊销 ⇒ 404 **同串** —— 设备 id 也是不可枚举面)
|
|
19
|
+
* ③ 无活跃 run 门(**CAS 前判、CAS 语句不兼职**;durable 判据 = run store 的 session claim
|
|
20
|
+
* `getActiveTaskId`,hub 内存在途计数不作门 —— 内存态跨副本不可见;拒 = 409
|
|
21
|
+
* `device.rebind_active_run` 体带 `activeTaskId`)
|
|
22
|
+
* ④ 前态 CAS(店内单事务:`rev` 恒变列 + `session_rebound` 审计行;输 = 409
|
|
23
|
+
* `device.rebind_rev_conflict` 体带 `currentRev` —— [ref] currentPending 哲学,壳一跳重定位)
|
|
24
|
+
* ⑤ 赢 = 200 `{ rootSessionId, deviceId, rev, workspaceCarryover: "none" }`。
|
|
25
|
+
* 幂等形:toDeviceId ≡ 当前 ∧ expectedRev ≡ 当前 rev ⇒ 200 回显不加 rev(真幂等,不写审计)。
|
|
26
|
+
* `workspaceCarryover: "none"` 是 **wire 冻结字面**(v2 稿 §5):换绑只改路由,不搬字节 —— 壳据
|
|
27
|
+
* 此渲「新设备上是全新工作区」的诚实披露;两个 200 形恒带。
|
|
28
|
+
*
|
|
29
|
+
* ── 调用方域(owner domain)的唯一铸造点 ─────────────────────────────────────────────────────────
|
|
30
|
+
* device lane 的三表自持 O11 复合身份 `{owner_tenant, owner_subject}`(device-store.ts 头注;全局
|
|
31
|
+
* principal 一个字节不动)。基线稿 §11-O11 的落法:「认证需在 SSO 校验点把 tenant+subject 传到 device
|
|
32
|
+
* placement 校验腿」——对位到本仓的真源即:subject = `gatedPrincipal`(verified 链),tenant =
|
|
33
|
+
* `ssoVerifiedScope`(registry-JWT 的 `scope` claim,security.ts:同为签名真源、非自报头)。两者齐备
|
|
34
|
+
* 才构成一个 device 域;缺 tenant claim 的凭据(纯 principal 头 / 静态 token 直连)**判不出域** ⇒
|
|
35
|
+
* 非 operator 一律响亮 403(fail-closed + loud,[ref]:静默空列表是假健康)——基线稿对 enrollment 的
|
|
36
|
+
* 同向裁定是「缺 tenant/global scope 明确拒注册」。将来的 enrollment/管理端点(车C 余件)**必须**从
|
|
37
|
+
* 这同一个函数取域,不得手抄第二份派生。
|
|
38
|
+
*
|
|
39
|
+
* ── 反枚举 404 的单一铸造点 ──────────────────────────────────────────────────────────────────────
|
|
40
|
+
* 「绑定不在 / 不是你的 / 目标设备不在 / 不是该域的 / 已吊销」五臂共用 {@link sendDeviceNotFound} ——
|
|
41
|
+
* 判别位在一个函数里就消失,memory `anti-enum-404-ambiguity` 的教训(判别位留在上层 = 迟早有一个
|
|
42
|
+
* 调用点忘记归一)在结构上关掉。码/文案与 device-enrollment.ts 的 `rejectNotFound` 同串
|
|
43
|
+
* (`not_found.device` + "device not found")。
|
|
44
|
+
*
|
|
45
|
+
* ── 门与失败方向 ────────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
* 活跃 run 门的 store 读**不吞错**(server.ts 有两处 `getActiveTaskId(...).catch(() => undefined)`,
|
|
47
|
+
* 那是观测面;这里是门 —— store 打不通就 500,绝不当「没有活跃 run」放行,[ref])。run store 缺席 =
|
|
48
|
+
* 501 `capability.run_store_required`(门无判据即拒,不是跳过)。
|
|
49
|
+
*
|
|
50
|
+
* billable = false 全族(纯管理/读面,零模型工作);刻意不吃 drain 503(读面与管理动作在排空期
|
|
51
|
+
* 照常有效 —— 换绑本身正是「设备要换了」的运维动作)。错误码登记:附录 A + error-code-catalog-live
|
|
52
|
+
* (send 点全部写**字面量** + `satisfies DeviceErrorCode` 钉闭集 —— 目录门扫的是字面量,闭集纪律由
|
|
53
|
+
* 编译器执行,两道门同时成立;device-store.ts「禁手抄」条的本义是禁**脱离闭集**的字面量)。
|
|
54
|
+
*/
|
|
55
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
56
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
57
|
+
import { type DeviceOwner } from "../../device-store.js";
|
|
58
|
+
/**
|
|
59
|
+
* 调用方 device 域的**唯一铸造点**(文件头注)。undefined = 判不出域(无 principal,或无 tenant
|
|
60
|
+
* claim)——消费点必须走 explicit operator 或响亮 403,禁静默空视图。
|
|
61
|
+
*/
|
|
62
|
+
export declare function deviceOwnerDomainOf(req: IncomingMessage, config: RouteCtx["deps"]["config"]): DeviceOwner | undefined;
|
|
63
|
+
export declare function handleDevices(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
64
|
+
//# sourceMappingURL=devices.d.ts.map
|