magic-agent 2.6.0 → 2.6.3
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/CLIENT-CONTRACT.md +188 -16
- package/client-ui/pc/app.css +42 -0
- package/client-ui/pc/js/90-glue.js +3 -0
- package/client-ui/shared/conv/03-chat-message.js +25 -19
- package/client-ui/shared/conv/09-stream-apply.js +9 -0
- package/client-ui/shared/conv/29-msg-variants.js +136 -0
- package/client-ui/shared/conv/conv.css +253 -158
- package/client-ui/shared/conversation.js +39 -2
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +2695 -379
- package/client-ui-mobile/reqs.css +11 -4
- package/client-ui-mobile/reqs.js +49 -5
- package/client-ui-mobile/scripts.css +46 -0
- package/client-ui-mobile/scripts.js +66 -6
- package/cmd/magic-client/browsectl.go +1 -1
- package/cmd/magic-client/clientctl.go +37 -2
- package/cmd/magic-client/main.go +159 -12
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +144 -5
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/stream.go +30 -0
- package/internal/agent/streamjson.go +11 -1
- package/internal/cli/ask.go +8 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +11 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/root.go +35 -6
- package/internal/cli/session.go +69 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +209 -18
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +233 -12
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/requirements.go +9 -1
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +167 -13
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +140 -1
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +4 -0
- package/internal/client/scripts_decode_test.go +16 -11
- package/internal/client/scripts_decode_windows.go +0 -24
- package/internal/client/scripts_test.go +14 -1
- package/internal/client/service.go +403 -87
- package/internal/client/service_test.go +166 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +83 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +8 -1
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +8 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
// coreexe.go - core 可执行文件的路径解析。
|
|
4
|
+
//
|
|
5
|
+
// 2026-10-26 真机事故(「每条任务都 failed,但错误跟引擎无关」的根因):
|
|
6
|
+
//
|
|
7
|
+
// 客户端的 opts.MagicAgent 缺省是相对名 "magic-agent",exec 时走 PATH,
|
|
8
|
+
// 命中的是 npm 全局 bin 里那份**转发脚本**(`#!/usr/bin/env node`)。
|
|
9
|
+
// 如果客户端本身是从宿主 IDE(WorkBuddy)的终端起的,环境里带着
|
|
10
|
+
//
|
|
11
|
+
// NODE_OPTIONS=--require=".../app.asar.unpacked/.../node-language-shim.cjs"
|
|
12
|
+
// ELECTRON_RUN_AS_NODE=1
|
|
13
|
+
// PATH=<...>/shim/brokered-bin:<...> ← 假 cat/cp/dd,require 时去连宿主 broker
|
|
14
|
+
//
|
|
15
|
+
// 那么 `env node` 解析到的就是宿主那份 node,转发脚本**在 require 阶段就崩**:
|
|
16
|
+
// `connect failed: Connection refused`。core 一个字节都没跑起来 ——
|
|
17
|
+
// 所以现象是「日志里 core 路径看着对、任务全 failed、连 core 进程都抓不到」。
|
|
18
|
+
//
|
|
19
|
+
// 修法:core 与 client 永远同目录发布(npm/build.js 两个产物都落 dist/<plat>/,
|
|
20
|
+
// 仓库 bin/ 同理),优先取同目录那份**真二进制**,绕开 PATH 上的 node 转发层。
|
|
21
|
+
// 这一步与环境清理(internal/agent/env.go)互补:那个保证 core 起的引擎干净,
|
|
22
|
+
// 这个保证 core 自己能起来。
|
|
23
|
+
|
|
24
|
+
import (
|
|
25
|
+
"os"
|
|
26
|
+
"os/exec"
|
|
27
|
+
"path/filepath"
|
|
28
|
+
"runtime"
|
|
29
|
+
"strings"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// coreBinaryName 允许调用方只给命令名。
|
|
33
|
+
const coreBinaryName = "magic-agent"
|
|
34
|
+
|
|
35
|
+
// resolveCoreExe 把 core 的命令名解析成可执行路径,优先「与本进程同目录」。
|
|
36
|
+
//
|
|
37
|
+
// name —— 配置值(空 / "magic-agent" / 绝对路径 / 带目录的相对路径)
|
|
38
|
+
// selfExe —— 本进程可执行文件路径(生产传 os.Executable() 的结果;测试可注入)
|
|
39
|
+
func resolveCoreExe(name, selfExe string) string {
|
|
40
|
+
n := strings.TrimSpace(name)
|
|
41
|
+
if n == "" {
|
|
42
|
+
n = coreBinaryName
|
|
43
|
+
}
|
|
44
|
+
/* 带目录的写法同样可能是 npm 转发脚本(`--magic-agent /…/npm/bin/magic-agent.js`,
|
|
45
|
+
或者 daemon 状态文件里存下来的那条路径)—— 2026-10-26 实测:`magic-client start`
|
|
46
|
+
把它记成绝对路径后,上面「带目录就原样返回」直接把转发脚本放行了。见 npmShimBinary。 */
|
|
47
|
+
if strings.ContainsAny(n, `/\`) {
|
|
48
|
+
if bin, ok := npmShimBinary(n); ok {
|
|
49
|
+
return bin
|
|
50
|
+
}
|
|
51
|
+
return n
|
|
52
|
+
}
|
|
53
|
+
if bin, ok := siblingCore(n, selfExe); ok {
|
|
54
|
+
return bin
|
|
55
|
+
}
|
|
56
|
+
if p, err := exec.LookPath(n); err == nil {
|
|
57
|
+
/* PATH 上命中的是 npm 全局 bin 里的转发脚本(`#!/usr/bin/env node`)。
|
|
58
|
+
真身在同包 npm/dist/<plat>/ 下 —— 解析到它,别每次调用都白起一个 Node。
|
|
59
|
+
解析不出来才退回脚本本身(环境干净时它本来也能工作)。 */
|
|
60
|
+
if bin, ok := npmShimBinary(p); ok {
|
|
61
|
+
return bin
|
|
62
|
+
}
|
|
63
|
+
return p
|
|
64
|
+
}
|
|
65
|
+
return n
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
npmShimBinary 把 npm 包里的 node 转发脚本(npm/bin/magic-agent.js)换成它要 spawn 的
|
|
70
|
+
真二进制(npm/dist/<plat>/magic-agent)。解析不出来就返回 false,调用方保留原路径。
|
|
71
|
+
|
|
72
|
+
为什么必须做(2026-10-26 真机实测,用户报「会话列表现在显示很慢」):
|
|
73
|
+
|
|
74
|
+
`magic-client start` 把 core 路径记成绝对路径时落的是 npm/bin/magic-agent.js ——
|
|
75
|
+
那是 `#!/usr/bin/env node` 的转发层。于是**每一次** `--sessions` / `--contract` /
|
|
76
|
+
`--engines` 都要先起一个 Node 解释器(约 40ms,Host 环境更贵),再 spawnSync 到 Go。
|
|
77
|
+
同一个 core 直连只要 18ms,经客户端却要 200~900ms(机器负载 9~11 时放大到接近 1s)。
|
|
78
|
+
转发层本身没有一行业务逻辑,纯属白烧 —— 这里一次性解析掉。
|
|
79
|
+
|
|
80
|
+
布局依据 npm/lib/platform.js:dist/<goos>-<nodearch>/<exe>,node 的 x64 对应 goarch amd64。
|
|
81
|
+
|
|
82
|
+
…/node_modules/magic-agent/npm/bin/magic-agent.js
|
|
83
|
+
└── dist/darwin-arm64/magic-agent
|
|
84
|
+
*/
|
|
85
|
+
func npmShimBinary(path string) (string, bool) {
|
|
86
|
+
/* 先穿透符号链接(2026-10-26 补):/opt/homebrew/bin/magic-agent 是指向
|
|
87
|
+
../lib/node_modules/magic-agent/npm/bin/magic-agent.js 的 symlink,**没有 .js 后缀**。
|
|
88
|
+
只看扩展名的老实现把它当普通二进制原样返回 → 仍然每次白起一个 Node。
|
|
89
|
+
实测差 3 倍(直跑 dist 10ms / 经 symlink 30ms),所以这一步不能省。
|
|
90
|
+
|
|
91
|
+
注意用 derefLink 而不是 filepath.EvalSymlinks:后者会把路径里所有组件都解掉,
|
|
92
|
+
macOS 上 `/var/folders/...` 会变成 `/private/var/folders/...`。路径字符串一变,
|
|
93
|
+
日志、状态文件比对、测试期望全跟着漂 —— 只解最后那段链接就够了。 */
|
|
94
|
+
if direct := derefLink(path, 8); direct != path {
|
|
95
|
+
if bin, ok := npmShimBinary(direct); ok {
|
|
96
|
+
return bin, true
|
|
97
|
+
}
|
|
98
|
+
// 解出来的不是转发脚本(可能指向真二进制)→ 用解出来的路径继续判。
|
|
99
|
+
path = direct
|
|
100
|
+
}
|
|
101
|
+
ext := strings.ToLower(filepath.Ext(path))
|
|
102
|
+
if ext != ".js" && ext != ".cjs" {
|
|
103
|
+
return "", false
|
|
104
|
+
}
|
|
105
|
+
base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
|
106
|
+
// 只认本仓的两个转发脚本,别的 .js 一律不碰(调用方可能真的想跑一个脚本)。
|
|
107
|
+
if base != coreBinaryName && base != "magic-client" {
|
|
108
|
+
return "", false
|
|
109
|
+
}
|
|
110
|
+
binDir := filepath.Dir(path)
|
|
111
|
+
if filepath.Base(binDir) != "bin" {
|
|
112
|
+
return "", false
|
|
113
|
+
}
|
|
114
|
+
pkgRoot := filepath.Dir(binDir) // npm/
|
|
115
|
+
distDir := filepath.Join(pkgRoot, "dist")
|
|
116
|
+
// 包根判定:npm/ 下同时有 bin/ 与 dist/。不满足就不是本仓的包结构,别乱猜。
|
|
117
|
+
if st, err := os.Stat(distDir); err != nil || !st.IsDir() {
|
|
118
|
+
return "", false
|
|
119
|
+
}
|
|
120
|
+
cand := filepath.Join(distDir, npmPlatformDir(), exeNameFor(base))
|
|
121
|
+
st, err := os.Stat(cand)
|
|
122
|
+
if err != nil || st.IsDir() {
|
|
123
|
+
return "", false
|
|
124
|
+
}
|
|
125
|
+
if runtime.GOOS != "windows" && st.Mode()&0o111 == 0 {
|
|
126
|
+
return "", false
|
|
127
|
+
}
|
|
128
|
+
return cand, true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// derefLink 只解「路径最后一段是不是符号链接」,逐层跟到底(相对链接按所在目录解)。
|
|
132
|
+
//
|
|
133
|
+
// 不用 filepath.EvalSymlinks 的原因:它会解掉**路径里每一级**组件。macOS 上
|
|
134
|
+
// /var/folders/... 实际是 /private/var/folders/... 的链接,于是同一个文件会返回
|
|
135
|
+
// 两个不同的字符串 —— 调用方拿去比对状态文件、写日志、做相等断言就会全乱。
|
|
136
|
+
//
|
|
137
|
+
// 解不动(循环 / 层级过深 / 读不到)就原样返回,交给调用方按普通路径处理。
|
|
138
|
+
func derefLink(path string, maxDepth int) string {
|
|
139
|
+
cur := path
|
|
140
|
+
for i := 0; i < maxDepth; i++ {
|
|
141
|
+
fi, err := os.Lstat(cur)
|
|
142
|
+
if err != nil || fi.Mode()&os.ModeSymlink == 0 {
|
|
143
|
+
return cur
|
|
144
|
+
}
|
|
145
|
+
target, err := os.Readlink(cur)
|
|
146
|
+
if err != nil {
|
|
147
|
+
return cur
|
|
148
|
+
}
|
|
149
|
+
if !filepath.IsAbs(target) {
|
|
150
|
+
target = filepath.Join(filepath.Dir(cur), target)
|
|
151
|
+
}
|
|
152
|
+
cur = filepath.Clean(target)
|
|
153
|
+
}
|
|
154
|
+
return cur
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// npmPlatformDir npm 产物目录名(与 npm/lib/platform.js 的 currentTarget().dir 逐字一致)。
|
|
158
|
+
func npmPlatformDir() string {
|
|
159
|
+
goos := runtime.GOOS
|
|
160
|
+
if goos == "windows" {
|
|
161
|
+
goos = "win32"
|
|
162
|
+
}
|
|
163
|
+
arch := runtime.GOARCH
|
|
164
|
+
if arch == "amd64" {
|
|
165
|
+
arch = "x64"
|
|
166
|
+
}
|
|
167
|
+
return goos + "-" + arch
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// exeNameFor 平台可执行文件名(Windows 带 .exe)。
|
|
171
|
+
func exeNameFor(base string) string {
|
|
172
|
+
if runtime.GOOS == "windows" {
|
|
173
|
+
return base + ".exe"
|
|
174
|
+
}
|
|
175
|
+
return base
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// siblingCore 在 selfExe 同目录找同名可执行文件。
|
|
179
|
+
func siblingCore(name, selfExe string) (string, bool) {
|
|
180
|
+
if strings.TrimSpace(selfExe) == "" {
|
|
181
|
+
return "", false
|
|
182
|
+
}
|
|
183
|
+
dir := filepath.Dir(selfExe)
|
|
184
|
+
cand := filepath.Join(dir, name)
|
|
185
|
+
if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(cand), ".exe") {
|
|
186
|
+
cand += ".exe"
|
|
187
|
+
}
|
|
188
|
+
st, err := os.Stat(cand)
|
|
189
|
+
if err != nil || st.IsDir() {
|
|
190
|
+
return "", false
|
|
191
|
+
}
|
|
192
|
+
if runtime.GOOS != "windows" && st.Mode()&0o111 == 0 {
|
|
193
|
+
return "", false
|
|
194
|
+
}
|
|
195
|
+
return cand, true
|
|
196
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// 同目录有真二进制时必须优先用它 —— 这就是 2026-10-26 真机事故的修法:
|
|
10
|
+
// 别让 exec 走 PATH 撞上 npm 的 node 转发脚本。
|
|
11
|
+
func TestResolveCoreExePrefersSibling(t *testing.T) {
|
|
12
|
+
dir := t.TempDir()
|
|
13
|
+
core := filepath.Join(dir, "magic-agent")
|
|
14
|
+
if err := os.WriteFile(core, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
15
|
+
t.Fatal(err)
|
|
16
|
+
}
|
|
17
|
+
self := filepath.Join(dir, "magic-client")
|
|
18
|
+
got := resolveCoreExe("magic-agent", self)
|
|
19
|
+
if got != core {
|
|
20
|
+
t.Fatalf("应取同目录 core %s,实际 %s", core, got)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 显式给了路径就照用,不猜。
|
|
25
|
+
func TestResolveCoreExeKeepsExplicitPath(t *testing.T) {
|
|
26
|
+
if got := resolveCoreExe("/opt/x/magic-agent", "/tmp/magic-client"); got != "/opt/x/magic-agent" {
|
|
27
|
+
t.Fatalf("显式路径被改写:%s", got)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 同目录没有就退回命令名/PATH 解析,不制造不存在的路径。
|
|
32
|
+
func TestResolveCoreExeFallsBack(t *testing.T) {
|
|
33
|
+
dir := t.TempDir()
|
|
34
|
+
got := resolveCoreExe("", filepath.Join(dir, "magic-client"))
|
|
35
|
+
if filepath.Dir(got) == dir {
|
|
36
|
+
t.Fatalf("同目录没有 core 却编出一个:%s", got)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// npm 包布局:npmShimBinary 把 bin/magic-agent.js 换成 dist/<plat>/magic-agent。
|
|
41
|
+
//
|
|
42
|
+
// 这就是「会话列表慢」的根因修法:转发脚本零业务逻辑,却让每次列表请求多起一个 Node
|
|
43
|
+
// 解释器(实测 40ms → 800ms 量级,机器负载高时接近 1 秒)。
|
|
44
|
+
func TestNpmShimBinaryResolvesDistBinary(t *testing.T) {
|
|
45
|
+
pkg := filepath.Join(t.TempDir(), "node_modules", "magic-agent", "npm")
|
|
46
|
+
binScript := filepath.Join(pkg, "bin", "magic-agent.js")
|
|
47
|
+
real := filepath.Join(pkg, "dist", npmPlatformDir(), exeNameFor("magic-agent"))
|
|
48
|
+
for _, p := range []string{binScript, real} {
|
|
49
|
+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if err := os.WriteFile(binScript, []byte("#!/usr/bin/env node\n"), 0o755); err != nil {
|
|
54
|
+
t.Fatal(err)
|
|
55
|
+
}
|
|
56
|
+
if err := os.WriteFile(real, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
57
|
+
t.Fatal(err)
|
|
58
|
+
}
|
|
59
|
+
got, ok := npmShimBinary(binScript)
|
|
60
|
+
if !ok || got != real {
|
|
61
|
+
t.Fatalf("应解析到真二进制 %s,实际 ok=%v got=%s", real, ok, got)
|
|
62
|
+
}
|
|
63
|
+
// 绝对路径同样要走这条解析(daemon 状态文件记下的就是绝对路径)。
|
|
64
|
+
if viaResolve := resolveCoreExe(binScript, ""); viaResolve != real {
|
|
65
|
+
t.Fatalf("resolveCoreExe 没解析转发脚本:%s", viaResolve)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// dist 下没有该平台产物时不许编路径 —— 宁可原样返回脚本,让调用方看到真实错误。
|
|
70
|
+
func TestNpmShimBinaryNoDistKeepsScript(t *testing.T) {
|
|
71
|
+
pkg := filepath.Join(t.TempDir(), "node_modules", "magic-agent", "npm")
|
|
72
|
+
binScript := filepath.Join(pkg, "bin", "magic-agent.js")
|
|
73
|
+
if err := os.MkdirAll(filepath.Dir(binScript), 0o755); err != nil {
|
|
74
|
+
t.Fatal(err)
|
|
75
|
+
}
|
|
76
|
+
if err := os.WriteFile(binScript, []byte("#!/usr/bin/env node\n"), 0o755); err != nil {
|
|
77
|
+
t.Fatal(err)
|
|
78
|
+
}
|
|
79
|
+
if _, ok := npmShimBinary(binScript); ok {
|
|
80
|
+
t.Fatal("没有 dist 产物却解析成功了")
|
|
81
|
+
}
|
|
82
|
+
if got := resolveCoreExe(binScript, ""); got != binScript {
|
|
83
|
+
t.Fatalf("应原样保留脚本路径,实际 %s", got)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 非本仓的脚本(名字对不上)不碰 —— 调用方可能真的想跑一个 .js。
|
|
88
|
+
func TestNpmShimBinaryIgnoresForeignScript(t *testing.T) {
|
|
89
|
+
pkg := filepath.Join(t.TempDir(), "npm")
|
|
90
|
+
other := filepath.Join(pkg, "bin", "something-else.js")
|
|
91
|
+
if err := os.MkdirAll(filepath.Dir(other), 0o755); err != nil {
|
|
92
|
+
t.Fatal(err)
|
|
93
|
+
}
|
|
94
|
+
if err := os.WriteFile(other, []byte("#!/usr/bin/env node\n"), 0o755); err != nil {
|
|
95
|
+
t.Fatal(err)
|
|
96
|
+
}
|
|
97
|
+
if _, ok := npmShimBinary(other); ok {
|
|
98
|
+
t.Fatal("不相干的 .js 被当成转发层了")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// symlink 形式的全局 bin 必须穿透(2026-10-26 真机发现的漏洞):
|
|
103
|
+
// /opt/homebrew/bin/magic-agent -> ../lib/node_modules/magic-agent/npm/bin/magic-agent.js
|
|
104
|
+
// 它**没有 .js 后缀**,只看扩展名的实现会原样放行,于是每次请求照样白起一个 Node。
|
|
105
|
+
// 实测直跑 dist 10ms / 经 symlink 30ms,3 倍差。
|
|
106
|
+
func TestNpmShimBinaryResolvesThroughSymlink(t *testing.T) {
|
|
107
|
+
pkg := filepath.Join(t.TempDir(), "node_modules", "magic-agent", "npm")
|
|
108
|
+
binScript := filepath.Join(pkg, "bin", "magic-agent.js")
|
|
109
|
+
real := filepath.Join(pkg, "dist", npmPlatformDir(), exeNameFor("magic-agent"))
|
|
110
|
+
for _, p := range []string{binScript, real} {
|
|
111
|
+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
112
|
+
t.Fatal(err)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if err := os.WriteFile(binScript, []byte("#!/usr/bin/env node\n"), 0o755); err != nil {
|
|
116
|
+
t.Fatal(err)
|
|
117
|
+
}
|
|
118
|
+
if err := os.WriteFile(real, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
119
|
+
t.Fatal(err)
|
|
120
|
+
}
|
|
121
|
+
// 模拟 Homebrew / npm prefix 的 bin:同一层级放一个无后缀的 symlink。
|
|
122
|
+
linkDir := filepath.Join(t.TempDir(), "homebrew", "bin")
|
|
123
|
+
if err := os.MkdirAll(linkDir, 0o755); err != nil {
|
|
124
|
+
t.Fatal(err)
|
|
125
|
+
}
|
|
126
|
+
link := filepath.Join(linkDir, "magic-agent")
|
|
127
|
+
if err := os.Symlink(binScript, link); err != nil {
|
|
128
|
+
t.Skipf("本平台不支持 symlink:%v", err)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
got, ok := npmShimBinary(link)
|
|
132
|
+
if !ok || got != real {
|
|
133
|
+
t.Fatalf("symlink 没穿透到真二进制:ok=%v got=%s(期望 %s)", ok, got, real)
|
|
134
|
+
}
|
|
135
|
+
if viaResolve := resolveCoreExe(link, ""); viaResolve != real {
|
|
136
|
+
t.Fatalf("resolveCoreExe 没穿透 symlink:%s", viaResolve)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 反过来:symlink 指向**真二进制**时必须原样可用(别把它当成转发脚本拦下来)。
|
|
141
|
+
func TestNpmShimBinarySymlinkToRealBinaryPassesThrough(t *testing.T) {
|
|
142
|
+
real := filepath.Join(t.TempDir(), "dist", "magic-agent")
|
|
143
|
+
if err := os.MkdirAll(filepath.Dir(real), 0o755); err != nil {
|
|
144
|
+
t.Fatal(err)
|
|
145
|
+
}
|
|
146
|
+
if err := os.WriteFile(real, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
147
|
+
t.Fatal(err)
|
|
148
|
+
}
|
|
149
|
+
link := filepath.Join(t.TempDir(), "magic-agent")
|
|
150
|
+
if err := os.Symlink(real, link); err != nil {
|
|
151
|
+
t.Skipf("本平台不支持 symlink:%v", err)
|
|
152
|
+
}
|
|
153
|
+
// 不 panic、不误判;返回值就是链接本身或它解出来的路径,两者都指向真二进制。
|
|
154
|
+
got, ok := npmShimBinary(link)
|
|
155
|
+
if ok && got != real && got != link {
|
|
156
|
+
t.Fatalf("symlink 指向真二进制时解析出了怪路径:%s", got)
|
|
157
|
+
}
|
|
158
|
+
if viaResolve := resolveCoreExe(link, ""); viaResolve == "" {
|
|
159
|
+
t.Fatal("resolveCoreExe 返回空")
|
|
160
|
+
}
|
|
161
|
+
}
|