@pawlogic/dl 0.4.0-staging.f5043c2 → 0.4.0-staging.f563fa2
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/dl-npm-update.sh +200 -0
- package/dl.mjs +1 -1
- package/ilands.mjs +1 -1
- package/package.json +3 -2
package/dl-npm-update.sh
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# dl 沙箱内自更新 (npm 版本) —— 替代 dl-selfupdate.sh 的 GCS 路径。
|
|
3
|
+
#
|
|
4
|
+
# wrapper 在 exec dl 之前调一次本脚本。任何步骤失败一律 swallow,永远 exit 0,
|
|
5
|
+
# 绝不阻塞业务 dl 调用。当次仍用旧 bundle,新 bundle 装完后**下次** dl 调用才生效
|
|
6
|
+
# (agent 失败重试天然吃到新版)。
|
|
7
|
+
#
|
|
8
|
+
# 触发条件:DL_NPM_DIST_TAG 已 bake 到 sandbox env(prod=latest,staging=staging;PR
|
|
9
|
+
# preview 留空 → 本脚本直接 exit,沙箱内 dl 锁在 template 烤进去那版,便于 PR 间
|
|
10
|
+
# 复现 / 互不污染)。
|
|
11
|
+
#
|
|
12
|
+
# 鉴权: 不需要。包发到 npmjs.com 的 @pawlogic scope (public),npm 默认 registry
|
|
13
|
+
# 就是 npmjs.com,匿名 download 直接通,沙箱没有 ~/.npmrc。
|
|
14
|
+
#
|
|
15
|
+
# TTL 300s 缓存:同一 sandbox 5 分钟内多次 dl 调用只查一次。LLM 调用链常一分钟 10+
|
|
16
|
+
# 次,不加缓存白白打满 npm registry。
|
|
17
|
+
#
|
|
18
|
+
# 观测: 每条分支结束 fire-and-forget POST 一条事件到 callback-server
|
|
19
|
+
# $CALLBACK_SERVER_URL/sandbox-rpc/dl/selfupdate-event,backend 仅转发到 Honeycomb,
|
|
20
|
+
# schema 校验由本脚本端负责。TTL 命中频繁(每个沙箱 5min 内 N 次), 按 1/10 采样上报;
|
|
21
|
+
# 其余分支(install / no_tag / no_npm / lock_skip)全量上报。
|
|
22
|
+
# 鉴权: 复用 $DL_PROXY_TOKEN (沙箱 env, hono-pi-e2b 注入), Authorization Bearer。
|
|
23
|
+
# 上报失败一律 swallow,绝不让观测链阻塞自更新本身。
|
|
24
|
+
|
|
25
|
+
set -u
|
|
26
|
+
|
|
27
|
+
STAMP=/tmp/dl-npm-checkedat
|
|
28
|
+
LOCK=/tmp/dl-npm-update.lock
|
|
29
|
+
TTL=300
|
|
30
|
+
PACKAGE="@pawlogic/dl"
|
|
31
|
+
|
|
32
|
+
# ── 观测上报: fire-and-forget JSON POST,失败 swallow ────────────────────────
|
|
33
|
+
# args: outcome duration_ms [error_tail_file]
|
|
34
|
+
# error_tail_file 仅 npm_fail 分支传入,读末尾 ~200 字节带上去 —— 之前 npm stderr
|
|
35
|
+
# 全 swallow,根本看不到为什么失败,fleet 升级率 0.24% 都查不到根因。
|
|
36
|
+
report_event() {
|
|
37
|
+
_outcome="$1"
|
|
38
|
+
_dur="${2:-null}"
|
|
39
|
+
_errfile="${3:-}"
|
|
40
|
+
[ -z "${CALLBACK_SERVER_URL:-}" ] && return 0
|
|
41
|
+
[ -z "${DL_PROXY_TOKEN:-}" ] && return 0
|
|
42
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
43
|
+
_tag="${DL_NPM_DIST_TAG:-unknown}"
|
|
44
|
+
case "$_tag" in
|
|
45
|
+
latest|staging) ;;
|
|
46
|
+
*) _tag="unknown" ;;
|
|
47
|
+
esac
|
|
48
|
+
_ver=""
|
|
49
|
+
_verfile="/home/user/.local/lib/node_modules/@pawlogic/dl/package.json"
|
|
50
|
+
if [ -f "$_verfile" ] && command -v node >/dev/null 2>&1; then
|
|
51
|
+
_ver=$(node -e 'try{console.log(require("'"$_verfile"'").version||"")}catch(e){}' 2>/dev/null || echo "")
|
|
52
|
+
fi
|
|
53
|
+
# 上下文维度: 全部从 hono-pi-e2b 注入的 PI_* env 里读 (e2b-operations.ts:582-627),
|
|
54
|
+
# 加上 E2B sandbox hostname (= sandbox ID)。校验只允许 [A-Za-z0-9_-], 防奇怪字符
|
|
55
|
+
# 破坏 JSON 后续没办法 escape; 不合法 / 空一律 null。
|
|
56
|
+
#
|
|
57
|
+
# 维度用途:
|
|
58
|
+
# - project_id per-project 去重 (COUNT_DISTINCT) 是 fleet 升级率口径
|
|
59
|
+
# - mode dramaland / ilands / ... 按产品线切升级率
|
|
60
|
+
# - user_tier starter / pro / ... 看付费用户升级率差异
|
|
61
|
+
# - user_id 单用户排障 (高基数)
|
|
62
|
+
# - prompt_id LLM turn 级聚合 (高基数)
|
|
63
|
+
# - tool_call_id 单次工具调用关联到自更新 outcome (高基数)
|
|
64
|
+
# - sandbox_id 单沙箱排障; E2B 内 hostname 就是 sandbox ID
|
|
65
|
+
_json_or_null() {
|
|
66
|
+
case "${1:-}" in
|
|
67
|
+
"") echo "null" ;;
|
|
68
|
+
*[!A-Za-z0-9_-]*) echo "null" ;;
|
|
69
|
+
*) printf '"%s"' "$1" ;;
|
|
70
|
+
esac
|
|
71
|
+
}
|
|
72
|
+
_proj_json=$(_json_or_null "${PI_PROJECT_ID:-}")
|
|
73
|
+
_mode_json=$(_json_or_null "${PI_MODE:-}")
|
|
74
|
+
_tier_json=$(_json_or_null "${PI_USER_TIER:-}")
|
|
75
|
+
_user_json=$(_json_or_null "${PI_USER_ID:-}")
|
|
76
|
+
_prompt_json=$(_json_or_null "${PI_PROMPT_ID:-}")
|
|
77
|
+
_toolcall_json=$(_json_or_null "${PI_TOOL_CALL_ID:-}")
|
|
78
|
+
_sandbox_json=$(_json_or_null "${HOSTNAME:-$(hostname 2>/dev/null || echo)}")
|
|
79
|
+
# error_tail: 末尾 200 字节, JSON 不安全字符 (\ " 控制符 / 换行 / tab) 一律压成
|
|
80
|
+
# 单空格; 长度上限挡住 LB 滥用,字符替换让 JSON 不需要复杂 escape。
|
|
81
|
+
_err_json="null"
|
|
82
|
+
if [ -n "$_errfile" ] && [ -f "$_errfile" ]; then
|
|
83
|
+
_err_raw=$(tail -c 200 "$_errfile" 2>/dev/null | tr '\n\r\t\\"' ' ' | tr -d '\000-\037')
|
|
84
|
+
if [ -n "$_err_raw" ]; then
|
|
85
|
+
_err_json="\"$_err_raw\""
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
# 后台 curl, -m 2 上限 2s; 不解析响应; stderr/stdout 全丢。
|
|
89
|
+
(
|
|
90
|
+
curl -fsS -m 2 -X POST "${CALLBACK_SERVER_URL%/}/sandbox-rpc/dl/selfupdate-event" \
|
|
91
|
+
-H 'content-type: application/json' \
|
|
92
|
+
-H "authorization: Bearer ${DL_PROXY_TOKEN}" \
|
|
93
|
+
-d "{\"trigger\":\"wrapper\",\"dist_tag\":\"$_tag\",\"outcome\":\"$_outcome\",\"duration_ms\":$_dur,\"installed_version\":\"$_ver\",\"build_short_sha\":null,\"error_tail\":$_err_json,\"project_id\":$_proj_json,\"mode\":$_mode_json,\"user_tier\":$_tier_json,\"user_id\":$_user_json,\"prompt_id\":$_prompt_json,\"tool_call_id\":$_toolcall_json,\"sandbox_id\":$_sandbox_json}" \
|
|
94
|
+
>/dev/null 2>&1 || true
|
|
95
|
+
) &
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 采样 TTL hit 上报: 10 选 1。awk 比 $RANDOM 更便携 (busybox sh 默认没 $RANDOM)。
|
|
99
|
+
should_sample_ttl() {
|
|
100
|
+
awk 'BEGIN{srand(); exit !(int(rand()*10)==0)}' 2>/dev/null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# 环境不全 → no-op。staging / prod 才烤 DL_NPM_DIST_TAG,PR preview / 本地裸跑空。
|
|
104
|
+
if [ -z "${DL_NPM_DIST_TAG:-}" ]; then
|
|
105
|
+
report_event no_tag null
|
|
106
|
+
exit 0
|
|
107
|
+
fi
|
|
108
|
+
if ! command -v npm >/dev/null 2>&1; then
|
|
109
|
+
report_event no_npm null
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# TTL 拦截。stamp 损坏当 0 处理。
|
|
114
|
+
now=$(date +%s 2>/dev/null) || exit 0
|
|
115
|
+
if [ -f "$STAMP" ]; then
|
|
116
|
+
last=$(cat "$STAMP" 2>/dev/null || echo 0)
|
|
117
|
+
case "$last" in
|
|
118
|
+
''|*[!0-9]*) last=0 ;;
|
|
119
|
+
esac
|
|
120
|
+
delta=$((now - last))
|
|
121
|
+
if [ "$delta" -ge 0 ] && [ "$delta" -lt "$TTL" ]; then
|
|
122
|
+
if should_sample_ttl; then
|
|
123
|
+
report_event ttl_hit null
|
|
124
|
+
fi
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# 单飞锁:并发 dl 调用同时进来时只让一个跑 npm install,其他直接退出。
|
|
130
|
+
# flock 不一定有(busybox sh) → mkdir 兜底原子锁。
|
|
131
|
+
run_install() {
|
|
132
|
+
_t0=$(date +%s 2>/dev/null || echo 0)
|
|
133
|
+
# stderr 写到独立文件 npm_fail 分支带末尾给观测端;stdout 仍丢掉。
|
|
134
|
+
_errlog=$(mktemp 2>/dev/null || echo "/tmp/dl-npm-err.$$")
|
|
135
|
+
# 必须带 --prefix=/home/user/.local 跟 HOME=/home/user,跟 sandbox-template
|
|
136
|
+
# 首次安装那一段对齐 (packages/sandbox-template/scripts/publish-e2b-template.ts
|
|
137
|
+
# 里 wrapper 写法)。沙箱进程 owner 是 user,无 /usr/lib/node_modules 写权限;
|
|
138
|
+
# 不带 prefix → npm 走默认全局路径 → EACCES 1 秒内挂掉, stderr 又被吞 → fleet
|
|
139
|
+
# 自更新 100% 失败但完全黑盒。这是 PR #729 漏掉 prefix 留下的 bug。
|
|
140
|
+
#
|
|
141
|
+
# --force 必须带: 本脚本 install 后会 `ln -sf /usr/local/bin/dl
|
|
142
|
+
# /home/user/.local/bin/dl` 把 npm shim 顶成软链 (修 PATH 顺序, 见下方
|
|
143
|
+
# ln -sf 那两行)。下次 install 时 npm 看 /home/user/.local/bin/dl 已经存在
|
|
144
|
+
# 且不是它自己管理的, 默认 EEXIST 拒绝覆盖 (error 信息里直接写 "run npm with
|
|
145
|
+
# --force to overwrite"); --force 允许 npm 覆盖。Honeycomb 上 staging
|
|
146
|
+
# dl-selfupdate dataset 看到的所有 npm_fail outcome (#741 接通后) 都是这条,
|
|
147
|
+
# 加 --force 就清零。
|
|
148
|
+
if HOME=/home/user npm install -g --prefix=/home/user/.local \
|
|
149
|
+
--no-audit --no-fund --no-update-notifier --force \
|
|
150
|
+
"$PACKAGE@$DL_NPM_DIST_TAG" >/dev/null 2>"$_errlog"; then
|
|
151
|
+
_outcome=ok
|
|
152
|
+
else
|
|
153
|
+
_outcome=npm_fail
|
|
154
|
+
fi
|
|
155
|
+
_t1=$(date +%s 2>/dev/null || echo 0)
|
|
156
|
+
_dur_ms=$(( (_t1 - _t0) * 1000 ))
|
|
157
|
+
# npm install -g 会重生成 /home/user/.local/bin/{dl,ilands} shim 顶掉 wrapper
|
|
158
|
+
# 软链, 立刻 re-link 回 /usr/local/bin/{dl,ilands} wrapper —— 不然 PATH 命中
|
|
159
|
+
# npm shim, 下次调用绕过 wrapper, dl 的 DL_NPM_DIST_TAG export 跑不到, 自更新
|
|
160
|
+
# 链路死掉; ilands 也跟着丢 wrapper(虽然 ilands 自己不需要 env, 但 update.sh
|
|
161
|
+
# 入口被绕过, 跟 dl 同时挂)。
|
|
162
|
+
ln -sf /usr/local/bin/dl /home/user/.local/bin/dl >/dev/null 2>&1 || true
|
|
163
|
+
ln -sf /usr/local/bin/ilands /home/user/.local/bin/ilands >/dev/null 2>&1 || true
|
|
164
|
+
# 自我热更: @pawlogic/dl npm 包里 ship 了本脚本的最新版 (packages/dl/sandbox-assets/
|
|
165
|
+
# dl-npm-update.sh), npm install 完成后覆盖一份到 wrapper 调用的固定路径。
|
|
166
|
+
# 1 代 lag: 本次仍跑老逻辑, 新逻辑下次 dl 调用生效 —— 比"等 e2b template republish
|
|
167
|
+
# + 老 snapshot 自然下线"快好几个量级, snapshot resume 的沙箱也能跟上。
|
|
168
|
+
# 当前进程文件是 shell 解释器一次性读入内存的, 覆盖自身不会撕裂正在执行的循环。
|
|
169
|
+
if [ -f /home/user/.local/lib/node_modules/@pawlogic/dl/dl-npm-update.sh ]; then
|
|
170
|
+
cp /home/user/.local/lib/node_modules/@pawlogic/dl/dl-npm-update.sh \
|
|
171
|
+
/home/user/.local/lib/dl-npm-update.sh 2>/dev/null || true
|
|
172
|
+
chmod +x /home/user/.local/lib/dl-npm-update.sh 2>/dev/null || true
|
|
173
|
+
fi
|
|
174
|
+
echo "$now" > "$STAMP"
|
|
175
|
+
if [ "$_outcome" = "npm_fail" ]; then
|
|
176
|
+
report_event "$_outcome" "$_dur_ms" "$_errlog"
|
|
177
|
+
else
|
|
178
|
+
report_event "$_outcome" "$_dur_ms"
|
|
179
|
+
fi
|
|
180
|
+
rm -f "$_errlog" 2>/dev/null || true
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if command -v flock >/dev/null 2>&1; then
|
|
184
|
+
(
|
|
185
|
+
if ! flock -n 9; then
|
|
186
|
+
report_event lock_skip null
|
|
187
|
+
exit 0
|
|
188
|
+
fi
|
|
189
|
+
run_install
|
|
190
|
+
) 9>"$LOCK"
|
|
191
|
+
else
|
|
192
|
+
if mkdir "$LOCK" 2>/dev/null; then
|
|
193
|
+
trap 'rmdir "$LOCK" 2>/dev/null || true' EXIT INT TERM
|
|
194
|
+
run_install
|
|
195
|
+
else
|
|
196
|
+
report_event lock_skip null
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
exit 0
|