 tao.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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.chenandClaude
|
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 |
|