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