patchwarden 1.1.0 → 1.5.1
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/README.en.md +83 -8
- package/README.md +78 -10
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +261 -255
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +45 -0
- package/dist/tools/evidencePack.js +375 -0
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/policy/projectPolicy.ts +344 -0
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +254 -252
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +251 -0
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# PatchWarden User Feedback
|
|
2
|
+
|
|
3
|
+
This file tracks real feedback only. Do not add invented users, fake adoption,
|
|
4
|
+
or private conversations that cannot be shared.
|
|
5
|
+
|
|
6
|
+
## Current Status
|
|
7
|
+
|
|
8
|
+
No third-party feedback has been imported into this repository yet.
|
|
9
|
+
|
|
10
|
+
Before using this file as application evidence, collect public feedback through
|
|
11
|
+
GitHub issues or discussions from real testers who have tried at least one of
|
|
12
|
+
the documented flows.
|
|
13
|
+
|
|
14
|
+
## Feedback To Collect
|
|
15
|
+
|
|
16
|
+
Ask testers to include:
|
|
17
|
+
|
|
18
|
+
- operating system
|
|
19
|
+
- Node.js version
|
|
20
|
+
- MCP client used, such as ChatGPT, Codex, OpenCode, or another MCP client
|
|
21
|
+
- whether `npm.cmd run doctor` or the documented setup passed
|
|
22
|
+
- what task they attempted
|
|
23
|
+
- whether task evidence, safe summaries, or audit output helped
|
|
24
|
+
- confusing setup steps
|
|
25
|
+
- feature requests
|
|
26
|
+
|
|
27
|
+
## Public Feedback Log
|
|
28
|
+
|
|
29
|
+
| Date | Source | Tester context | Result | Follow-up |
|
|
30
|
+
| --- | --- | --- | --- | --- |
|
|
31
|
+
| Pending | Pending GitHub issue or discussion | Pending | Pending | Pending |
|
|
32
|
+
|
|
33
|
+
## Maintainer Response Rules
|
|
34
|
+
|
|
35
|
+
- Keep feedback public when possible.
|
|
36
|
+
- Redact local paths, logs, keys, tokens, cookies, `.env` contents, and account
|
|
37
|
+
identifiers.
|
|
38
|
+
- Convert recurring friction into issues with `documentation`, `good first
|
|
39
|
+
issue`, `help wanted`, or `security` labels when appropriate.
|
|
40
|
+
- Close the loop by linking fixes, docs, or release notes back to the feedback.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# 为什么需要 PatchWarden
|
|
2
|
+
|
|
3
|
+
> 本文基于 v1.5.1 源码编写;安装时请使用已验证发布的 <published-version>。
|
|
4
|
+
> 面向:使用 ChatGPT、Codex、OpenCode 等 MCP 客户端但需要安全边界的开发者
|
|
5
|
+
|
|
6
|
+
## 背景:本地 Agent 的安全缺口
|
|
7
|
+
|
|
8
|
+
当前主流的本地编程 Agent(如 ChatGPT 桌面端、Codex CLI、OpenCode、OpenHands)
|
|
9
|
+
在带来生产力提升的同时,也引入了新的攻击面:
|
|
10
|
+
|
|
11
|
+
- Agent 可执行本地命令,一旦被 prompt injection 或上下文污染,可能执行越权操作。
|
|
12
|
+
- 通用远程 Shell 模式将完整命令执行能力暴露给 MCP 客户端,缺乏边界。
|
|
13
|
+
- 任务产物分散在 stdout / stderr / diff 中,难以审计与验收。
|
|
14
|
+
- `.env`、token、SSH key、cookie、浏览器状态等敏感文件缺乏强制隔离。
|
|
15
|
+
|
|
16
|
+
PatchWarden 不是另一个通用 Shell。它定位为本地优先的 MCP 安全与验证层
|
|
17
|
+
(local-first MCP safety and verification layer for AI coding agents),
|
|
18
|
+
在不牺牲 Agent 能力的前提下提供可审计的安全边界。
|
|
19
|
+
|
|
20
|
+
## 核心价值
|
|
21
|
+
|
|
22
|
+
### 1. Workspace Confinement(工作区隔离)
|
|
23
|
+
|
|
24
|
+
所有仓库路径必须位于配置的 `workspaceRoot` 之下。PatchWarden 会阻断:
|
|
25
|
+
|
|
26
|
+
- 越出 workspace 的文件读写。
|
|
27
|
+
- 对敏感文件名(如 `.env`、`id_rsa`、`cookies.db`)的访问。
|
|
28
|
+
- 跨仓库的非授权改动。
|
|
29
|
+
|
|
30
|
+
### 2. Command Allowlists(命令允许列表)
|
|
31
|
+
|
|
32
|
+
PatchWarden 采用精确命令匹配,而非宽松的 glob 或前缀匹配:
|
|
33
|
+
|
|
34
|
+
- 只有显式注册的命令才能执行。
|
|
35
|
+
- 参数变化同样受约束,避免命令注入绕过。
|
|
36
|
+
- 不存在“允许 `git` 即允许任意 git 子命令”的隐式放大。
|
|
37
|
+
|
|
38
|
+
### 3. Scope-Violation Detection(范围违规检测)
|
|
39
|
+
|
|
40
|
+
Agent 提出的任务若超出声明的 scope(例如修改了未授权文件、执行了未注册命令),
|
|
41
|
+
PatchWarden 会在执行前或执行中识别并阻断,而不是事后才在日志里发现。
|
|
42
|
+
|
|
43
|
+
### 4. Auditable Task Evidence(可审计任务证据)
|
|
44
|
+
|
|
45
|
+
每个任务通过 Evidence Pack v2 导出 8 个有界文件:
|
|
46
|
+
|
|
47
|
+
| 文件 | 用途 |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `evidence.json` | 完整有界证据包(机器可读) |
|
|
50
|
+
| `EVIDENCE.md` | 人类可读的 Markdown 摘要 |
|
|
51
|
+
| `risk.json` | 聚合的风险项与严重度 |
|
|
52
|
+
| `verify.json` | 每轮迭代的结构化验证记录 |
|
|
53
|
+
| `diffstat.json` | 文件级增删统计(不含完整 diff) |
|
|
54
|
+
| `lineage.json` | 任务谱系与上下文链 |
|
|
55
|
+
| `attestation.json` | 任务签名与证明 |
|
|
56
|
+
| `redactions.json` | 已脱敏内容清单 |
|
|
57
|
+
|
|
58
|
+
这些文件只包含有界摘要,**不**包含完整 stdout/stderr、完整 diff、
|
|
59
|
+
secrets、token、cookie 或凭据路径。
|
|
60
|
+
|
|
61
|
+
## 与通用远程 Shell 的区别
|
|
62
|
+
|
|
63
|
+
| 维度 | 通用远程 Shell | PatchWarden |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| 命令执行 | 任意命令 | 精确允许列表 |
|
|
66
|
+
| 路径访问 | 通常无约束 | 强制 workspace 隔离 |
|
|
67
|
+
| 敏感文件 | 取决于调用方自觉 | 强制阻断 |
|
|
68
|
+
| 任务证据 | 散落在日志 | 8 个有界文件可审计 |
|
|
69
|
+
| 范围违规 | 事后发现 | 执行前/中阻断 |
|
|
70
|
+
| MCP 客户端权限 | 全权委托 | 受约束代理 |
|
|
71
|
+
|
|
72
|
+
PatchWarden 假设上游模型或 MCP 客户端可能出错、过度宽泛或被 prompt injection。
|
|
73
|
+
它不应被信任以无约束方式执行命令。
|
|
74
|
+
|
|
75
|
+
## 适用场景
|
|
76
|
+
|
|
77
|
+
- 使用 ChatGPT / Codex / OpenCode / OpenHands 等 MCP 客户端进行本地代码维护。
|
|
78
|
+
- 需要对 Agent 任务进行可审计验收的团队或个人。
|
|
79
|
+
- 希望将 Agent 限制在特定 workspace 内的开发工作流。
|
|
80
|
+
- 需要生成 spec 验收证据的 spec-driven 开发流程。
|
|
81
|
+
|
|
82
|
+
## 不适用场景
|
|
83
|
+
|
|
84
|
+
- 需要任意远程 shell 访问的场景(PatchWarden 刻意不提供)。
|
|
85
|
+
- 需要读取或修改 `.env`、SSH key、浏览器状态等敏感文件的工作流。
|
|
86
|
+
- 需要 Agent 跨多个不受信任 workspace 自由移动的场景。
|
|
87
|
+
- 不接受命令允许列表约束的通用任务执行需求。
|
|
88
|
+
|
|
89
|
+
## 目标用户
|
|
90
|
+
|
|
91
|
+
- 使用 ChatGPT 桌面端、Codex CLI、OpenCode 等 MCP 客户端的开发者。
|
|
92
|
+
- 在本地维护多个仓库并希望统一安全边界的维护者。
|
|
93
|
+
- 需要为 Agent 任务生成可审计证据的团队。
|
|
94
|
+
- 对 prompt injection 风险敏感、希望有执行前阻断机制的安全工程师。
|
|
95
|
+
|
|
96
|
+
## 安全契约要点
|
|
97
|
+
|
|
98
|
+
PatchWarden 的安全契约(详见 `docs/threat-model.md`)包括:
|
|
99
|
+
|
|
100
|
+
- 不暴露通用远程 shell,不弱化精确命令匹配。
|
|
101
|
+
- 所有仓库路径保持在 `workspaceRoot` 下,阻断敏感名与越界改动。
|
|
102
|
+
- 不读取或持久化 token、cookie、浏览器状态、`.env`、SSH key、凭据文件。
|
|
103
|
+
- 不无差别 kill watcher 或 tunnel,仅监督 launcher 拥有的进程。
|
|
104
|
+
- 保留结构化任务证据、心跳状态、Git before/after 快照、改动文件记录与脱敏。
|
|
105
|
+
|
|
106
|
+
## 下一步
|
|
107
|
+
|
|
108
|
+
- 阅读 `docs/threat-model.md` 了解完整威胁模型。
|
|
109
|
+
- 阅读 `docs/evidence-pack-schema.md` 了解证据包文件结构。
|
|
110
|
+
- 阅读 `docs/dashboard-overview.md` 了解 Control Center 工作流。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchwarden",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped
|
|
3
|
+
"version": "1.5.1",
|
|
4
|
+
"description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped task loops, safe summaries, audits, and Direct editing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
-
"@types/node": "^
|
|
65
|
+
"@types/node": "^26.1.0",
|
|
66
66
|
"typescript": "^6.0.3"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
@@ -229,6 +229,7 @@ async function testStaticFiles() {
|
|
|
229
229
|
if (!rootCt.includes("text/html")) checks.push(`GET / -> Content-Type ${rootCt} (expected text/html)`);
|
|
230
230
|
if (!root.body.includes("setup-checklist-card")) checks.push("dashboard missing setup checklist card");
|
|
231
231
|
if (!root.body.includes("Show Core / Direct log tails")) checks.push("dashboard activity log is not collapsed behind a summary");
|
|
232
|
+
if (!root.body.includes("evidence-pack-card")) checks.push("dashboard missing v1.5 evidence pack card");
|
|
232
233
|
|
|
233
234
|
if (vendor.status !== 200) checks.push(`GET /vendor/tailwindcss-browser.js -> status ${vendor.status}`);
|
|
234
235
|
const vendorCt = vendor.headers["content-type"] || "";
|
|
@@ -667,6 +668,92 @@ async function testOtherGetApis() {
|
|
|
667
668
|
problems.push(`/api/audit error: ${err.message}`);
|
|
668
669
|
}
|
|
669
670
|
|
|
671
|
+
// /api/lineages
|
|
672
|
+
try {
|
|
673
|
+
const res = await httpGet(`${BASE_URL}/api/lineages`);
|
|
674
|
+
if (res.status !== 200) problems.push(`/api/lineages -> ${res.status} (expected 200)`);
|
|
675
|
+
else {
|
|
676
|
+
const json = tryJson(res.body);
|
|
677
|
+
if (!json || !Array.isArray(json.lineages) || typeof json.total !== "number") {
|
|
678
|
+
problems.push(`/api/lineages missing bounded lineage summary fields: ${res.body.slice(0, 120)}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
problems.push(`/api/lineages error: ${err.message}`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// /api/project-policy
|
|
686
|
+
try {
|
|
687
|
+
const res = await httpGet(`${BASE_URL}/api/project-policy?repo_path=.`);
|
|
688
|
+
if (res.status !== 200) problems.push(`/api/project-policy -> ${res.status} (expected 200)`);
|
|
689
|
+
else {
|
|
690
|
+
const json = tryJson(res.body);
|
|
691
|
+
if (!json || !("effective_policy" in json) || !Array.isArray(json.issues)) {
|
|
692
|
+
problems.push(`/api/project-policy missing policy summary fields: ${res.body.slice(0, 120)}`);
|
|
693
|
+
}
|
|
694
|
+
const text = res.body.toLowerCase();
|
|
695
|
+
if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
|
|
696
|
+
problems.push("/api/project-policy leaked log/diff-shaped fields");
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch (err) {
|
|
700
|
+
problems.push(`/api/project-policy error: ${err.message}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// /api/release/status
|
|
704
|
+
try {
|
|
705
|
+
const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
|
|
706
|
+
if (res.status !== 200) problems.push(`/api/release/status -> ${res.status} (expected 200)`);
|
|
707
|
+
else {
|
|
708
|
+
const json = tryJson(res.body);
|
|
709
|
+
if (!json || !("release_readiness" in json) || json.remote_write_performed !== false) {
|
|
710
|
+
problems.push(`/api/release/status missing read-only release status fields: ${res.body.slice(0, 120)}`);
|
|
711
|
+
}
|
|
712
|
+
const text = res.body.toLowerCase();
|
|
713
|
+
if (text.includes("stdout") || text.includes("stderr") || text.includes("diff_patch")) {
|
|
714
|
+
problems.push("/api/release/status leaked log/diff-shaped fields");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
} catch (err) {
|
|
718
|
+
problems.push(`/api/release/status error: ${err.message}`);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// /api/evidence-packs
|
|
722
|
+
try {
|
|
723
|
+
const res = await httpGet(`${BASE_URL}/api/evidence-packs`);
|
|
724
|
+
if (res.status !== 200) problems.push(`/api/evidence-packs -> ${res.status} (expected 200)`);
|
|
725
|
+
else {
|
|
726
|
+
const json = tryJson(res.body);
|
|
727
|
+
if (!json || !Array.isArray(json.evidence_packs) || typeof json.total !== "number") {
|
|
728
|
+
problems.push(`/api/evidence-packs missing bounded evidence summary fields: ${res.body.slice(0, 120)}`);
|
|
729
|
+
}
|
|
730
|
+
const text = res.body.toLowerCase();
|
|
731
|
+
if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff.patch") || text.includes("verification.log")) {
|
|
732
|
+
problems.push("/api/evidence-packs leaked log/diff-shaped fields");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
} catch (err) {
|
|
736
|
+
problems.push(`/api/evidence-packs error: ${err.message}`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// /api/direct-sessions/:id/summary (safe, bounded route; missing id is still non-leaky)
|
|
740
|
+
try {
|
|
741
|
+
const res = await httpGet(`${BASE_URL}/api/direct-sessions/direct_missing_summary/summary`);
|
|
742
|
+
if (res.status !== 200) problems.push(`/api/direct-sessions/:id/summary -> ${res.status} (expected 200)`);
|
|
743
|
+
else {
|
|
744
|
+
const json = tryJson(res.body);
|
|
745
|
+
if (!json || json.session_id !== "direct_missing_summary") {
|
|
746
|
+
problems.push(`/api/direct-sessions/:id/summary missing safe summary envelope: ${res.body.slice(0, 120)}`);
|
|
747
|
+
}
|
|
748
|
+
const text = res.body.toLowerCase();
|
|
749
|
+
if (text.includes("stdout_tail") || text.includes("stderr_tail") || text.includes("diff_patch") || text.includes("verification.log")) {
|
|
750
|
+
problems.push("/api/direct-sessions/:id/summary leaked log/diff-shaped fields");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
} catch (err) {
|
|
754
|
+
problems.push(`/api/direct-sessions/:id/summary error: ${err.message}`);
|
|
755
|
+
}
|
|
756
|
+
|
|
670
757
|
// /api/logs/core
|
|
671
758
|
try {
|
|
672
759
|
const res = await httpGet(`${BASE_URL}/api/logs/core`);
|
|
@@ -1001,6 +1088,351 @@ async function testControlCenterStatusApi() {
|
|
|
1001
1088
|
}
|
|
1002
1089
|
}
|
|
1003
1090
|
|
|
1091
|
+
// ── Test 21: GET /api/workspace/repos returns valid JSON with repos array ──
|
|
1092
|
+
async function testWorkspaceReposApi() {
|
|
1093
|
+
const name = "Test 21: /api/workspace/repos returns valid JSON with repos array";
|
|
1094
|
+
try {
|
|
1095
|
+
const res = await httpGet(`${BASE_URL}/api/workspace/repos`);
|
|
1096
|
+
if (res.status !== 200) {
|
|
1097
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const json = tryJson(res.body);
|
|
1101
|
+
if (!json || !isObject(json)) {
|
|
1102
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const problems = [];
|
|
1106
|
+
if (!Array.isArray(json.repos)) problems.push("'repos' is not an array");
|
|
1107
|
+
if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
|
|
1108
|
+
problems.push("'workspace_root' is neither string nor null");
|
|
1109
|
+
}
|
|
1110
|
+
if (Array.isArray(json.repos)) {
|
|
1111
|
+
for (let i = 0; i < json.repos.length; i++) {
|
|
1112
|
+
const repo = json.repos[i];
|
|
1113
|
+
if (!isObject(repo)) {
|
|
1114
|
+
problems.push(`repos[${i}] is not an object`);
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof repo.name !== "string") problems.push(`repos[${i}].name not string`);
|
|
1118
|
+
if (typeof repo.path !== "string") problems.push(`repos[${i}].path not string`);
|
|
1119
|
+
if (typeof repo.has_package_json !== "boolean") problems.push(`repos[${i}].has_package_json not boolean`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (problems.length === 0) {
|
|
1123
|
+
record(name, true);
|
|
1124
|
+
} else {
|
|
1125
|
+
record(name, false, problems.join("; "));
|
|
1126
|
+
}
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
record(name, false, `error: ${err.message}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ── Test 22: /api/release/status no longer mentions release_prepare and has ready_state ──
|
|
1133
|
+
async function testReleaseStatusFields() {
|
|
1134
|
+
const name = "Test 22: /api/release/status has ready_state and no release_prepare text";
|
|
1135
|
+
try {
|
|
1136
|
+
const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
|
|
1137
|
+
if (res.status !== 200) {
|
|
1138
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const json = tryJson(res.body);
|
|
1142
|
+
if (!json || !isObject(json)) {
|
|
1143
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const problems = [];
|
|
1147
|
+
if (!("ready_state" in json)) problems.push("missing 'ready_state' field");
|
|
1148
|
+
if (typeof json.ready_state !== "string") problems.push("'ready_state' is not a string");
|
|
1149
|
+
if (!("package_name" in json)) problems.push("missing 'package_name' field");
|
|
1150
|
+
if (!("version_source" in json)) problems.push("missing 'version_source' field");
|
|
1151
|
+
if (!("version_consistent" in json)) problems.push("missing 'version_consistent' field");
|
|
1152
|
+
if (!Array.isArray(json.required_commands)) problems.push("'required_commands' is not an array");
|
|
1153
|
+
if (typeof json.commands_blocked_count !== "number") problems.push("'commands_blocked_count' is not a number");
|
|
1154
|
+
// The old misleading "release_prepare" text must no longer appear.
|
|
1155
|
+
if (res.body.includes("release_prepare")) {
|
|
1156
|
+
problems.push("response still contains 'release_prepare' text");
|
|
1157
|
+
}
|
|
1158
|
+
if (Array.isArray(json.required_commands)) {
|
|
1159
|
+
for (let i = 0; i < json.required_commands.length; i++) {
|
|
1160
|
+
const cmd = json.required_commands[i];
|
|
1161
|
+
if (!isObject(cmd)) {
|
|
1162
|
+
problems.push(`required_commands[${i}] is not an object`);
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof cmd.command !== "string") problems.push(`required_commands[${i}].command not string`);
|
|
1166
|
+
if (typeof cmd.allowed !== "boolean") problems.push(`required_commands[${i}].allowed not boolean`);
|
|
1167
|
+
if (!("blocked_reason" in cmd)) problems.push(`required_commands[${i}] missing 'blocked_reason'`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (problems.length === 0) {
|
|
1171
|
+
record(name, true);
|
|
1172
|
+
} else {
|
|
1173
|
+
record(name, false, problems.join("; "));
|
|
1174
|
+
}
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
record(name, false, `error: ${err.message}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// ── Test 23: Safe task endpoints return valid JSON without full content ──
|
|
1181
|
+
async function testSafeTaskEndpoints() {
|
|
1182
|
+
const name = "Test 23: safe task endpoints return valid JSON without stdout/stderr/diff";
|
|
1183
|
+
const endpoints = [
|
|
1184
|
+
"safe-result",
|
|
1185
|
+
"safe-audit",
|
|
1186
|
+
"safe-test-summary",
|
|
1187
|
+
"safe-diff-summary",
|
|
1188
|
+
];
|
|
1189
|
+
const problems = [];
|
|
1190
|
+
for (const ep of endpoints) {
|
|
1191
|
+
try {
|
|
1192
|
+
const res = await httpGet(`${BASE_URL}/api/tasks/nonexistent-smoke-task/${ep}`);
|
|
1193
|
+
if (res.status !== 200) {
|
|
1194
|
+
problems.push(`/api/tasks/:id/${ep} -> status ${res.status} (expected 200, not 500)`);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const json = tryJson(res.body);
|
|
1198
|
+
if (!json || !isObject(json)) {
|
|
1199
|
+
problems.push(`/api/tasks/:id/${ep} body is not a JSON object: ${res.body.slice(0, 120)}`);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
// Safe endpoints must NOT expose full stdout/stderr/diff content.
|
|
1203
|
+
const text = res.body.toLowerCase();
|
|
1204
|
+
if (text.includes('"stdout"') && text.includes('"stderr"')) {
|
|
1205
|
+
// Some safe endpoints may include a "stdout" key as a bounded field; the
|
|
1206
|
+
// invariant we care about is that they don't return the full raw logs.
|
|
1207
|
+
// We only flag if both stdout AND stderr appear together (log-shaped).
|
|
1208
|
+
problems.push(`/api/tasks/:id/${ep} may leak stdout+stderr shaped fields`);
|
|
1209
|
+
}
|
|
1210
|
+
if (text.includes('"diff_patch"')) {
|
|
1211
|
+
problems.push(`/api/tasks/:id/${ep} leaks 'diff_patch' field`);
|
|
1212
|
+
}
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
problems.push(`/api/tasks/:id/${ep} error: ${err.message}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (problems.length === 0) {
|
|
1218
|
+
record(name, true);
|
|
1219
|
+
} else {
|
|
1220
|
+
record(name, false, problems.join("; "));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// ── Test 24: /api/tasks?status=running returns valid filtered JSON ──
|
|
1225
|
+
async function testTasksFilter() {
|
|
1226
|
+
const name = "Test 24: /api/tasks?status=running returns valid filtered JSON";
|
|
1227
|
+
try {
|
|
1228
|
+
const res = await httpGet(`${BASE_URL}/api/tasks?status=running`);
|
|
1229
|
+
if (res.status !== 200) {
|
|
1230
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const json = tryJson(res.body);
|
|
1234
|
+
if (!json || !isObject(json)) {
|
|
1235
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const problems = [];
|
|
1239
|
+
if (!Array.isArray(json.tasks)) problems.push("'tasks' is not an array");
|
|
1240
|
+
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
1241
|
+
if (typeof json.returned !== "number") problems.push("'returned' is not a number");
|
|
1242
|
+
// Every returned task must match the filter (if any tasks exist).
|
|
1243
|
+
if (Array.isArray(json.tasks)) {
|
|
1244
|
+
for (let i = 0; i < json.tasks.length; i++) {
|
|
1245
|
+
if (json.tasks[i].status !== "running") {
|
|
1246
|
+
problems.push(`tasks[${i}].status is '${json.tasks[i].status}' (expected 'running')`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (json.tasks.length !== json.total) {
|
|
1250
|
+
problems.push(`tasks.length (${json.tasks.length}) != total (${json.total})`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (problems.length === 0) {
|
|
1254
|
+
record(name, true);
|
|
1255
|
+
} else {
|
|
1256
|
+
record(name, false, problems.join("; "));
|
|
1257
|
+
}
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
record(name, false, `error: ${err.message}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// ── Test 25: /api/tasks/stale has explanation and next_action on stale tasks ──
|
|
1264
|
+
async function testStaleTasksExplanation() {
|
|
1265
|
+
const name = "Test 25: /api/tasks/stale stale tasks have explanation and next_action";
|
|
1266
|
+
try {
|
|
1267
|
+
const res = await httpGet(`${BASE_URL}/api/tasks/stale`);
|
|
1268
|
+
if (res.status !== 200) {
|
|
1269
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const json = tryJson(res.body);
|
|
1273
|
+
if (!json || !isObject(json)) {
|
|
1274
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const problems = [];
|
|
1278
|
+
if (!Array.isArray(json.stale_tasks)) problems.push("'stale_tasks' is not an array");
|
|
1279
|
+
if (Array.isArray(json.stale_tasks) && json.stale_tasks.length > 0) {
|
|
1280
|
+
for (let i = 0; i < json.stale_tasks.length; i++) {
|
|
1281
|
+
const t = json.stale_tasks[i];
|
|
1282
|
+
if (!isObject(t)) {
|
|
1283
|
+
problems.push(`stale_tasks[${i}] is not an object`);
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
if (typeof t.explanation !== "string" || t.explanation.length === 0) {
|
|
1287
|
+
problems.push(`stale_tasks[${i}] missing 'explanation' string`);
|
|
1288
|
+
}
|
|
1289
|
+
if (typeof t.next_action !== "string" || t.next_action.length === 0) {
|
|
1290
|
+
problems.push(`stale_tasks[${i}] missing 'next_action' string`);
|
|
1291
|
+
}
|
|
1292
|
+
if (typeof t.reason_code !== "string") {
|
|
1293
|
+
problems.push(`stale_tasks[${i}] missing 'reason_code' string`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (problems.length === 0) {
|
|
1298
|
+
record(name, true);
|
|
1299
|
+
} else {
|
|
1300
|
+
record(name, false, problems.join("; "));
|
|
1301
|
+
}
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
record(name, false, `error: ${err.message}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ── Test 26: POST /api/tasks/:taskId/hide-stale token gate + valid response ──
|
|
1308
|
+
async function testHideStaleNoToken() {
|
|
1309
|
+
const name = "Test 26: POST /api/tasks/:taskId/hide-stale without token -> 403";
|
|
1310
|
+
try {
|
|
1311
|
+
const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {});
|
|
1312
|
+
if (res.status !== 403) {
|
|
1313
|
+
record(name, false, `status ${res.status} (expected 403 — token gate must fire before hide-stale runs)`);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const json = tryJson(res.body);
|
|
1317
|
+
if (!json || typeof json.error !== "string") {
|
|
1318
|
+
record(name, false, `response body missing 'error' field: ${res.body.slice(0, 200)}`);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
record(name, true);
|
|
1322
|
+
} catch (err) {
|
|
1323
|
+
record(name, false, `error: ${err.message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function testHideStaleWithToken(token) {
|
|
1328
|
+
const name = "Test 27: POST /api/tasks/:taskId/hide-stale with token returns valid JSON";
|
|
1329
|
+
try {
|
|
1330
|
+
const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {
|
|
1331
|
+
"X-PatchWarden-Control-Token": token,
|
|
1332
|
+
});
|
|
1333
|
+
if (res.status !== 200) {
|
|
1334
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const json = tryJson(res.body);
|
|
1338
|
+
if (!json || !isObject(json)) {
|
|
1339
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (json.ok !== true) {
|
|
1343
|
+
record(name, false, `expected ok=true, got: ${res.body.slice(0, 200)}`);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (json.hidden !== "nonexistent") {
|
|
1347
|
+
record(name, false, `expected hidden='nonexistent', got: ${json.hidden}`);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
record(name, true);
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
record(name, false, `error: ${err.message}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// ── Test 28: GET /api/warnings returns valid JSON ───────────────
|
|
1357
|
+
async function testWarningsApi() {
|
|
1358
|
+
const name = "Test 28: /api/warnings returns valid JSON with warnings array";
|
|
1359
|
+
try {
|
|
1360
|
+
const res = await httpGet(`${BASE_URL}/api/warnings`);
|
|
1361
|
+
if (res.status !== 200) {
|
|
1362
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const json = tryJson(res.body);
|
|
1366
|
+
if (!json || !isObject(json)) {
|
|
1367
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const problems = [];
|
|
1371
|
+
if (!Array.isArray(json.warnings)) problems.push("'warnings' is not an array");
|
|
1372
|
+
if (typeof json.total !== "number") problems.push("'total' is not a number");
|
|
1373
|
+
if (Array.isArray(json.warnings)) {
|
|
1374
|
+
for (let i = 0; i < json.warnings.length; i++) {
|
|
1375
|
+
const w = json.warnings[i];
|
|
1376
|
+
if (!isObject(w)) {
|
|
1377
|
+
problems.push(`warnings[${i}] is not an object`);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (typeof w.type !== "string") problems.push(`warnings[${i}].type is not a string`);
|
|
1381
|
+
if (typeof w.severity !== "string") problems.push(`warnings[${i}].severity is not a string`);
|
|
1382
|
+
if (typeof w.affected_tasks_count !== "number") problems.push(`warnings[${i}].affected_tasks_count is not a number`);
|
|
1383
|
+
if (typeof w.recommended_action !== "string") problems.push(`warnings[${i}].recommended_action is not a string`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (problems.length === 0) {
|
|
1387
|
+
record(name, true);
|
|
1388
|
+
} else {
|
|
1389
|
+
record(name, false, problems.join("; "));
|
|
1390
|
+
}
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
record(name, false, `error: ${err.message}`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ── Test 29: GET /api/diagnostics returns valid JSON with redaction ─
|
|
1397
|
+
async function testDiagnosticsApi() {
|
|
1398
|
+
const name = "Test 29: /api/diagnostics returns valid JSON with required fields and redaction";
|
|
1399
|
+
try {
|
|
1400
|
+
const res = await httpGet(`${BASE_URL}/api/diagnostics`);
|
|
1401
|
+
if (res.status !== 200) {
|
|
1402
|
+
record(name, false, `status ${res.status} (expected 200)`);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const json = tryJson(res.body);
|
|
1406
|
+
if (!json || !isObject(json)) {
|
|
1407
|
+
record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const problems = [];
|
|
1411
|
+
if (typeof json.server_version !== "string") problems.push("'server_version' is not a string");
|
|
1412
|
+
if (typeof json.schema_epoch !== "string") problems.push("'schema_epoch' is not a string");
|
|
1413
|
+
if (typeof json.watcher_status !== "string") problems.push("'watcher_status' is not a string");
|
|
1414
|
+
if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
|
|
1415
|
+
problems.push("'workspace_root' is neither string nor null");
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof json.direct_profile_enabled !== "boolean") problems.push("'direct_profile_enabled' is not a boolean");
|
|
1418
|
+
// Redaction: response body must not contain sensitive keywords
|
|
1419
|
+
const lower = res.body.toLowerCase();
|
|
1420
|
+
const forbidden = ["token", "password", "secret", "credential", "api_key"];
|
|
1421
|
+
for (const word of forbidden) {
|
|
1422
|
+
if (lower.includes(word)) {
|
|
1423
|
+
problems.push(`response body contains forbidden word '${word}' (redaction failure)`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (problems.length === 0) {
|
|
1427
|
+
record(name, true);
|
|
1428
|
+
} else {
|
|
1429
|
+
record(name, false, problems.join("; "));
|
|
1430
|
+
}
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
record(name, false, `error: ${err.message}`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1004
1436
|
async function main() {
|
|
1005
1437
|
// Sanity: ensure the compiled server exists before spawning.
|
|
1006
1438
|
if (!existsSync(serverPath) || !statSync(serverPath).isFile()) {
|
|
@@ -1072,6 +1504,17 @@ async function main() {
|
|
|
1072
1504
|
await testControlCenterLogApi();
|
|
1073
1505
|
await testRestartAllNoToken();
|
|
1074
1506
|
await testControlCenterStatusApi();
|
|
1507
|
+
// v1.5.1 Dashboard UI optimization coverage
|
|
1508
|
+
await testWorkspaceReposApi();
|
|
1509
|
+
await testReleaseStatusFields();
|
|
1510
|
+
await testSafeTaskEndpoints();
|
|
1511
|
+
await testTasksFilter();
|
|
1512
|
+
await testStaleTasksExplanation();
|
|
1513
|
+
await testHideStaleNoToken();
|
|
1514
|
+
if (token) await testHideStaleWithToken(token);
|
|
1515
|
+
// P1 Task 10/11 coverage
|
|
1516
|
+
await testWarningsApi();
|
|
1517
|
+
await testDiagnosticsApi();
|
|
1075
1518
|
|
|
1076
1519
|
// Use token to silence unused-var linters and confirm we actually fetched it.
|
|
1077
1520
|
if (token) {
|
|
@@ -76,7 +76,7 @@ try {
|
|
|
76
76
|
ready: false,
|
|
77
77
|
pid: null,
|
|
78
78
|
tool_profile: "chatgpt_direct",
|
|
79
|
-
tool_count:
|
|
79
|
+
tool_count: 14,
|
|
80
80
|
tools_ready: true,
|
|
81
81
|
}), "utf8");
|
|
82
82
|
writeFileSync(join(directRuntime, "tunnel-client.pid"), String(fakeTunnel.pid), "utf8");
|
|
@@ -114,7 +114,7 @@ try {
|
|
|
114
114
|
reason_code: "stale_fixture",
|
|
115
115
|
last_error: "stale failure",
|
|
116
116
|
tool_profile: "chatgpt_core",
|
|
117
|
-
tool_count:
|
|
117
|
+
tool_count: 26,
|
|
118
118
|
tools_ready: true,
|
|
119
119
|
}), "utf8");
|
|
120
120
|
healthServer = spawn(
|