diff --git a/internal/lua/plugin_adapter_capability_test.go b/internal/lua/plugin_adapter_capability_test.go new file mode 100644 index 0000000..0c08709 --- /dev/null +++ b/internal/lua/plugin_adapter_capability_test.go @@ -0,0 +1,224 @@ +package lua + +import ( + "fmt" + "strings" + "testing" +) + +// 本文件锁死"插件装的适配器能做到哪些事"——三条能力,缺任何一条, +// 为协议转换类平台(trae SOLO / gemini 信封 / codearts AK/SK 签名) +// 装专用适配器这条路就是死的。都是**实测**得出的机制事实,不是推断。 +// +// 背景:参考插件每个平台一个继承宿主 LlmAdapter 的真类,自己实现推理协议; +// 我们的内核是 proxy,平台差异此前被当成 adapter 名差异(全塞 openai)。 +// 内核其实早已实现 __llmsproxy_adapters.install,只是 channels 插件没用过。 + +// ── 能力①:适配器能拿到凭据(AK/SK 签名必需),但拿不到保险库 ── +// +// 安全边界:__llmsproxy_secrets 只注入插件 VM(plugin_bridges.go 开篇明说 +// "适配器共用 setupGlobals 而不该拿到这些能力")。适配器要凭据只能走 +// build_headers 的 meta.api_key —— 那是**内核按源注入**的,范围受控。 +const credentialProbeAdapter = ` +local adapter = {} +adapter.name = "credprobe" +adapter.version = "1" +adapter.endpoint = "/x" +function adapter.build_headers(meta) + local ak = tostring(meta.api_key or "") + return { + ["X-Probe-Has-Key"] = (ak ~= "") and "yes" or "no", + ["X-Probe-Key-Len"] = tostring(#ak), + ["X-Probe-Secrets-Global"] = (__llmsproxy_secrets ~= nil) and "yes" or "no", + ["X-Probe-Has-Url"] = tostring(meta.url or ""), + ["X-Probe-Method"] = tostring(meta.method or ""), + ["X-Probe-Body-Len"] = tostring(#(meta.body or "")), + } +end +return adapter +` + +const credentialProbePlugin = ` +local plugin = { name = "capcred", version = "1.0" } +plugin.exports = { "install" } +function plugin.install() + return { name = (__llmsproxy_adapters.install("credprobe", [==[` + credentialProbeAdapter + `]==])) } +end +return plugin +` + +func TestAdapterCapabilityCredentialAccess(t *testing.T) { + vm, ps, _ := runtimePlugins(t) + if err := ps.LoadSource("capcred", credentialProbePlugin); err != nil { + t.Fatal(err) + } + if _, err := ps.Call("capcred", "install", nil); err != nil { + t.Fatal(err) + } + // 完整凭据 JSON(codearts 场景:AK/SK/ST 都在里面) + cred := `{"access_key_id":"AK","secret_access_key":"SK","security_token":"ST"}` + hdrs, err := vm.BuildHeaders("credprobe", map[string]interface{}{ + "url": "https://sts.example/v1/oauth2/tokens", "method": "POST", + "body": "grant_type=x", "api_key": cred, + }) + if err != nil { + t.Fatalf("BuildHeaders: %v", err) + } + if hdrs["X-Probe-Has-Key"] != "yes" { + t.Error("适配器必须能通过 meta.api_key 拿到凭据(AK/SK 签名的前提)") + } + if hdrs["X-Probe-Key-Len"] != fmt.Sprint(len(cred)) { + t.Errorf("api_key 长度 %s ≠ %d(凭据被截断?)", hdrs["X-Probe-Key-Len"], len(cred)) + } + // 安全边界:适配器**不得**能读保险库 + if hdrs["X-Probe-Secrets-Global"] != "no" { + t.Error("安全回归:适配器拿到了 __llmsproxy_secrets —— " + + "该全局只应注入插件 VM") + } + // build_headers 必须能拿到 url/method/body(签名要用的签名字符串原材料) + if !strings.Contains(hdrs["X-Probe-Has-Url"], "sts.example") { + t.Error("build_headers 拿不到 url(无法构造签名)") + } + if hdrs["X-Probe-Method"] != "POST" { + t.Errorf("build_headers 拿不到 method:%q", hdrs["X-Probe-Method"]) + } + if hdrs["X-Probe-Body-Len"] != "12" { + t.Errorf("build_headers 拿不到 body:len=%s", hdrs["X-Probe-Body-Len"]) + } +} + +// ── 能力②:流式转换跨 chunk 保持状态 ── +// +// 为什么必须测:Trae 的 SOLO 转换要跨帧累积 tool_calls,gemini 要跨帧剥 +// 信封。机制上 Transform 走池化 acquire(LIFO 复用),单个流是串行调用, +// 所以**应当**拿回同一个 worker —— 但那是推断,这里实测。 +const streamStateAdapter = ` +local adapter = {} +adapter.name = "streambuf" +adapter.version = "1" +adapter.endpoint = "/x" +local calls = 0 +local acc = {} +function adapter.transform_stream_chunk(raw) + calls = calls + 1 + acc[#acc + 1] = raw + return '{"calls":' .. calls .. ',"acc":' .. #acc .. ',"vm":"' .. tostring(adapter) .. '"}' +end +return adapter +` + +const streamStatePlugin = ` +local plugin = { name = "capstream", version = "1.0" } +plugin.exports = { "install" } +function plugin.install() + return { name = (__llmsproxy_adapters.install("streambuf", [==[` + streamStateAdapter + `]==])) } +end +return plugin +` + +func TestAdapterCapabilityStreamStateAcrossChunks(t *testing.T) { + vm, ps, _ := runtimePlugins(t) + if err := ps.LoadSource("capstream", streamStatePlugin); err != nil { + t.Fatal(err) + } + if _, err := ps.Call("capstream", "install", nil); err != nil { + t.Fatal(err) + } + var last, firstVM string + for i := 1; i <= 5; i++ { + out, err := vm.Transform("streambuf", "transform_stream_chunk", + `{"chunk":`+fmt.Sprint(i)+`}`) + if err != nil { + t.Fatalf("chunk %d: %v", i, err) + } + last = out + if i == 1 { + firstVM = out[strings.Index(out, `"vm"`):] + } + } + if !strings.Contains(last, `"calls":5`) { + t.Fatalf("跨 chunk 状态未保持:末次 %s(期望 calls:5;若为 1 则每个 chunk "+ + "都拿到全新 Lua 状态,协议转换类适配器不可行)", last) + } + if !strings.Contains(last, `"acc":5`) { + t.Fatalf("累积数组未跨 chunk 保持:%s", last) + } + // 同一个 Lua 状态(表地址恒定)—— 比计数更直接的证据 + if got := last[strings.Index(last, `"vm"`):]; got != firstVM { + t.Errorf("chunk 间 Lua 状态被换了:首 %s 末 %s", firstVM, got) + } +} + +// ── 能力③:适配器能自带加密原语(签名/摘要)── +// +// 内核在 setupGlobals 注册 sha256_hex / hmac_sha256_hex,适配器与插件共用, +// 所以 AK/SK 签名(SDK-HMAC-SHA256)不需要外部进程。 +const cryptoProbeAdapter = ` +local adapter = {} +adapter.name = "cryptoprobe" +adapter.version = "1" +adapter.endpoint = "/x" +function adapter.probe() + return { + has_sha256 = (sha256_hex ~= nil) and "yes" or "no", + has_hmac = (hmac_sha256_hex ~= nil) and "yes" or "no", + -- 固定向量:sha256("abc") + sha256_abc = sha256_hex and sha256_hex("abc") or "", + hmac_ok = hmac_sha256_hex and hmac_sha256_hex("k", "v") or "", + } +end +return adapter +` + +const cryptoProbePlugin = ` +local plugin = { name = "capcrypto", version = "1.0" } +plugin.exports = { "install", "probe" } +function plugin.install() + return { name = (__llmsproxy_adapters.install("cryptoprobe", [==[` + cryptoProbeAdapter + `]==])) } +end +-- 探针:直接要求内核在该适配器上下文里跑一个函数是不可能的(Transform 只认 +-- 那几个钩子),故用 transform_request 把 probe 结果带出来。 +return plugin +` + +func TestAdapterCapabilityCryptoPrimitives(t *testing.T) { + vm, ps, _ := runtimePlugins(t) + if err := ps.LoadSource("capcrypto", cryptoProbePlugin); err != nil { + t.Fatal(err) + } + if _, err := ps.Call("capcrypto", "install", nil); err != nil { + t.Fatal(err) + } + if !vm.HasFn("cryptoprobe", "probe") { + t.Skip("适配器未导出 probe(本断言不依赖它)") + } + // 直接用 Transform 调 probe 不行(它只认 transform_* 钩子), + // 改为验证内核确实注册了两个原语:拿一个用它们的适配器做端到端。 + h, err := vm.BuildHeaders("cryptoprobe", map[string]interface{}{"api_key": "k"}) + if err != nil { + t.Fatalf("BuildHeaders: %v", err) + } + _ = h + // 真正的断言放在下面:装一个用 sha256_hex 的适配器并跑它的钩子 + if err := vm.LoadAdapterSource("hashproof", ` +local adapter = {} +adapter.name = "hashproof" +adapter.version = "1" +adapter.endpoint = "/x" +function adapter.transform_request(raw) + -- sha256("abc") 的标准值(用内核原语算;写死的是期望值) + return '{"got":"' .. sha256_hex("abc") .. '"}' +end +return adapter +`); err != nil { + t.Fatal(err) + } + out, err := vm.Transform("hashproof", "transform_request", "{}") + if err != nil { + t.Fatalf("transform_request: %v", err) + } + const wantSHA256abc = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + if !strings.Contains(out, wantSHA256abc) { + t.Fatalf("内核 sha256_hex 原语在适配器里不可用或不正确:%s", out) + } +}