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
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
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
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
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
be4ca460c9
Phase 5 Batch 1 (补): mcp/server + 2 Tool (Batch 1 commit 时漏 stage)
...
上一个 commit (7056657 ) 漏 add 的 mcp 包文件:
- internal/mcp/server.go (Streamable HTTP Handler)
- internal/mcp/tools/list_clusters.go (发现入口)
- internal/mcp/tools/spark_submit.go (本地 exec 提交)
- internal/mcp/tools/helpers.go (textResult/errResult/encodeJSON)
deps.go / rm.go 已在 Phase 5 Batch 2 一起 commit, 此处不重复。
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-10 17:24:10 +08:00
tao.chen and Claude
e6411b0dc4
Phase 5 Batch 4: analyzer 规则引擎 + 3 个高层 Tool
...
- internal/analyzer: 纯函数规则引擎
- Finding / Severity / Thresholds / Input / StageMetric / ExecutorMetric
- 3 规则: data_skew (max/min ratio), gc_pressure (GC/CPU ratio),
bottleneck (shuffle read+write GB)
- 严重度阶梯: warning / critical (阈值 ×2)
- Analyze 入口数据驱动 for ... range
- 11 个单测 (3 规则 × normal/warning/critical + 零除防御)
- fetch_spark_metrics: SHS 业务封装, format=summary 触发 analyzer
- fetch_cluster_env: RM /cluster/info + /cluster/metrics 聚合
- analyze_spark_log: RM 日志降级链 + LLM-Ready Prompt 拼装
- prompt 结构: cluster 元信息 + log source + log tail +
heuristic findings + suggested LLM analysis
- findings 是 first-pass filter, LLM 做最终判断
- deps.go: +AnalyzerThresholds
- main.go: 注入 cfg 的 3 个阈值
- 端到端实测: analyze_spark_log 完整 prompt 渲染
完成 11 个 Tool 表面 (list_clusters / spark_submit / 4 RM /
fetch_spark_metrics / fetch_cluster_env / analyze_spark_log /
fetch_url / upload_file)
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-10 16:53:47 +08:00
tao.chen and Claude
d4ad97f595
Phase 5 Batch 3: fetch_url + upload_file Tool (底层原语)
...
- fetch_url: 通用 HTTP 原语
- host allowlist 校验 (cluster.URLAllowlist + RM/SHS host 兜底)
- 复用 httpclient.ApplyAuth + DoWithRedirect
- mcp.WithEnum 约束 method
- 单元测试: 9 子测试 (basic auth 透传 + 跨主机 redirect 保留 auth)
- upload_file: 本地文件存储 (供 spark_submit 引用)
- filename sanitize: [a-zA-Z0-9._-], 拒绝路径逃逸
- filepath.Base 二次校验
- 写到 <DataDir>/uploads/<name>, 权限 0640
- 单元测试: 路径遍历全部拒绝
- deps.go: +DataDir
- main.go: 注入 cfg.DataDir
- 端到端实测: upload_file + spark_submit 引用上传脚本
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-10 16:52:10 +08:00
tao.chen and Claude
c4ac3cc354
Phase 5 Batch 2: 4 RM Tool + rm 客户端 + 日志降级链
...
- internal/rm/client.go: YARN RM 客户端
- ListApps / GetApp / KillApp / GetLogs
- GetLogs 降级链数据驱动 (amContainerLogs → aggregated-logs → logs)
- 跨主机 redirect 保留 Authorization (URLAllowlist 兜底)
- 4 Tool: list_applications / get_application_status /
get_application_logs / kill_application
- mcp.WithEnum(state) 约束 YARN app 状态
- tool handler 永远 (result, nil), 业务错误用 NewToolResultError
- deps.go: +HTTPClient + MaxResponseBytes
- main.go: 注入 httpclient.Client
- 端到端实测: mock RM 验证 4 Tool + 降级链 + 错误路径
Co-Authored-By: Claude <noreply@anthropic.com >
2026-07-10 16:50:27 +08:00