@pawlogic/dl 0.4.0-staging.f7f0c02 → 0.4.0-staging.f8126e6

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