tao.chen
ec5292f9f7
ci: rollback
CI / Release / CI (push) Successful in 3m30s
CI / Release / Release (push) Has been skipped
2026-07-15 18:57:05 +08:00
tao.chen
fefd816a21
ci: update Checkout uses
CI / Release / CI (push) Failing after 30s
CI / Release / Release (push) Has been skipped
2026-07-15 18:51:30 +08:00
tao.chen and Claude
2571ce69c0
ci: 换 gitea/action-checkout 替代手写 git clone
...
CI / Release / CI (push) Failing after 8s
CI / Release / Release (push) Has been skipped
上一轮改用 git clone 手写 checkout 是因为 actions/checkout@v4 是
Node 实现, alpine golang-1.25 镜像没装 Node. 但后来发现镜像连
bash 也没有 (alpine 极简镜像, 只剩 sh), run: | 块也会崩.
gitea/action-checkout 是 Gitea 官方 Go 实现的 checkout action,
不依赖 Node 也不依赖 bash, 跟 Gitea runner 原生集成. 替换两个
job 的 checkout step.
注意: gitea/action-checkout 只解决 checkout. 后续 run: | 块
(Set GOPROXY / Build / Test / Upload release asset / Notify) 仍
然用 bash, alpine 没 bash 会继续崩. 这是已知遗留, 跑起来再看
是否真崩. 完整修法是给所有 run: | 块加 shell: sh, 但本 commit
先按用户要求只换 checkout, 观察跑结果再决定.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 18:48:25 +08:00
tao.chen and Claude
160c9f32c8
ci: 用 git clone 替换 actions/checkout (alpine 无 Node)
...
CI / Release / CI (push) Failing after 44s
CI / Release / Release (push) Has been skipped
alpine golang-1.25 镜像没装 Node, actions/checkout@v4 启动失败:
Run Checkout
OCI runtime exec failed: exec failed: unable to start container
process: exec: "node": executable file not found in \$PATH: unknown
actions/checkout 是 Node.js 应用, Gitea alpine runner 镜像不
带 Node. 改用 git clone 手写 checkout step:
git clone --depth 1 "\${{ github.server_url }}/\${{ github.repository }}.git" .
shallow clone (\$depth 1) 减少传输量, \${{ github.server_url }} 自动
填入 Gitea 实例 URL, \${{ github.repository }} 是 owner/repo 路径.
这个 runner 应该跟 Gitea 在同一网络, 仓库公开, 不需要 auth
token. 如果将来换 runner 跨网络或者仓库私有, 改成:
git clone --depth 1 \\
-c http.extraHeader="Authorization: token \$GITEA_TOKEN" \\
"\${{ github.server_url }}/\${{ github.repository }}.git" .
GITEA_TOKEN 走 \${{ secrets.GITHUB_TOKEN }} (Gitea 自动注入).
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 18:45:31 +08:00
tao.chen and Claude
e5f7d661b4
ci: 切到 Gitea golang-1.25 runner, alpine 镜像
...
CI / Release / CI (push) Failing after 49s
CI / Release / Release (push) Has been skipped
Gitea runner 现在用 golang-1.25 标签 (alpine + Go 1.25 预装), 不
再需要 wget + tar 装 Go. 删 "Install Go (Mirror in China)" step,
保留 GOPROXY 设置 (alpine 默认 GOPROXY=proxy.golang.org, 国内
runner 慢).
runner: golang-1.25 (两 job 都改)
step 1: actions/checkout@v4 (alpine 已有, 不需要 apt 装 git)
step 2: "Set GOPROXY" 写 \$GITHUB_ENV, 后续 step 继承
(https://goproxy.cn/,https://goproxy.io,direct )
grep -P 兼容性 fix: alpine busybox grep 不支持 -P (PCRE). 之前
release job 用 grep -oP '"id":\s*\K[0-9]+' 拆 release id, 在
alpine 上会 "grep: unrecognized option". 改两次 grep -o:
grep -o '"id":[0-9]*' | head -n 1 | grep -o '[0-9]*'
第 1 个 grep 拉出 "id":<num>, 第 2 个去掉引号和 key 名.
不依赖 -P, POSIX grep 也跑.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 18:40:43 +08:00
tao.chen and Claude
6f4c3a3e21
ci: Bark 推送逻辑提到独立脚本, 两 job 复用
...
CI / Release / CI (push) Successful in 3m15s
CI / Release / Release (push) Successful in 4m43s
之前 ci/release 各自 inline 拼 payload + curl, 重复 25 行 x 2.
device_key / endpoint / group / ttl 硬编码两次, 改一处忘另一处
会出现两个不同 group 的推送.
新建 .github/scripts/bark-notify.sh:
bash .github/scripts/bark-notify.sh "<title>" "<body>"
脚本封装:
- endpoint / device_key / group (Gitea) / isArchive / ttl 单一来源
- heredoc 拼 JSON, 不依赖 jq
- 错误用法打印 usage 并 exit 1 (set -euo pipefail)
workflow 两处都改成一行:
bash .github/scripts/bark-notify.sh "CI \${REF_NAME} \${STATUS}" "\${BODY}"
bash .github/scripts/bark-notify.sh "\${TAG_NAME} \${STATUS}" "\${BODY}"
emoji (✅ /❌ ) 跟 status 映射仍在 workflow 里, 因为不同 job 拼
不同的 title prefix (CI vs release), 脚本不管这个, 调用方传什么
就推什么.
chmod +x 加在 commit 里, 但 git mode bit 在 .gitattributes 或
core.fileMode 关时可能丢. runner 一般会按 working tree 模式跑,
不需要额外 chmod 在 step 里.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 16:36:07 +08:00
tao.chen and Claude
44a30c4a8a
ci: Bark 通知 group 改 Gitea, CI/Release 成功失败都推
...
CI / Release / CI (push) Successful in 2m52s
CI / Release / Release (push) Has been skipped
group 从 "release" 改为 "Gitea" — 不再按事件分类, 跟 Bark 设备
group 设置对齐.
两个 job (ci / release) 都加 if: always() Bark step, 任何结束
状态 (success / failure / cancelled) 都推送. 之前 release job
只在 success() 触发, 失败时收不到通知, 调试慢.
通知内容带 job.status:
- 成功: 标题 "v1.0.0 success", body 加 ✅ emoji + release URL
- 失败: 标题 "v1.0.0 failure", body 加 ❌ emoji + release URL
(release job 失败时) 或 actions run URL (ci job 失败时)
Bark 端 group 分类: "Gitea" group 收所有 CI/release 推送, 用户
在 Bark 设备上一目了然.
\${{ job.status }} 是 GitHub Actions 标准 context, Gitea 兼容
(API-compatible).
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 16:33:41 +08:00
tao.chen and Claude
287d098e06
ci: release upload 改用 Gitea API 直调
...
CI / Release / CI (push) Successful in 1m55s
CI / Release / Release (push) Successful in 6m28s
softprops/action-gh-release@v2 在 Gitea 上偶发不工作, 改用
Gitea 原生 API:
POST /repos/{owner}/{repo}/releases <- 创建 release
POST /repos/{owner}/{repo}/releases/{id}/assets <- 上传 binary
token 走 secrets.GITHUB_TOKEN (Gitea 自动注入平台 token),
api_url 走 \${{ github.api_url }} (Gitea 自动解析).
RELEASE_ID 从 POST /releases 响应 grep 出来 (用 -oP "id":\s*\K[0-9]+).
- PCRE 模式 (grep -P) 依赖 GNU grep, ubuntu 镜像自带有, alpine
runner 需要补 pcre 工具. 跟之前去 jq 一样是 runner 镜像依赖,
这次保留因为已经在用 \${{ secrets.GITHUB_TOKEN }} 这种 GitHub
风格 secret, runner 假定 ubuntu-family.
asset 上传字段是 attachment (Gitea API 文档要求, 不是 name).
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-15 09:37:57 +08:00
tao.chen and Claude
e2bbbd5c45
ci: Bark 通知 group 改 release
...
CI / Release / CI (push) Successful in 1m49s
CI / Release / Release (push) Failing after 1m31s
之前用 "backup" 是从另一个 workflow 抄过来, 不准确. release 通
知归档到 "release" group, Bark 端按 group 分类更直观.
只改一处, 不动其他逻辑.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 19:22:00 +08:00
tao.chen and Claude
1cf4f90452
ci: release 通知 step 去掉 jq 依赖
...
CI / Release / CI (push) Successful in 5m42s
CI / Release / Release (push) Has been skipped
之前用 jq -n --arg 拼 JSON payload, 依赖 runner 镜像预装 jq.
Gitea act_runner 镜像可以自定义 (alpine / distroless / 极简
ubuntu), 不一定带 jq. 一旦缺, release 通知 step 静默失败
(curl 仍然发但 payload 是空字符串, Bark 端按 400/500 处理),
release 仍然成功上传但 user 收不到通知.
改用 heredoc 直接展开 shell 变量拼 JSON:
PAYLOAD=$(cat <<EOF
{ "device_key": "...", "title": "${TITLE}", ... }
EOF
)
TITLE (tag 名, 形如 v0.0.1-beta) 和 BODY (release URL, 只含
https URL + tag 字符串) 都没有 ", \, 控制字符, heredoc 展开
安全. 不需要 jq 转义.
这一步跟 release upload 串行 (release job 本身顺序 steps), Bark
通知仍然是 release 成功的最后一步, if: success() 保留.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 19:21:18 +08:00
tao.chen and Claude
2bd67a5afc
ci: release 后调 Bark 推送 release 通知
...
CI / Release / CI (push) Failing after 16m37s
CI / Release / Release (push) Has been skipped
release job 在 softprops/action-gh-release@v2 上传完
spark-mcp-linux-amd64 后, 调 Bark (http://101.43.40.124:81/push ) 推
一条 device_key 绑定的通知, 标题用 github.ref_name (e.g.
v0.0.1-beta), body 带 release 页面 URL.
device_key 直接写进 workflow, 是公开的推送 key, group=backup
归档, ttl=3600 (1 小时后从 Bark 设备上消失).
if: success() 保证 release upload 失败时不会发通知, 避免推送
"已发布" 但实际没有的误导信息.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 19:19:35 +08:00
tao.chen
1460303b3e
update: GOPROXY in release workflow
CI / Release / CI (push) Successful in 3m37s
CI / Release / Release (push) Failing after 1m27s
2026-07-14 19:11:43 +08:00
tao.chen
02499922b4
update: GOPROXY in building workflow
CI / Release / Release (push) Has been cancelled
CI / Release / CI (push) Has been cancelled
2026-07-14 19:06:53 +08:00
tao.chen and Claude
25c2fc66be
ci: 改 workflow 用国内镜像装 Go + goproxy.cn 加速
...
CI / Release / CI (push) Successful in 7m45s
CI / Release / Release (push) Has been skipped
之前 ci/release 两个 job 用 actions/setup-go@v5 从 go.mod 装 Go,
内置 cache. 在 Gitea runner 所在网络区域 (ping 23ms 短, 推测
国内) 跑得动但慢.
改用用户提供的脚本手动装 Go 1.25.5 (NJU 镜像) + goproxy.cn 代理
+ GITHUB_PATH/GITHUB_ENV (Gitea runner 也兼容这俩 env var).
actions/setup-go 整个去掉, 因为手动装后 PATH 跟 GOPROXY 都设好,
go build 直接可跑.
GITBUB_PATH/GITHUB_ENV 是 GitHub Actions 的特殊 file path env var,
Gitea Actions 兼容 (api-compatible). 写到这两个文件等价于在
后续 step 设置 PATH 和 环境变量.
保留 go build / go vet / go test / gofmt -l . 四项 CI gate.
保留 release job 的 tag 触发 + softprops/action-gh-release@v2 上传.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 19:03:29 +08:00
tao.chen and Claude
80e644483a
Add Gitea Actions CI/release workflow
...
CI / Release / Release (push) Has been cancelled
CI / Release / CI (push) Has been cancelled
The ci job triggers on every push to main and every pull request; the release job triggers only on tag pushes matching v* and needs the ci job to pass first.
We place the workflow under .github/workflows/ because Gitea Actions is API-compatible with GitHub Actions and discovers workflows in that directory.
The release build command is GOOS=linux GOARCH=amd64 go build -o spark-mcp-linux-amd64 ./main.go, producing the Linux amd64 artifact requested for deployment.
CI runs go build ./..., go vet ./..., go test ./..., and fails if gofmt -l . reports any unformatted files.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 16:11:27 +08:00
tao.chen and Claude
25f93dfb59
admin: submissions page, download endpoint, uploads search by app_id
...
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 13:45:11 +08:00
tao.chen and Claude
573397dbb4
tools: persist spark_submit calls to SubmissionRepo
...
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 13:45:07 +08:00
tao.chen and Claude
a2232cecc4
storage: add spark_submissions table and SubmissionRepo
...
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 13:45:03 +08:00
tao.chen and Claude
d6e3952a4b
executor: parse tracking_url and error on missing application_id
...
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 13:45:01 +08:00
tao.chen and Claude
df9b01272f
admin: uploads list view with search, sort, and delete
...
Add /admin/uploads HTML page and JSON API backed by the upload_files
DB index. The page lists file_id, name, size (KB), uploaded_at, and
sha256 with client-side search by name, column sort, and per-row delete.
DELETE /admin/uploads/:id removes the DB index row and the on-disk
data file plus .meta.json sidecar. Admin deletes are written to the
audit log.
Wire the new dependencies through admin.Mount and main.go, and run
the backfill step on startup so pre-existing sidecars are indexed.
Tests cover auth, HTML rendering, JSON search, delete removing all
artifacts, and audit entry creation.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 11:39:56 +08:00
tao.chen and Claude
876f1c9edb
uploads: dual-write uploads to DB, startup backfill, and audit log
...
Store now indexes every Save into the upload_files table via SetRepo.
DB failures are logged as warnings and do not fail the upload because
.meta.json remains the source of truth and startup backfill recovers.
Add Store.Backfill to walk Root at startup and insert index rows for
any pre-existing .meta.json sidecars, swallowing duplicate-key races.
The upload_file MCP Tool now writes an audit_log entry on success.
Tests cover dual-write args, repo-error non-failure, and backfill
skipping existing rows.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 11:30:39 +08:00
tao.chen and Claude
1796b5b872
storage: add upload_files table and UploadRepo
...
Add the upload_files SQLite table and UploadRepo CRUD methods
(Create/Get/List/Delete). The table stores file_id, name, size,
sha256, and uploaded_at as a queryable index.
The .meta.json sidecar remains the source of truth for uploads;
the DB is only an index for admin visibility. On startup the next
commit will backfill any pre-existing sidecars into this table.
Tests cover Create/Get/List/Delete round-trip, name search
filtering, descending time ordering, and limit defaults/cap.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 11:21:38 +08:00
tao.chen and Claude
4af166ea48
tools: spark_submit 记录拼装命令到 per-tool log
...
之前 d.Logger.Debug("spark_submit.built", "argv", cmd) 在 spark-submit
执行前 log 一次, 但 LOG_LEVEL=info 默认看不到, 拼装结果只活在
运行时 stack. 现在把 binary + argv 合并进 callLog.WithResult 写入
per-tool 文件 (data/logs/tools/<ts>_spark_submit_<id>.log), 每次
执行都在 tail -f 的现场能 grep 出来.
tail -f data/logs/tools/*.log | grep -A1 '"argv"'
-> 看每次实际传给 spark-submit 的 argv, 含 script_path 在最后
保留 Debug 调用: verbose 模式下用 base.Logger 直接看到完整 argv 跟
cluster_id, 不依赖 file tail.
两份记录 (Debug + WithResult) 字段一致, 都是 binary + argv, 排查
spark-submit 行为时可以交叉对照.
测试:
- 已有 TestSparkSubmit_StructuredCommand 跑通, argv 字段被
JSON marshal 进 resultRaw, 不需要新增测试
- go test ./... 全绿
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 10:51:03 +08:00
tao.chen and Claude
157848d87a
main: bound startup sweep with context timeout
...
Sweep() used to run synchronously at startup without any time bound.
On a large uploads directory this can block the server for 30s or more,
keeping /healthz returning 503 while the reaper works. Add a context
timeout so startup continues if the sweep cannot finish within budget.
The timeout is set to 5 seconds: long enough to clear typical leftover
state, short enough that a stuck sweep will not meaningfully delay
health checks or startup readiness.
Change Store.Sweep to accept a context.Context and check ctx.Err() at
the top of each iteration. If the context is cancelled or times out,
Sweep returns the files deleted so far and ctx.Err().
Add TestStore_Sweep_HonorsContextCancel to ensure a cancelled context
causes Sweep to exit early instead of running to completion.
Deliberately unchanged: there is still no periodic reaper. The server
only sweeps once at startup; this change just bounds that single sweep.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 10:18:13 +08:00
tao.chen and Claude
97f6184f65
cluster: deprecate DefaultSubmitArgs (no longer prepended)
...
cluster.DefaultSubmitArgs is now deprecated. It used to be prepended to
spark_submit Tool calls, but since 7920dc9 the spark-submit builder
constructs argv from the structured fields in the tool call. The field is
still accepted by the admin API and still persisted to the DB, so this is
a non-breaking change.
This commit deliberately does NOT remove:
- the Cluster.DefaultSubmitArgs field in internal/cluster/types.go
- the default_submit_args DB column
- the admin API read/write support in internal/admin/router.go
- existing storage/repo tests
Runtime now logs a one-time slog.Info when the PUT handler receives a
non-empty default_submit_args value, warning operators that the field is
ignored by spark_submit and that they should use spark_conf / extra_args
instead.
Follow-up removal should touch:
- the Cluster struct field and its JSON tag
- storage/cluster_repo.go scan/insert/update SQL and params
- admin/router.go JSON round-tripping
- storage/cluster_repo_test.go assertions
- the DB migration dropping the default_submit_args column
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-14 10:11:39 +08:00
tao.chen
dab4cd062e
修复 spark_submit 参数重复、上传校验绕过等 8 处缺陷
...
对应代码审查发现的问题(#1, #2 , #4-#8, #10):
#1 CRITICAL:spark_submit 在 argv 中重复拼接 binary。此前
cmd := []string{binary} 后又把 cmd 作为 Args 传给 executor.Run,
而 executor 会再拼一次 Binary,导致 OS argv 为 [binary, binary, ...],
spark-submit 会把自身当作应用 jar。现在 cmd 从 --master 开始,
executor.Run 使用 Binary + Args,argv 正确。
#2:Validate 曾接受 .meta.json 路径本身。现在显式拒绝 sidecar 路径,
要求传入数据文件路径。
#4:Sweep 对 sidecar 损坏的数据文件跳过清理。现在损坏 sidecar 会回退
到数据文件 mtime,超期即删除。
#5:upload_file 描述仍引用已移除的 args 字段,已改为引用 script_path
及结构化字段。
#6:Deps.UploadStore 改为值类型 uploads.Store,避免 nil 绕过上传校验;
移除 spark_submit/upload_file 中的 nil 检查。
#7:master/queue/executor_memory 增加空字符串校验。
#8:提取 buildSparkSubmitCommand 构建 argv,消除双写参数的结构性根因。
#10:Validate 失败时记录 slog.Warn("spark_submit.unminted_path_rejected")。
新增测试:
- TestSparkSubmit_StructuredCommand:断言 argv 首行为 --master,末行
仍为 script_path。
- TestSparkSubmit_EmptyMaster:空 master 返回错误。
- TestStore_Validate_RejectsSidecarPath:拒绝 .meta.json 路径。
- TestStore_Sweep_DeletesDataWithCorruptSidecar:损坏 sidecar 的数据文件
被清理。
未在本提交处理:
- #9 cluster.DefaultSubmitArgs 弃用留作后续批次。
Co-Authored-By: tao.chen <93983997+taochen-ct@users.noreply.github.com >
2026-07-14 10:09:48 +08:00
tao.chen and Claude
4a28886502
tools: 清理 fetch_url.go S1017 lint 警告
...
matchAllowedHost 里两处 if strings.HasPrefix(...) pattern = pattern[N:]
替成无条件 strings.TrimPrefix, 行为完全等价 (前缀不匹配时
TrimPrefix 是 no-op). 触发 S1017 (Should replace this if statement
with an unconditional strings.TrimPrefix).
跟 475e353 在 httpclient/ssrf.go 上做的清理一模一样, 当时
只清了一处, fetch_url.go 里 copy 了同款 pattern 漏了.
净 -4 行, 函数语义不变, 现有测试 (fetch_url_test.go 覆盖的精确
/ 后缀 / wildcard 三种 host 匹配路径) 不变.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:56:24 +08:00
tao.chen and Claude
67d1e9a6c5
gofmt: 清理 admin/executor 测试文件的对齐空格
...
这两个文件在 HEAD 上就未通过 gofmt, 但 go test 不检查 gofmt,
所以一直没暴露. 修一下让 gofmt -l . 干净, 否则新加的 commit
之前都得先 stash.
纯格式变更, 无逻辑变化.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:55:26 +08:00
tao.chen and Claude
44c9f415b7
tools: 修 spark_submit 校验的两处小问题
...
c9e54a3 引入了 server-minted 校验, 但有两处需要调整:
1. 错误信息用 fmt.Errorf(... %w).Error() 构造, %w 包装语义被丢
掉 (errResult 接收 string, Error() 之后 %w 已经不可达). 改用
fmt.Sprintf 拼接, %q 引用路径, 错误信息保持不变但代码不再误
导.
2. 校验放在 scriptPath 解析之后、queue 解析之前. 错误信息会按字
段出现顺序报 (cluster_id, master, deploy_mode, script_path,
queue, ...), 但当前顺序是 script_path 校验先报, 然后才报 queue
缺失. 把校验挪到所有 RequireString 之后、parseStringMap 之前,
LLM 看错误时字段顺序跟 schema 顺序一致.
nil-safe 校验逻辑不变 (d.UploadStore == nil 时跳过). 现有测试
全部通过:
- TestSparkSubmit_RejectsNonMintedPath 仍通过 (校验位置不影响
行为)
- TestSparkSubmit_StructuredCommand 仍通过 (走 mint 路径)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:55:16 +08:00
tao.chen and Claude
c9e54a3f27
tools: 强制 spark_submit 的 script_path 必须来自 upload_file
...
在 spark_submit handler 中增加 server-minted 校验:script_path 必须通过
本服务器 upload_file 的 UploadStore.Validate,否则立即返回清晰错误,避免
远程 agent 把自身本地路径转发给服务器侧 spark-submit 时出现难以定位的
FileNotFoundException。
校验对 nil UploadStore 是安全的:当 d.UploadStore 为 nil 时直接跳过,保留
不强制依赖 store 的测试或降级场景。当前 testDepsWithDataDir 始终会注入
UploadStore,所以测试走的是真实校验路径。
同步更新了 script_path 的工具描述,明确声明非 upload_file 铸造的路径会被
拒绝。这是对 7920dc9 中“path 永远在最后”规则的收紧——现在不仅位置固定,
而且必须是本机 uploads 目录下的有效上传文件。
测试调整:
- 新增 TestSparkSubmit_RejectsNonMintedPath:使用 t.TempDir() 下未通过
upload_file 写入的文件作为 script_path,断言返回错误并包含
"not from upload_file"。
- TestSparkSubmit_MissingRequiredField / TestSparkSubmit_BadSparkConfValue
原来使用 "/tmp/script.py" 作为占位路径,现在会先通过 UploadStore.Save
生成一个铸造路径,确保它们继续分别验证 master 缺失和 spark_conf 值类型
错误,而不是被新的路径守卫拦截。
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:49:26 +08:00
tao.chen and Claude
7920dc9597
tools: 仿照 Python build_spark_submit_command 将 spark_submit 改为结构化参数
...
将 spark_submit 的自由 args[] 替换为与 Python 服务一致的 schema:cluster_id、
master、deploy_mode、script_path、queue、executor_memory、executor_cores、
num_executors、spark_conf、extra_args。命令行顺序严格对齐 Python 实现:
binary → --master → --queue → --executor-memory → --executor-cores →
--num-executors → 按 key 排序的 --conf key=value → 按 key 排序的 --flag value →
script_path 永远在最后。
deploy_mode 是 Python 参考函数的入参,但该函数实际并不输出 --deploy-mode;
Go 端保留这个字段并校验其值,但同样不发射它,以保持行为一致。如果后续需要
--deploy-mode,再在此处添加并记录差异。
cluster.DefaultSubmitArgs 不再被拼接到 argv 中;该字段仍保留用于 admin API
向后兼容,运行时移除它的工作是后续独立的 follow-up。
删除了 translateArgs、pathLikeArg 以及 spark_submit.unminted_path 警告逻辑
(本次提交取代 2570713),因为自由参数已不存在,警告无从触发。新增
parseStringMap、sortedStringKeys 和 requireNonNegativeInt 辅助函数;
requireNonNegativeInt 同时接受 int 与 float64,以兼容测试直接构造的整数
字面量和 JSON 解码后的 float64。
测试更新:
- TestSparkSubmit_StructuredCommand:验证完整 argv 顺序,并断言 script_path
是最后一项。
- TestSparkSubmit_MissingRequiredField:校验缺少 master 时返回错误。
- TestSparkSubmit_BadSparkConfValue:校验 spark_conf 的值不是字符串时报错。
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:35:16 +08:00
tao.chen and Claude
2570713fbf
tools: 为 spark_submit 的未 mint 绝对路径增加可观测警告
...
LLM 有时会把自己本地的绝对路径直接传给 spark_submit;这个路径在 MCP
服务器上不存在,spark-submit 会失败,但当前 translateArgs 是静默透传,
问题难以定位。添加 logger.Warn("spark_submit.unminted_path"),对
服务器未 mint 的绝对路径/路径型参数发出警告,同时保持透传不变。
生产环境中这条警告是诚实的可观测信号,而不是回归指标:集群本地合法路径
(如 /opt/spark/examples/pi.py)也会触发警告,这是设计上的,因为我们
无法区分“合法集群路径”和“LLM 本地路径”。
测试锁定:
- minted_path_no_warn:上传文件返回的 path 不触发警告
- cluster_local_path_warns:未 mint 的绝对路径触发一次警告且 path 正确
- non_path_args_never_warn:--class、Main、--master 等普通参数不触发警告
translateArgs 对 logger 做 nil-safe 处理:测试中 deps.Logger 未设置时不
会 panic,也不会产生日志,保持现有测试的简洁。
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:18:35 +08:00
tao.chen and Claude
f374ebfdb4
uploads,tools,config,main: 将 upload_file 返回路径改为服务器端绝对路径并透传给 spark_submit
...
之前 upload_file 把文件写到 DataDir/uploads/<filename>,返回相对路径
uploads/hello.txt。MCP 服务器与 agent 不在同一台机器,agent 无法构造服务
器本地路径,因此 spark_submit 无法定位脚本。
改为由新的 internal/uploads 包 mint 一个 32 字符十六进制 file_id,文件落盘为
DataDir/uploads/<file_id>,元数据写入 <file_id>.meta.json(原始文件名只存在
sidecar 里)。upload_file 返回 {file_id, path, name, size, sha256},其中 path
是绝对路径。spark_submit 的 description 明确要求 LLM 直接把 upload_file 返回
的 path 放进 args,不要自己构造路径。
为什么只在描述里约束而不在代码里拒绝非 mint 的绝对路径:集群本地已有路径
(如 /opt/spark/examples/pi.py)是合法的 spark-submit 参数,代码不能替
LLM 拒绝。
测试锁定:
- internal/uploads: Save 往返、AbsPath/Validate 非法路径、Sweep 过期/未过期/
孤立 sidecar
- internal/mcp/tools: upload_file 新响应字段、spark_submit 透传 mint 路径与
集群本地路径
刻意未做:S3/HDFS 上传、给 spark_submit 新增 file_id 参数、在代码层面拒绝
集群本地绝对路径。
破坏性变更:upload_file 响应从相对 path 改为绝对 path,并新增 file_id/name/
sha256 字段。该工具尚无外部调用者。
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 19:13:14 +08:00
tao.chen and Claude
210aeb1315
docs: CLAUDE.md 补 project-specific 工作流校准
...
~/.claude/CLAUDE.md 已经定义 Codex-first 路由、Codex MCP 强制参数、
Linus 三问、4-phase 工作流、prompt 模板、auto-confirm/pause 边界。本
文件不重复这些全局规则, 只加项目级校准:
- 路由表: 用本仓库实际任务举例 (GIN_MODE 接线 / loadDotenv 新增 /
S1017 修 / .gitignore 行 / CLAUDE.md 自身 / 注释 typo / .env.example
标题), 给出"trivial 阈值"在本项目的具体判据
- Codebase 模式: 指向 internal/httpclient/redirect_test.go (httptest
+ "127.0.0.1" 白名单), internal/logging/tool_call_test.go
(bytes.Buffer + slog.NewJSONHandler 捕获), internal/mcp/tools/
目录约定, 让未来 agent 进包前先读已有测试文件
- 预提交检查 (项目级): 强调 deferred-behavior test 是 gap-locking
不是回归, 公共签名变更必须在 commit body 解释兼容性
- commit 消息规约: 从 git log 实际样本里抽出 <package>: 中文动词
开头 + body 回答"gap / fix / 风险 / 锁定测试" + Co-Authored-By
- 项目坑: .env 不透明 / internal/ Go 强制 / multiHandler 双写
(新事件 = 一次 base.Info 调用) / TestDoWithRedirect_DeferredRFC9110Gaps
是 gap 测试不是 bug 报告
仅改 CLAUDE.md, 0 代码变更, 无需 build/test 验证.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 18:14:40 +08:00
tao.chen and Claude
475e353850
httpclient: 清理 ssrf.go S1017 lint 警告
...
matchAllowedHost 里两处 if strings.HasPrefix(...) pattern = pattern[N:]
替成无条件 strings.TrimPrefix, 行为完全等价 (前缀不匹配时 TrimPrefix
是 no-op). 触发 S1017 (Should replace this if statement with an
unconditional strings.TrimPrefix).
净 -4 行, 函数语义不变, matchAllowedHost 现有测试 (ssrf_test.go)
覆盖的精确/后缀/wildcard 三种 host 匹配路径都不变.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 16:26:17 +08:00
tao.chen and Claude
3bc8bd49f6
httpclient: 记录 RFC 9110 redirect gap, deferred test 锁住当前行为
...
代码 review 发现 DoWithRedirect 有两个 RFC 9110 §15.4 标准违反,
但当前不修:
1. 303 See Other 应该把 method 切到 GET, 当前保持原 method (例如
POST → 307 → 继续 POST). YARN/SHS 当前不用 303, gap 未触发.
2. 307 Temporary Redirect / 308 Permanent Redirect 收到 POST 时
应该原 method + body 重发, 当前 body 强制置 nil. YARN amContainerLogs
是 GET 触发 307, gap 未触发.
不修原因: 没有真实用户报告. 修 307 body 重发要 buffer 起来 + 重写
Content-Length, 容易出 subtle bug (multipart/trailer). 修复成本 vs
收益不成比例.
新增 TestDoWithRedirect_DeferredRFC9110Gaps (2 subtest) 锁定当前
行为, 未来要修必须主动改测试 → 强制 review 行为变更. 同时给
redirect.go 加 TODO(redirect) 注释详细说明两个 gap 和修法方向.
subtest 修正一个错位: 303 的 body 丢失不是 gap (RFC 规定 303 必须
丢 body, 跟 method 错没错无关), gap 只是 method 保留 vs 切 GET.
已对应调整注释和断言.
未提交: .env (gitignored 内容之外有意未提交)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 16:24:40 +08:00
tao.chen and Claude
1a7c692e79
gitignore: 跨平台 build 产物 spark-mcp-*-*
...
覆盖 GOOS-GOARCH 命名约定的所有二进制变体 (spark-mcp-linux-amd64,
spark-mcp-darwin-arm64 等). 之前每次交叉编译都会留一个未跟踪的
二进制在仓库根.
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 15:28:01 +08:00
tao.chen and Claude
9bcfe558cf
logging: tool.call.error/end 接入主 logger (双写终端 + spark-mcp.log)
...
之前所有 tool 调用的 error 和 end 事件只写进 per-tool 文件
(data/logs/tools/<ts>_<tool>_<id>.log), 主日志文件 spark-mcp.log 完全
看不到。tail -f spark-mcp.log 排查问题时形同虚设, 必须去找具体的
per-tool 文件才能看到错误。
修复: toolCallLogger struct 加 base *slog.Logger 字段, StartToolCall
把传入的 base 写进 struct. WithError 在记录错误的同时调 base.Error
("tool.call.error", tool, name, err), End 在写完 per-tool 文件后
调 base.Info("tool.call.end", tool, name, duration_ms, error).
base 走的是 main logger (multiHandler 包装), 自动同时写终端 (遵循
LOG_FORMAT, 默认 text) 和主日志文件 (永远 JSON). LOG_LEVEL 过滤对
这两条事件仍生效, 行为统一.
新增 3 个单元测试 (internal/logging/tool_call_test.go):
- TestStartToolCall_EmitsErrorAndEnd: 验证错误路径两条事件都进
主 logger (用 bytes.Buffer 捕获 slog 输出, 解码 JSON 校验字段)
- TestStartToolCall_NilBaseSafe: 验证 base=nil (测试场景) 不 panic
且 per-tool 文件仍写入
- TestStartToolCall_SuccessfulCall_EmitsEndOnly: 验证成功路径
只发 end 不发 error, duration_ms >= 0
附带: gofmt 重排 internal/logging/tool_call.go import 块 (按字母序)
未提交: spark-mcp-linux-amd64 (本地 build 产物, .gitignore 未拦,
需另决定); .env (gitignored 内容)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 15:27:28 +08:00
tao.chen and Claude
2537976722
rm: list_applications 加 limit/queue + 日志 fallback 链补 NM 直连
...
P0 (Python parity 缺口):
- rm.Client.ListApps 新增 queue + limit 查询参数
- list_applications MCP tool schema 暴露 queue (string) 和 limit
(number, default 100), handler 透传给 RM client
- 限流原因: 生产 RM 上无 limit 会拉回 N MB JSON 撞 MaxResponseBytes
- limit <= 0 不发送 query 参数, 跟 RM 默认行为一致; 老的调用方
(TestListApplications_EndToEnd 等) 不传参时行为不变
- 4 个旧 ListApps 测试调用点跟着更新, 加 3 个新测试:
TestListApps_WithLimit, TestListApps_WithQueue,
TestListApps_LimitZeroNoParam
P1.3 (Python fallback 行为补全):
- rm.Client.GetLogs 加第 4 步 fallback: 当前 3 步
(amContainerLogs/aggregated-logs/logs) 全失败时, 调 GetApp
解析 app.amContainerLogs 字段, GET 该 URL 直连 NodeManager
- 新 source 名 'amContainerLogs-direct' 区分 RM-level endpoint
- 提取 doBytesWithHosts(extraHosts...) 支持 per-call host 白名单,
NM host 动态加进 allowedHosts, SSRF 保护不破 (URL 是 RM 响应里
回来的, 不是 LLM 任意填的)
- 新增 TestGetLogs_FallbackToAMDirect (成功路径) 和
TestGetLogs_FallbackFailsWhenAMFieldMissing (amContainerLogs
字段为空时正常返回 error)
P1.4 (零代码改动 + 文档化):
- get_application_logs / get_application_status 工具 description
更新, 提示 LLM raw RM JSON 里包含 amContainerLogs 字段, 可在
aggregated-logs 全部失败时自己用 fetch_url 直连 NM
- get_application_status 函数体不变 (本就是透传 raw JSON)
验证:
- go build / go vet / go test 全部通过
- 5 个新测试全 PASS, 老的 TestListApplications_EndToEnd 仍 PASS
- 单 ListApps 调用点 (list_applications.go:81) 编译过
未提交: spark-mcp-linux-amd64 (本地 build 产物, 当前 .gitignore
没拦, 需另行决定)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 15:15:21 +08:00
tao.chen and Claude
bf2640d8fd
admin: /admin HTML 公开可访问,让 token 弹窗真正能触发
...
上一轮 (90eba22 ) 把 admin 页改成 token 弹窗模式,但 webHandler 还
挂在带 AdminAuth 的 group 里。结果无 token 访问 /admin 拿到的不是
HTML,而是 {"error":"missing bearer token"} 的 401 JSON,弹窗
变成死代码。
修复: 拆出 gPublic group,只放 webHandler (GET /admin 和 /admin/)。
API 路由 (/clusters*, /audit) 和 OpenAPI docs (/docs, /docs/spec)
继续受 AdminAuth 保护 (后者暴露内部 API 结构,不该公网可见)。
HTML 自身的 token 输入流不变 — 弹窗仍然在客户端拦截 401、清旧 token、
记录 pendingRetry、保存新 token 后自动重试。
补一个 TestAdminWeb_NoTokenReturnsHTML 锁定新契约:
- GET /admin 无 token -> 200 + text/html (含 token-modal 元素)
- GET /admin/ 同上
- GET /admin/clusters 无 token -> 401 (回归保护)
- GET /admin/clusters good-token -> 200 (回归保护)
旧 TestMount_RequiresAuth 测的全是 API 路径, 不受影响, 全绿。
验证:
- go build / go vet / go test 全部通过
- 端到端 curl 复现用户报告场景: /admin 无 token -> 200 + 弹窗 HTML
- API + docs 鉴权行为不变
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 14:16:49 +08:00
tao.chen and Claude
90eba221d4
admin: token 输入改为弹窗模式 (modal)
...
把 header 内嵌的 token 输入框 + Save 按钮拆掉,改用原生 <dialog>
弹窗。三种触发场景: 首访无 token 自动弹、API 401 自动弹 (清旧 token
后弹带错误信息)、用户主动点 Set Token 按钮弹。无有效 token 时阻止
Escape 关闭 (cancel 事件 preventDefault), 防止误操作丢失输入。
localStorage 键名 adminToken 不变, 零迁移成本。401 路径在 api()
内部清 token + 弹窗 + 记录 pendingRetry 闭包, 用户保存新 token 后
自动重试失败请求。
设计取舍:
- 选原生 <dialog> 而非自造 div + JS, 自带 modal/背板/焦点陷阱/a11y
- 选 <form> + preventDefault 而非 method='dialog', 避免自动关闭
- CSS 全部复用现有 dark theme 变量, 0 视觉割裂
- 0 新依赖 (纯 HTML5 + 原生 JS)
验证:
- go build / go vet / go test 全部通过
- 端到端 curl 确认新元素嵌入、旧元素删除、401/200 路径正常
- router_test.go 无回归 (HTML 改动不涉及 Go)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 14:08:16 +08:00
tao.chen and Claude
7f8960823b
config: GIN_MODE 改为环境变量配置 + Load() 自动加载 .env
...
main.go 硬编码 gin.ReleaseMode 改成读 cfg.GinMode(默认 release),
方便生产/调试切换 debug/release/test。Load() 开头先调 loadDotenv
(".env"): 缺文件静默跳过,shell env 优先于 .env,跳过注释/空行/
无 '=' 行并 slog.Warn。仅用标准库,不引新依赖。
验证:
- go build / go vet / go test 全部通过
- shell env 覆盖 .env (LISTEN_ADDR=:19090 覆盖 .env 的 :18080)
- 仅 .env 时正确加载 (监听 18080, GIN_MODE=debug 输出 [GIN-debug])
- 无 .env 且无 shell env 时清晰报错 ADMIN_TOKENS is not set
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-13 13:48:35 +08:00
tao.chen
1dfef0f598
init
2026-07-10 10:19:28 +08:00