From ceb8a7b165ee0f21b066b3950daafa17e340a631 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 13 Oct 2026 09:39:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor(proc)!:=20=E7=89=88=E6=9C=AC=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E5=8F=AA=E7=94=B1=E7=89=88=E6=9C=AC=E5=8F=B7=E6=9C=AC?= =?UTF-8?q?=E8=BA=AB=E6=89=BF=E8=BD=BD=EF=BC=8C=E5=88=A0=E9=99=A4=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E8=87=AA=E5=AE=9A=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内核侧配合 SDK 仓 b2872f3:兼容性不再依赖任何自造版本号。 删除三处自造号: - ProtocolVersion(协议号)→ 改为比对主版本段(MajorVersion) - shmVersion(共享段 header)→ 改为校验描述符个数(field_count) - unifiedVersion(SuperBlock)→ 结构自描述,不再校验 握手契约随之调整: HandshakeParams: protocol(int) → core_version(string) HandshakeResult: protocol(int) → sdk_version 承担判据 内核比对 MajorVersion(meta.Version) == MajorVersion(res.SDKVersion), 跨主版本即拒并提示重编(主版本跃迁 = 破坏性变更的定义)。 为什么废除:额外的号需要人去记得升,而它没有任何结构性约束兜底, 必然腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次, 对应的守卫测试长期失败无人察觉(期望 18/1,实际 19/2)。 新增判据: - TestProcess_MajorVersionMismatchRejected:跨主版本被拒(testdata/badproto) - TestProcess_SameMajorDifferentMinorAccepted:同主版本放行(新增 testdata/oldminor) 后者是 §七「1.x 内纯追加、不调新面不用重编」规约的落地 —— 若误用全等 比较,未重编的存量插件会被无谓地全部拒绝。 两条均经变异验证:改成全等比较 → 前者过/后者挂;删掉校验 → 前者挂。 --- docs/git-branching.md | 18 ++++-- internal/meta/meta.go | 10 ++-- internal/plugin/proc/process.go | 14 ++--- internal/plugin/proc/process_test.go | 29 +++++++--- internal/plugin/proc/protocol.go | 48 ++++++++++------ internal/plugin/proc/shm.go | 46 ++++++++------- internal/plugin/proc/testdata/appendplugin.go | 2 +- .../plugin/proc/testdata/badprotoplugin.go | 8 ++- .../plugin/proc/testdata/callbackplugin.go | 3 +- internal/plugin/proc/testdata/crashplugin.go | 2 +- internal/plugin/proc/testdata/echoplugin.go | 3 +- internal/plugin/proc/testdata/forkplugin.go | 2 +- internal/plugin/proc/testdata/hangplugin.go | 2 +- .../plugin/proc/testdata/oldminorplugin.go | 57 +++++++++++++++++++ .../plugin/proc/testdata/readonlyplugin.go | 2 +- internal/plugin/proc/testdata/stageplugin.go | 2 +- internal/plugin/proc/unified.go | 14 ++--- third_party/homeagent-sdk/meta/meta.go | 10 ++-- 18 files changed, 184 insertions(+), 88 deletions(-) create mode 100644 internal/plugin/proc/testdata/oldminorplugin.go diff --git a/docs/git-branching.md b/docs/git-branching.md index 09609b70..78ba0e51 100644 --- a/docs/git-branching.md +++ b/docs/git-branching.md @@ -496,12 +496,20 @@ GITCODE_REPO=JianFeeeee/homeagent-sdk ASSET_DIR=/dist/release \ 测试替身(`fakeCoreSDK`、`injectCapture`、`capability_test.go` 的手工方法清单)。 还要同步 `yaegi/mocksdk`——它没有任何代码对着编译,漂移**不会被编译器抓到**。 -### 4. 「接口纯追加」不等于「无需重编」 +### 4. 兼容性只认主版本号(不再有独立的协议号) -插件运行协议版本(`ProtocolVersion`)与 SDK 接口版本是**两件事**。 -协议升级(如 1.2.0 的 fd3 布局变更,不支持滚动升级)时,`ProtocolVersion` 不匹配 -会在握手时被明确拒绝并提示用配套 `hmapdev` 重编。 -必须把两者分开说,否则会被误读成"既然纯追加就还能用旧产物"。 +**不自定协议版本号**:兼容性由双方版本号的**主版本段**表态 +(内核 `meta.Version` ↔ 插件 `sdk_version`)。主版本相同即兼容,跨主版本则 +在握手时被拒绝并提示用配套 `hmapdev` 重编。 + +为什么不要独立协议号:语义版本变动频繁(修 bug、加字段),额外的号必然被 +漏升而腐烂(历史实证:曾有一个 `ProtocolVersion` 常量,多次布局变更里只升过 +一次,而对应守卫测试长期失败无人察觉)。兼容性只挂在**始终会被发的那个号**上, +就不存在「忘同步」这个失效模式。 + +布局一致性(如 1.2.0 的 fd3 → 统一共享区)另由**结构派生事实**把关: +统一区魔数 + `field_count`(描述符个数)在握手上显式校验。 +它们是二进制里的实际值,不需人去维护,也不同“谁记得升号”。 ### 5. 合回 main 前要同步的东西 diff --git a/internal/meta/meta.go b/internal/meta/meta.go index c256b85a..76f6ef33 100644 --- a/internal/meta/meta.go +++ b/internal/meta/meta.go @@ -88,15 +88,15 @@ func FullVersion() string { return KernelName + " v" + Version + " (" + Commit + ")" } -// ---- 协议版本 ---- +// ---- 协议兼容性 ---- // -// 子进程 RPC 的协议版本是一个独立的小整数,与内核语义版本解耦: -// 语义版本变动频繁(修 bug、加字段),而 wire 协议只在**帧格式或握手语义** -// 变化时才升。当前值见 internal/plugin/proc/protocol.go 的 ProtocolVersion。 +// **不自定协议号**:兼容性只认双方版本号的**主版本段**。 +// 主版本相同即兼容(1.x 内全是纯追加);跨主版本则是破坏性变更 +// (如 1.0.0 从 C ABI 改子进程),握手时显式拒绝并提示重编。 // // C ABI 时代的 ABIVersion / CABINum / 51 个 Core 整数 ID 已随 // Part 6.2 删除 internal/plugin/cabi/ 一并退场: // - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量) -// - 版本协商改为握手帧里的 protocol 字段 +// - 版本协商改为握手帧里的 core_version / sdk_version 主版本比对 // // 保留那些常量只会让人以为它们还在生效。 diff --git a/internal/plugin/proc/process.go b/internal/plugin/proc/process.go index 6f11a0d9..e2284b3e 100644 --- a/internal/plugin/proc/process.go +++ b/internal/plugin/proc/process.go @@ -255,10 +255,9 @@ func (p *Process) handshake(timeout time.Duration) error { defer cancel() raw, err := p.CallContext(ctx, MethodHandshake, HandshakeParams{ - Protocol: ProtocolVersion, CoreVersion: meta.Version, PluginName: p.name, - ShmVersion: shmVersion, + FieldCount: uint32(stageFieldCount), ShmSize: p.shmSize, EvtRingSize: p.evtRingSize, }) @@ -269,12 +268,13 @@ func (p *Process) handshake(timeout time.Duration) error { if err := json.Unmarshal(raw, &res); err != nil { return fmt.Errorf("proc: %s 握手应答解析失败: %w", p.name, err) } - if res.Protocol != ProtocolVersion { - return fmt.Errorf("proc: %s 协议版本不匹配(插件 %d,内核 %d)——请用配套 hmapdev 重编", - p.name, res.Protocol, ProtocolVersion) + // 主版本号是唯一兼容判据:跨主版本必是被破坏性变更分开的两端。 + if want, got := MajorVersion(meta.Version), MajorVersion(res.SDKVersion); want != got { + return fmt.Errorf("proc: %s 主版本不匹配(插件 SDK %s,内核 %s)——请用配套 hmapdev 重编", + p.name, res.SDKVersion, meta.Version) } - log.Printf("[proc] %s 已建链(pid=%d protocol=%d sdk=%s)", - p.name, p.PID(), res.Protocol, res.SDKVersion) + log.Printf("[proc] %s 已建链(pid=%d sdk=%s core=%s)", + p.name, p.PID(), res.SDKVersion, meta.Version) return nil } diff --git a/internal/plugin/proc/process_test.go b/internal/plugin/proc/process_test.go index e823c3d1..830fa9e7 100644 --- a/internal/plugin/proc/process_test.go +++ b/internal/plugin/proc/process_test.go @@ -315,19 +315,19 @@ func TestProcess_ConcurrentCallsRouteCorrectly(t *testing.T) { } } -// 协议版本不匹配必须显式拒绝,不能半兼容运行。 +// 主版本不匹配必须显式拒绝,不能半兼容运行。 // -// v2 引入的必要性就靠这条:v1 插件(只读内联 args)遇上 v2 内核,跟 v2 插件 -// 发 blocks_ref 给 v1 内核,都会静默失效、不报任何错。只有在这里显式拦下, -// 那双错配才会变成一条带修复指令的启动失败。 -func TestProcess_ProtocolMismatchRejected(t *testing.T) { +// 为什么需要显式拦下:主版本两端(如 C ABI 时代与子进程时代)的帧格式与 +// 共享段布局都不同,错配不会报错、只会静默失效(空参数、字段被忽略)。 +// 判据是双方版本号的**主版本段**,不是任何自定协议号。 +func TestProcess_MajorVersionMismatchRejected(t *testing.T) { bin := buildTestPlugin(t, "badprotoplugin.go") _, err := Spawn("badproto", bin, Options{Handler: noopHandler}) if err == nil { - t.Fatal("协议版本不匹配应拒绝建链") + t.Fatal("主版本不匹配应拒绝建链") } - if !strings.Contains(err.Error(), "协议版本不匹配") { - t.Errorf("错误应说明版本不匹配,实际: %v", err) + if !strings.Contains(err.Error(), "主版本不匹配") { + t.Errorf("错误应说明主版本不匹配,实际: %v", err) } // 运维可读性:光报“不匹配”不能定位到行动。生产上碰到它的现场是 // “只更新了内核没重编插件”,所以错误里必须带出这条修复指令。 @@ -336,6 +336,19 @@ func TestProcess_ProtocolMismatchRejected(t *testing.T) { } } +// 同主版本内的中版本差异(内核 1.4.0 vs 插件 SDK 1.1.0)必须**允许**建链。 +// +// 这是 §七「1.x 内纯追加、不调新面就不用重编」规约的落地:若不校验主版本 +// 而是全等比较,未重编的存量插件会被无谓地全部拒绝。 +func TestProcess_SameMajorDifferentMinorAccepted(t *testing.T) { + bin := buildTestPlugin(t, "oldminorplugin.go") + p, err := Spawn("oldminor", bin, Options{Handler: noopHandler}) + if err != nil { + t.Fatalf("同主版本(1.x)应允许建链: %v", err) + } + defer func() { _ = p.Stop() }() +} + func TestProcess_SpawnRequiresHandler(t *testing.T) { if _, err := Spawn("x", "/bin/true", Options{}); err == nil { t.Fatal("缺少 Handler 应报错(插件无法回调内核)") diff --git a/internal/plugin/proc/protocol.go b/internal/plugin/proc/protocol.go index 22f34d40..08146976 100644 --- a/internal/plugin/proc/protocol.go +++ b/internal/plugin/proc/protocol.go @@ -1,6 +1,9 @@ package proc -import "encoding/json" +import ( + "encoding/json" + "strings" +) // RPC 协议定义:控制面(§3.2 method id 平移为 method 名)。 // @@ -12,17 +15,29 @@ import "encoding/json" // JSON 序列化 3-8 µs 对比 LLM 单轮 2-8 秒占 0.0001%,可读性与可调试性更值。 // 大 payload(多媒体二进制)走共享内存 arena,不进 RPC 帧(§3.3 实验 10:18-22x)。 -// 协议版本:与共享段版本独立演进。 -// 插件握手时上报,内核校验——不匹配显式拒绝,避免半兼容导致的诡异行为。 +// 协议兼容性判据:**大版本号唯一**。 // -// v2(§13.6 / §13.13):内核→插件的 payload 改为调用帧承载。 -// v1 插件只读内联 args,遇上 v2 内核会拿到空参数;v2 插件发 blocks_ref, -// v1 内核反序列化时静默忽略(旧内核 io.setToolBlocks 还是桩)。两种错配 -// 都不会报错,只会静默失效——所以必须 bump 版本,让它在握手上就**显式**失败。 +// 不自定协议号。插件的兼容性由双方各自的版本号表态:内核侧 meta.Version、 +// 插件侧 sdk_version(SDK 版本跟随内核中版本,patch 恒为 .0)。 +// 版本号唯一事实源就是这两个 meta.Version(见 docs/zh/ci-cd-runbook.md)。 // -// 部署纪律:内核与全部插件必须同批重建、同批安装;改协议就要改这个常量, -// 不得依赖“两边大致兼容”。 -const ProtocolVersion = 2 +// 判据是**主版本相同**: +// - 1.x ↔ 1.y:兼容。1.0.0 之后全是纯追加,不调新面就不受影响。 +// - 跨主版本(如 1.x → 2.x):拒绝建链,提示用配套 hmapdev 重编。 +// 主版本跃迁正是**破坏性变更**的定义(1.0.0 从 C ABI 改子进程即属此列), +// 所以它天然是正确的分界,不需要另一个号来复述这件事。 +// +// 布局一致性另由 stageFieldCount(描述符个数)把关:它是结构的派生事实, +// 也不是版本号。两者错配都在握手上显式失败,不半兼容运行。 + +// MajorVersion 取版本号的主版本段("1.4.0" → "1","" → "")。 +func MajorVersion(v string) string { + v = strings.TrimPrefix(strings.TrimSpace(v), "v") + if i := strings.IndexByte(v, '.'); i >= 0 { + return v[:i] + } + return v +} // Direction 无需显式字段:靠 Method 是否为空区分请求与响应 // (与 clawhubadapter/sidecar 的成熟做法一致)。 @@ -184,19 +199,20 @@ const ( // HandshakeParams 是内核 → 插件的建链首帧:告知内核侧规格。 type HandshakeParams struct { - Protocol int `json:"protocol"` // 内核支持的协议版本 - CoreVersion string `json:"core_version"` // 内核版本(诊断用) + CoreVersion string `json:"core_version"` // 内核版本(插件据此判主版本兼容) PluginName string `json:"plugin_name"` // 内核分配的插件名 - ShmVersion uint32 `json:"shm_version"` - ShmSize int `json:"shm_size"` + // FieldCount 是 StageContext 描述符个数。插件据此与自己的编译期 + // 字段数比对:不匹配意味着标志位基址会错位,必须重编(而非静默错读)。 + FieldCount uint32 `json:"field_count"` + ShmSize int `json:"shm_size"` // EvtRingSize 是事件环段大小(0 表示不支持事件环)。插件据此 mmap fd 4。 EvtRingSize int `json:"evt_ring_size,omitempty"` } // HandshakeResult 是插件 → 内核的建链应答:上报自身信息。 type HandshakeResult struct { - Protocol int `json:"protocol"` // 必须等于 ProtocolVersion - SDKVersion string `json:"sdk_version"` // 插件编译时链接的公开 SDK 版本 + // SDKVersion 是插件编译时链接的公开 SDK 版本。内核据此判主版本兼容。 + SDKVersion string `json:"sdk_version"` PluginName string `json:"plugin_name"` PID int `json:"pid"` } diff --git a/internal/plugin/proc/shm.go b/internal/plugin/proc/shm.go index f75986eb..baabe5d3 100644 --- a/internal/plugin/proc/shm.go +++ b/internal/plugin/proc/shm.go @@ -1,7 +1,7 @@ // Package proc 实现外部插件的子进程加载通道(plugin.bin)。 // // 数据面设计:定长头 + arena,变长数据用相对基址的 {off,len} 描述符 -//(相对偏移是关键:各进程 mmap 到不同虚拟地址也能正确解引用)。 +// (相对偏移是关键:各进程 mmap 到不同虚拟地址也能正确解引用)。 // // homed ──spawn──> plugin(纯 Go 二进制,无 cgo) // ├── stdio JSON-RPC 控制面:51 个 method id 平移为 method 名(§3.2) @@ -17,19 +17,22 @@ import ( "sync/atomic" ) -// 共享段魔数与版本,用于挂载时校验对端布局一致。 +// shmMagic 标识 HomeAgent 的 StageContext 共享段。 // -// v2(2026-10-12):新增 fExtraBag —— Extra 具名 4 键之外的插件自建键 -// (如 qq 的 qq_auth 绑帧身份)跨进程/跨 stage 透传。加字段必升版: -// AttachSegment 与插件握手对版本失败即拒,旧插件不会静默错读新布局。 +// **不设版本号**:段布局自描述 —— 内核把描述符个数写进 header +// (offFieldCount),插件据此定位标志位,不再用自己编译期的常量推算。 +// 这样「加字段」不再需要一个人工维护、必然腐烂的版本号来兜底: +// 布局不一致由描述符个数这种**派生事实**自动检出(见 AttachSegment)。 +// +// 协议兼容性由双方版本号的**主版本**在握手上校验(见 protocol.go 的 +// MajorVersion)。 const ( - shmMagic uint32 = 0x48415348 // "HASH" — HomeAgent SHared - shmVersion uint32 = 2 + shmMagic uint32 = 0x48415348 // "HASH" — HomeAgent SHared ) // 段布局(所有偏移均相对**段起始**,arena 内偏移相对 arenaBase): // -// [0, headerSize) Header:魔数/版本/arena 游标 +// [0, headerSize) Header:魔数/描述符个数/arena 游标 // [headerSize, ctxEnd) ShmStageCtx:每字段一个 Slice{off,len} 描述符 // [arenaBase, arenaBase+arenaCap) arena:append-only 变长数据区 // @@ -38,13 +41,13 @@ const ( headerSize = 64 // Header 内字段偏移 - offMagic = 0 // uint32 - offVersion = 4 // uint32 - offArenaBase = 8 // uint32 - offArenaCap = 12 // uint32 - offArenaUsed = 16 // uint32(原子 bump 游标) - offCtxBase = 20 // uint32 - offSeq = 24 // uint64:每次成功写回自增,供乐观读校验 + offMagic = 0 // uint32 + offFieldCount = 4 // uint32:StageContext 描述符个数(插件据此定位标志位) + offArenaBase = 8 // uint32 + offArenaCap = 12 // uint32 + offArenaUsed = 16 // uint32(原子 bump 游标) + offCtxBase = 20 // uint32 + offSeq = 24 // uint64:每次成功写回自增,供乐观读校验 ) // Slice 是 arena 内变长数据的描述符,off 相对 arenaBase。 @@ -136,7 +139,7 @@ func NewSegment(data []byte) (*Segment, error) { arenaCap := uint32(len(data)) - arenaBase binary.LittleEndian.PutUint32(data[offMagic:], shmMagic) - binary.LittleEndian.PutUint32(data[offVersion:], shmVersion) + binary.LittleEndian.PutUint32(data[offFieldCount:], uint32(stageFieldCount)) binary.LittleEndian.PutUint32(data[offArenaBase:], arenaBase) binary.LittleEndian.PutUint32(data[offArenaCap:], arenaCap) binary.LittleEndian.PutUint32(data[offArenaUsed:], 0) @@ -151,7 +154,10 @@ func NewSegment(data []byte) (*Segment, error) { } // AttachSegment 挂载一块已由 NewSegment 初始化的区域(插件进程侧调用)。 -// 校验魔数与版本,避免版本不一致时静默错读。 +// +// 校验魔数与**描述符个数**(而非人工版本号):个数是段布局的派生事实, +// 加字段时自动变化,因此不需要谁记得去升一个号。插件若按旧字段数推算 +// 标志位基址会静默错位,这里显式拒绝。 func AttachSegment(data []byte) (*Segment, error) { if len(data) < headerSize+ctxSize { return nil, fmt.Errorf("proc: 共享段过小(%d 字节)", len(data)) @@ -159,9 +165,9 @@ func AttachSegment(data []byte) (*Segment, error) { if got := binary.LittleEndian.Uint32(data[offMagic:]); got != shmMagic { return nil, fmt.Errorf("proc: 共享段魔数不匹配(0x%x,期望 0x%x)", got, shmMagic) } - if got := binary.LittleEndian.Uint32(data[offVersion:]); got != shmVersion { - return nil, fmt.Errorf("proc: 共享段版本不匹配(%d,本内核 %d)——插件需用配套 hmapdev 重编", - got, shmVersion) + if got := binary.LittleEndian.Uint32(data[offFieldCount:]); got != uint32(stageFieldCount) { + return nil, fmt.Errorf("proc: 共享段描述符个数不匹配(%d,本内核 %d)——插件需用配套 hmapdev 重编", + got, stageFieldCount) } return &Segment{data: data}, nil } diff --git a/internal/plugin/proc/testdata/appendplugin.go b/internal/plugin/proc/testdata/appendplugin.go index 5d2d062e..e1e613b9 100644 --- a/internal/plugin/proc/testdata/appendplugin.go +++ b/internal/plugin/proc/testdata/appendplugin.go @@ -187,7 +187,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", + "sdk_version": "1.0.0", "plugin_name": "append-" + tag, "pid": os.Getpid(), }}) diff --git a/internal/plugin/proc/testdata/badprotoplugin.go b/internal/plugin/proc/testdata/badprotoplugin.go index a119d8ba..c7dc3444 100644 --- a/internal/plugin/proc/testdata/badprotoplugin.go +++ b/internal/plugin/proc/testdata/badprotoplugin.go @@ -1,6 +1,9 @@ //go:build ignore -// badprotoplugin 上报错误的协议版本,验证内核显式拒绝而非半兼容运行。 +// badprotoplugin 上报不同主版本的 SDK,验证内核显式拒绝而非半兼容运行。 +// +// 主版本跃迁是破坏性变更的定义(如 1.0.0 从 C ABI 改子进程), +// 所以它是兼容性的正确分界——不需要另一个自定的协议号来复述。 package main import ( @@ -38,8 +41,7 @@ func main() { } if req.Method == "handshake" { send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 999, // 故意不匹配 - "sdk_version": "ancient", + "sdk_version": "99.0.0", // 主版本不同,故意不匹配 "plugin_name": "badproto", "pid": os.Getpid(), }}) diff --git a/internal/plugin/proc/testdata/callbackplugin.go b/internal/plugin/proc/testdata/callbackplugin.go index 05bfb41a..de66f6d6 100644 --- a/internal/plugin/proc/testdata/callbackplugin.go +++ b/internal/plugin/proc/testdata/callbackplugin.go @@ -95,8 +95,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, - "sdk_version": "test", + "sdk_version": "1.0.0", "plugin_name": "cb", "pid": os.Getpid(), }}) diff --git a/internal/plugin/proc/testdata/crashplugin.go b/internal/plugin/proc/testdata/crashplugin.go index c483b246..9627f2ad 100644 --- a/internal/plugin/proc/testdata/crashplugin.go +++ b/internal/plugin/proc/testdata/crashplugin.go @@ -40,7 +40,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", "plugin_name": "crash", "pid": os.Getpid(), + "sdk_version": "1.0.0", "plugin_name": "crash", "pid": os.Getpid(), }}) case "tool.invoke": // 模拟插件 bug:直接 panic,进程带非零码退出 diff --git a/internal/plugin/proc/testdata/echoplugin.go b/internal/plugin/proc/testdata/echoplugin.go index 8428a609..3892fc13 100644 --- a/internal/plugin/proc/testdata/echoplugin.go +++ b/internal/plugin/proc/testdata/echoplugin.go @@ -43,8 +43,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, - "sdk_version": "test", + "sdk_version": "1.0.0", "plugin_name": "echo", "pid": os.Getpid(), }}) diff --git a/internal/plugin/proc/testdata/forkplugin.go b/internal/plugin/proc/testdata/forkplugin.go index be0d2c61..53d69b78 100644 --- a/internal/plugin/proc/testdata/forkplugin.go +++ b/internal/plugin/proc/testdata/forkplugin.go @@ -57,7 +57,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", "plugin_name": "fork", "pid": os.Getpid(), + "sdk_version": "1.0.0", "plugin_name": "fork", "pid": os.Getpid(), }}) case "tool.invoke": // 不回应答,直接退出:模拟插件突然死亡(崩溃/被 kill)。 diff --git a/internal/plugin/proc/testdata/hangplugin.go b/internal/plugin/proc/testdata/hangplugin.go index c73e528f..6f37007e 100644 --- a/internal/plugin/proc/testdata/hangplugin.go +++ b/internal/plugin/proc/testdata/hangplugin.go @@ -42,7 +42,7 @@ func main() { switch req.Method { case "handshake": send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", "plugin_name": "hang", "pid": os.Getpid(), + "sdk_version": "1.0.0", "plugin_name": "hang", "pid": os.Getpid(), }}) case "tool.invoke": // 永久卡住,永不回应答 diff --git a/internal/plugin/proc/testdata/oldminorplugin.go b/internal/plugin/proc/testdata/oldminorplugin.go new file mode 100644 index 00000000..254a2593 --- /dev/null +++ b/internal/plugin/proc/testdata/oldminorplugin.go @@ -0,0 +1,57 @@ +//go:build ignore + +// oldminorplugin 上报同主版本但更早的中版本 SDK,验证内核**不会**无谓拒绝。 +// +// 规约(docs/git-branching.md §七.1):1.x 内的中版本跃迁全部是纯追加 —— +// 新增方法由插件调用、内核实现,不调就不受影响。因此未重编的存量插件 +// (如用 SDK 1.0.0/1.1.0 编的)必须能在新内核上直接建链。 +// +// 若判据误用「全等比较」而非「主版本比较」,这条会失败 —— 正是它的价值。 +package main + +import ( + "bufio" + "encoding/json" + "os" +) + +type request struct { + ID uint64 `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type response struct { + ID uint64 `json:"id"` + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +func main() { + in := bufio.NewScanner(bufio.NewReader(os.Stdin)) + out := bufio.NewWriter(os.Stdout) + send := func(v interface{}) { + b, _ := json.Marshal(v) + out.Write(b) + out.WriteByte('\n') + out.Flush() + } + + for in.Scan() { + var req request + if err := json.Unmarshal(in.Bytes(), &req); err != nil { + continue + } + if req.Method == "handshake" { + send(response{ID: req.ID, Result: map[string]interface{}{ + "sdk_version": "1.0.0", // 同主版本,更早的中版本 + "plugin_name": "oldminor", + "pid": os.Getpid(), + }}) + continue + } + if req.ID != 0 { + send(response{ID: req.ID}) + } + } +} diff --git a/internal/plugin/proc/testdata/readonlyplugin.go b/internal/plugin/proc/testdata/readonlyplugin.go index cb04d320..15eac281 100644 --- a/internal/plugin/proc/testdata/readonlyplugin.go +++ b/internal/plugin/proc/testdata/readonlyplugin.go @@ -139,7 +139,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", "plugin_name": "readonly", "pid": os.Getpid(), + "sdk_version": "1.0.0", "plugin_name": "readonly", "pid": os.Getpid(), }}) case "plugin.init": diff --git a/internal/plugin/proc/testdata/stageplugin.go b/internal/plugin/proc/testdata/stageplugin.go index 488497d2..9e07ae25 100644 --- a/internal/plugin/proc/testdata/stageplugin.go +++ b/internal/plugin/proc/testdata/stageplugin.go @@ -354,7 +354,7 @@ func main() { shm = m[ctxOff : ctxOff+ctxSize] } send(response{ID: req.ID, Result: map[string]interface{}{ - "protocol": 2, "sdk_version": "test", "plugin_name": "stage", "pid": os.Getpid(), + "sdk_version": "1.0.0", "plugin_name": "stage", "pid": os.Getpid(), }}) case "plugin.init": diff --git a/internal/plugin/proc/unified.go b/internal/plugin/proc/unified.go index f9855efe..098865b9 100644 --- a/internal/plugin/proc/unified.go +++ b/internal/plugin/proc/unified.go @@ -34,8 +34,7 @@ import ( const ( // unifiedMagic 标识统一共享内存区域。 - unifiedMagic uint32 = 0x554D5352 // "UMSR" — Unified Memory Shared Region - unifiedVersion uint32 = 1 + unifiedMagic uint32 = 0x554D5352 // "UMSR" — Unified Memory Shared Region superBlockSize = 64 // SuperBlock 占前 64 字节 ) @@ -43,7 +42,7 @@ const ( // SuperBlock 布局:64 字节,所有偏移相对区域起始。 // // [0,4) magic -// [4,8) version +// [4,8) reserved(原 version,已废:SuperBlock 靠 magic + capacity 自描述) // [8,16) generation(atomic uint64,resize 时 bump) // [16,20) capacity(区域总字节数) // [20,24) ctxOff(StageContext segment 偏移) @@ -55,7 +54,7 @@ const ( // [44,64) reserved const ( sbOffMagic = 0 - sbOffVersion = 4 + sbOffReserved0 = 4 // 保留(原 version 槽位,布局不变以免动其余偏移) sbOffGeneration = 8 sbOffCapacity = 16 sbOffCtxOff = 20 @@ -133,7 +132,6 @@ func initUnifiedRegion(data []byte, ctxTotal, evtTotal int) (*unifiedRegion, err arenaCap := cap - arenaOff putU32(data[sbOffMagic:], unifiedMagic) - putU32(data[sbOffVersion:], unifiedVersion) putU64(data[sbOffGeneration:], 0) putU32(data[sbOffCapacity:], cap) putU32(data[sbOffCtxOff:], ctxOff) @@ -163,10 +161,8 @@ func attachUnifiedRegion(data []byte) (*unifiedRegion, error) { if magic != unifiedMagic { return nil, fmt.Errorf("unified: 魔数不匹配(0x%x,期望 0x%x)", magic, unifiedMagic) } - ver := getU32(data[sbOffVersion:]) - if ver != unifiedVersion { - return nil, fmt.Errorf("unified: 版本不匹配(%d,期望 %d)", ver, unifiedVersion) - } + // 不自描述校验版本号:SuperBlock 靠 magic + capacity + 各段越界检查把关 + //(这些是结构的派生事实,不存在“忘同步一个人工号”的失效模式)。 ctxOff := getU32(data[sbOffCtxOff:]) ctxSize := getU32(data[sbOffCtxSize:]) evtOff := getU32(data[sbOffEvtOff:]) diff --git a/third_party/homeagent-sdk/meta/meta.go b/third_party/homeagent-sdk/meta/meta.go index c6553c62..59b21373 100644 --- a/third_party/homeagent-sdk/meta/meta.go +++ b/third_party/homeagent-sdk/meta/meta.go @@ -103,15 +103,15 @@ func FullVersion() string { return SDKName + " v" + Version + " (" + Commit + ")" } -// ---- 协议版本 ---- +// ---- 协议兼容性 ---- // -// 子进程 RPC 的协议版本是一个独立的小整数,与 SDK/内核语义版本解耦: -// 语义版本变动频繁(修 bug、加字段),而 wire 协议只在**帧格式或握手语义** -// 变化时才升。当前值见核心仓 internal/plugin/proc/protocol.go 的 ProtocolVersion。 +// **不自定协议号**:兼容性只认双方版本号的**主版本段**。 +// 主版本相同即兼容(1.x 内全是纯追加);跨主版本则是破坏性变更 +// (如 1.0.0 从 C ABI 改子进程),握手时显式拒绝并提示重编。 // // C ABI 时代的 ABIVersion / CABINum / 51 个 Core 整数 ID 已随 // Part 6.2 删除 internal/plugin/cabi/ 一并退场: // - 整数 method id 平移为 method 名字符串(proc/protocol.go 的 Method* 常量) -// - 版本协商改为握手帧里的 protocol 字段 +// - 版本协商改为握手帧里的 core_version / sdk_version 主版本比对 // // 保留那些常量只会让人以为它们还在生效。