flowforge-map 0.1.0__tar.gz → 0.3.0__tar.gz
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.
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/PKG-INFO +39 -3
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/README.md +35 -2
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/agent/server.py +104 -4
- flowforge_map-0.3.0/backend/flowforge/api/issues.py +150 -0
- flowforge_map-0.3.0/backend/flowforge/api/keys.py +184 -0
- flowforge_map-0.3.0/backend/flowforge/api/lock.py +265 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/scan.py +3 -0
- flowforge_map-0.3.0/backend/flowforge/cli.py +244 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/diff.py +27 -2
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/models.py +1 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/planner.py +12 -2
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/service.py +10 -2
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/home.py +14 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/main.py +10 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/security/finders.py +42 -0
- flowforge_map-0.3.0/backend/flowforge/security/fixes.py +246 -0
- flowforge_map-0.3.0/backend/flowforge/security/keystore.py +284 -0
- flowforge_map-0.3.0/backend/flowforge/security/lock.py +290 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/security/logfilter.py +12 -1
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/README.md +1 -1
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/config.py +3 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/services/llm.py +2 -2
- flowforge_map-0.3.0/frontend/dist/assets/index-BmxPFjrF.js +52 -0
- flowforge_map-0.3.0/frontend/dist/assets/index-C9vBGBLq.css +1 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/index.html +2 -2
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/pyproject.toml +4 -1
- flowforge_map-0.1.0/backend/flowforge/cli.py +0 -108
- flowforge_map-0.1.0/frontend/dist/assets/index-BGT5seIN.css +0 -1
- flowforge_map-0.1.0/frontend/dist/assets/index-BoGikST6.js +0 -52
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/.gitignore +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/LICENSE +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/agent/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/agent.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/checks.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/connectors.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/flowmap.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/runs.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/security.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/api/stats.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/checks/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/checks/models.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/checks/probes.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/catalog.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/models.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/presets.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/registry.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/runtime.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/connectors/secrets.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/example_data.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/advice.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/curly.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/index.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/order.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/reach.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flow/rewrite.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/fixture.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/flowmap/validate.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/base.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/check_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/http_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/llm_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/local_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/mcp_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/nodes/mock_node.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/facts.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/files.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/manifests.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/mcp_config.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/models.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/patterns.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/python_ast.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/scanner.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/services.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scan/urls.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/cache.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/critical_path.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/durations.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/executor.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/graph.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/scheduler/rate_limit.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/schema.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/security/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/security/findings.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/spa.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/state.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/storage.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/flowforge/templating.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/workflows/diamond_mock.json +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/backend/workflows/stripe_to_razorpay.json +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/.env.example +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/.gitignore +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/.mcp.json +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/mcp_servers/currency.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/planner.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/requirements.txt +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/services/__init__.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/services/geo.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/services/mcp_tools.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/services/weather.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/tools/packing_list.py +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/examples/trip-planner/trips/.gitkeep +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/classic.html +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- {flowforge_map-0.1.0 → flowforge_map-0.3.0}/frontend/dist/favicon.svg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flowforge-map
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Map what your app depends on (LLMs, MCP servers, APIs), check their health and security, and see what could run faster. Local and free; the scan needs no AI.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Vishwa-Thangapandiyan/flowforge-agent-orchestrator
|
|
6
6
|
Project-URL: Issues, https://github.com/Vishwa-Thangapandiyan/flowforge-agent-orchestrator/issues
|
|
@@ -19,14 +19,17 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Classifier: Topic :: System :: Monitoring
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
21
|
Requires-Dist: anthropic>=1.11.0
|
|
22
|
+
Requires-Dist: cryptography>=43
|
|
22
23
|
Requires-Dist: fastapi>=0.115
|
|
23
24
|
Requires-Dist: httpx>=0.27
|
|
24
25
|
Requires-Dist: jsonschema>=4.26.0
|
|
26
|
+
Requires-Dist: keyring>=25
|
|
25
27
|
Requires-Dist: mcp>=1.2
|
|
26
28
|
Requires-Dist: openai>=1.40
|
|
27
29
|
Requires-Dist: pydantic>=2.8
|
|
28
30
|
Requires-Dist: python-dotenv>=1.0
|
|
29
31
|
Requires-Dist: python-multipart>=0.0.32
|
|
32
|
+
Requires-Dist: segno>=1.6
|
|
30
33
|
Requires-Dist: uvicorn[standard]>=0.30
|
|
31
34
|
Description-Content-Type: text/markdown
|
|
32
35
|
|
|
@@ -53,7 +56,7 @@ What makes it different: **it knows which chain of steps is the bottleneck and s
|
|
|
53
56
|
|
|
54
57
|
## Quick start
|
|
55
58
|
|
|
56
|
-
### Install
|
|
59
|
+
### Install
|
|
57
60
|
|
|
58
61
|
```bash
|
|
59
62
|
uvx flowforge-map --example # try it: a sample app's maps in your browser, no keys, nothing sent
|
|
@@ -111,7 +114,37 @@ FlowForge is also an **MCP server**, so Claude Code, Codex or Antigravity can as
|
|
|
111
114
|
claude mcp add flowforge -- uv --directory /path/to/flowforge run flowforge mcp --repo /path/to/your/app
|
|
112
115
|
```
|
|
113
116
|
|
|
114
|
-
Your agent gets
|
|
117
|
+
Your agent gets ten tools: `get_map`, `get_flow`, `get_advice`, `check_health`, `get_security_findings`, `get_keys`, `get_fix_requests`, `request_key`, `wait_for_keys` and `what_changed`. All read only, except `request_key`, which only asks you for a key by name. Every answer is masked (key values never appear), the agent can't make FlowForge change your code, and a check starts an MCP server only after you said yes to it on the Checks page. FlowForge needs no AI of its own for any of this.
|
|
118
|
+
|
|
119
|
+
### Fix what's unsafe, with one click
|
|
120
|
+
|
|
121
|
+
The **Issues** page lists what the scan found unsafe, and every issue has one button:
|
|
122
|
+
|
|
123
|
+
| Issue | Button | What happens |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| A key written in Python, or in JS/TS that runs on Node | **Fix** | FlowForge shows the change (the key masked). On **Approve**, the key goes to your keys and the line reads it from the environment, on a new branch `flowforge/fix-…`. Your branch and files aren't touched; merge it when you're ready. |
|
|
126
|
+
| `http://`, an unpinned MCP server, a server given the whole disk, a key in `.mcp.json`, Dart or browser code | **Send to agent** | It joins your coding agent's list (`get_fix_requests`). Tell your agent "do the FlowForge fixes". |
|
|
127
|
+
|
|
128
|
+
Fix needs a git repo and the file committed. FlowForge works out the change itself, with no AI. Replace a key that was in your code with its provider anyway: older commits still hold it.
|
|
129
|
+
|
|
130
|
+
### Keys your coding agent never sees
|
|
131
|
+
|
|
132
|
+
Your agent only needs a key's **name**: the code it writes reads `os.environ["STRIPE_SECRET_KEY"]`. You give FlowForge the value once, and your app gets it when it starts:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
flowforge key add STRIPE_SECRET_KEY # typed hidden; saved in your OS keychain
|
|
136
|
+
flowforge key list # each key the code reads: saved · in .env · missing
|
|
137
|
+
flowforge key import # move a .env into FlowForge (then delete the .env)
|
|
138
|
+
flowforge run -- python app.py # your app starts with the keys in its environment
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Each command takes `--repo PATH` (default: the folder you're in). In the dashboard, the **Keys** page does the same with no commands: paste a missing key into its box, or click **Move into FlowForge** for one that's in `.env`. On the map, a 🔑 badge marks a block whose key is missing. Your agent's `get_keys` tool gets names, statuses and the command to ask you to run, never a value. With no keychain (a server, CI), keys go to an encrypted file in `~/.flowforge`.
|
|
142
|
+
|
|
143
|
+
**When your agent adds a dependency that needs a key**, it calls `request_key("STRIPE_SECRET_KEY", "for Stripe in pay.py")`. The map says "Your agent asked for a key", and the new block shows in coral, "key missing". Click it, paste the key on the Keys page, and the agent's `wait_for_keys` returns the moment it's saved. The agent only ever sees the name.
|
|
144
|
+
|
|
145
|
+
**See a key again** in the **Vault**: set a password and scan the QR with an authenticator app (Google Authenticator, Authy, 1Password), once. After that, your password, then the 6-digit code on your phone, open it for 5 minutes. It lists every key with its health: healthy, written in code, in `.env`, can't read, unused, or old. It also shows where each key lives, what uses it, and whether it's a live or test key. Each value shows for 30 seconds. Forgot the password? `flowforge lock reset --forget` drops the lock (your keys stay) and the dashboard says so.
|
|
146
|
+
|
|
147
|
+
What this does and doesn't promise: the key is never handed to your agent and isn't left in a file it reads by habit. An agent that can run commands as you could still run `flowforge run -- printenv`, read your keychain, or reset the lock (the dashboard then shows it was reset), so it isn't impossible. The 2FA stops web pages, anyone at your browser, and anything without your phone. MCP servers your coding tool starts get their environment from that tool, not from FlowForge.
|
|
115
148
|
|
|
116
149
|
### Send someone the map
|
|
117
150
|
|
|
@@ -238,6 +271,9 @@ FlowForge itself keeps keys out of its runs, events, cache, logs and the dashboa
|
|
|
238
271
|
| `POST /connectors/{id}/test` | One harmless check (MCP and local apps) |
|
|
239
272
|
| `GET /connectors/{id}/activity`, `GET /connectors/{id}/tools` | What an app has done; an MCP server's tools |
|
|
240
273
|
| `GET /security` | The scan's security findings, by level, with the fix and a prompt for each (D31) |
|
|
274
|
+
| `GET /keys`, `PUT /keys/{name}`, `DELETE /keys/{name}`, `POST /keys/{name}/import` | The keys the code reads and their status; save one (write-only), move one in from `.env`, or forget it (D36, D37) |
|
|
275
|
+
| `GET /issues`, `POST /issues/{id}/preview\|fix\|send` | Every issue with its button; the masked change; make it on a new branch; send it to your agent (D37) |
|
|
276
|
+
| `GET /lock`, `POST /lock/setup\|confirm\|password\|code\|close`, `GET /vault/keys`, `POST /keys/reveal` | The Vault: its lock (your password, then the authenticator code) and, once open, every key's health and one value at a time (D37, D38) |
|
|
241
277
|
| `GET /health/tools`, `GET /stats/savings`, `GET /meta`, `GET /presets` | Tool health, money and time, app info, ready-made apps |
|
|
242
278
|
| `GET /workflows`, `GET /workflows/{name}` | The example workflows |
|
|
243
279
|
| `GET /flow?layer=mine\|planner` | The flow map: blocks, lines, positions, critical path, your edits |
|
|
@@ -21,7 +21,7 @@ What makes it different: **it knows which chain of steps is the bottleneck and s
|
|
|
21
21
|
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
|
-
### Install
|
|
24
|
+
### Install
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
uvx flowforge-map --example # try it: a sample app's maps in your browser, no keys, nothing sent
|
|
@@ -79,7 +79,37 @@ FlowForge is also an **MCP server**, so Claude Code, Codex or Antigravity can as
|
|
|
79
79
|
claude mcp add flowforge -- uv --directory /path/to/flowforge run flowforge mcp --repo /path/to/your/app
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Your agent gets
|
|
82
|
+
Your agent gets ten tools: `get_map`, `get_flow`, `get_advice`, `check_health`, `get_security_findings`, `get_keys`, `get_fix_requests`, `request_key`, `wait_for_keys` and `what_changed`. All read only, except `request_key`, which only asks you for a key by name. Every answer is masked (key values never appear), the agent can't make FlowForge change your code, and a check starts an MCP server only after you said yes to it on the Checks page. FlowForge needs no AI of its own for any of this.
|
|
83
|
+
|
|
84
|
+
### Fix what's unsafe, with one click
|
|
85
|
+
|
|
86
|
+
The **Issues** page lists what the scan found unsafe, and every issue has one button:
|
|
87
|
+
|
|
88
|
+
| Issue | Button | What happens |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| A key written in Python, or in JS/TS that runs on Node | **Fix** | FlowForge shows the change (the key masked). On **Approve**, the key goes to your keys and the line reads it from the environment, on a new branch `flowforge/fix-…`. Your branch and files aren't touched; merge it when you're ready. |
|
|
91
|
+
| `http://`, an unpinned MCP server, a server given the whole disk, a key in `.mcp.json`, Dart or browser code | **Send to agent** | It joins your coding agent's list (`get_fix_requests`). Tell your agent "do the FlowForge fixes". |
|
|
92
|
+
|
|
93
|
+
Fix needs a git repo and the file committed. FlowForge works out the change itself, with no AI. Replace a key that was in your code with its provider anyway: older commits still hold it.
|
|
94
|
+
|
|
95
|
+
### Keys your coding agent never sees
|
|
96
|
+
|
|
97
|
+
Your agent only needs a key's **name**: the code it writes reads `os.environ["STRIPE_SECRET_KEY"]`. You give FlowForge the value once, and your app gets it when it starts:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
flowforge key add STRIPE_SECRET_KEY # typed hidden; saved in your OS keychain
|
|
101
|
+
flowforge key list # each key the code reads: saved · in .env · missing
|
|
102
|
+
flowforge key import # move a .env into FlowForge (then delete the .env)
|
|
103
|
+
flowforge run -- python app.py # your app starts with the keys in its environment
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Each command takes `--repo PATH` (default: the folder you're in). In the dashboard, the **Keys** page does the same with no commands: paste a missing key into its box, or click **Move into FlowForge** for one that's in `.env`. On the map, a 🔑 badge marks a block whose key is missing. Your agent's `get_keys` tool gets names, statuses and the command to ask you to run, never a value. With no keychain (a server, CI), keys go to an encrypted file in `~/.flowforge`.
|
|
107
|
+
|
|
108
|
+
**When your agent adds a dependency that needs a key**, it calls `request_key("STRIPE_SECRET_KEY", "for Stripe in pay.py")`. The map says "Your agent asked for a key", and the new block shows in coral, "key missing". Click it, paste the key on the Keys page, and the agent's `wait_for_keys` returns the moment it's saved. The agent only ever sees the name.
|
|
109
|
+
|
|
110
|
+
**See a key again** in the **Vault**: set a password and scan the QR with an authenticator app (Google Authenticator, Authy, 1Password), once. After that, your password, then the 6-digit code on your phone, open it for 5 minutes. It lists every key with its health: healthy, written in code, in `.env`, can't read, unused, or old. It also shows where each key lives, what uses it, and whether it's a live or test key. Each value shows for 30 seconds. Forgot the password? `flowforge lock reset --forget` drops the lock (your keys stay) and the dashboard says so.
|
|
111
|
+
|
|
112
|
+
What this does and doesn't promise: the key is never handed to your agent and isn't left in a file it reads by habit. An agent that can run commands as you could still run `flowforge run -- printenv`, read your keychain, or reset the lock (the dashboard then shows it was reset), so it isn't impossible. The 2FA stops web pages, anyone at your browser, and anything without your phone. MCP servers your coding tool starts get their environment from that tool, not from FlowForge.
|
|
83
113
|
|
|
84
114
|
### Send someone the map
|
|
85
115
|
|
|
@@ -206,6 +236,9 @@ FlowForge itself keeps keys out of its runs, events, cache, logs and the dashboa
|
|
|
206
236
|
| `POST /connectors/{id}/test` | One harmless check (MCP and local apps) |
|
|
207
237
|
| `GET /connectors/{id}/activity`, `GET /connectors/{id}/tools` | What an app has done; an MCP server's tools |
|
|
208
238
|
| `GET /security` | The scan's security findings, by level, with the fix and a prompt for each (D31) |
|
|
239
|
+
| `GET /keys`, `PUT /keys/{name}`, `DELETE /keys/{name}`, `POST /keys/{name}/import` | The keys the code reads and their status; save one (write-only), move one in from `.env`, or forget it (D36, D37) |
|
|
240
|
+
| `GET /issues`, `POST /issues/{id}/preview\|fix\|send` | Every issue with its button; the masked change; make it on a new branch; send it to your agent (D37) |
|
|
241
|
+
| `GET /lock`, `POST /lock/setup\|confirm\|password\|code\|close`, `GET /vault/keys`, `POST /keys/reveal` | The Vault: its lock (your password, then the authenticator code) and, once open, every key's health and one value at a time (D37, D38) |
|
|
209
242
|
| `GET /health/tools`, `GET /stats/savings`, `GET /meta`, `GET /presets` | Tool health, money and time, app info, ready-made apps |
|
|
210
243
|
| `GET /workflows`, `GET /workflows/{name}` | The example workflows |
|
|
211
244
|
| `GET /flow?layer=mine\|planner` | The flow map: blocks, lines, positions, critical path, your edits |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""FlowForge as an MCP server (D28, D29):
|
|
1
|
+
"""FlowForge as an MCP server (D28, D29, D36): read-only tools a coding agent can ask about the repo it works in.
|
|
2
2
|
|
|
3
3
|
The map and the flow are read fresh from the repo on every call (plain code, nothing run, no LLM: D20). The
|
|
4
4
|
dashboard's database supplies what only it knows, when it maps the same repo: measured latencies, past checks,
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
+
import time
|
|
12
13
|
import uuid
|
|
13
14
|
from collections import Counter
|
|
14
15
|
from pathlib import Path
|
|
@@ -19,6 +20,8 @@ from mcp.server.mcpserver import MCPServer
|
|
|
19
20
|
from mcp.types import ToolAnnotations
|
|
20
21
|
|
|
21
22
|
from flowforge.api.checks import CHECKABLE, check_workflow, command_text, confirmed_commands, target_for
|
|
23
|
+
from flowforge.api.issues import FIX_REQUESTS_SETTING, fix_requests
|
|
24
|
+
from flowforge.api.keys import add_key_request, clear_key_requests
|
|
22
25
|
from flowforge.api.scan import REPO_SETTING
|
|
23
26
|
from flowforge.checks import starts_process
|
|
24
27
|
from flowforge.connectors.registry import Registry
|
|
@@ -28,6 +31,7 @@ from flowforge.flowmap.validate import code_writes, critical
|
|
|
28
31
|
from flowforge.nodes.check_node import CheckNode
|
|
29
32
|
from flowforge.scan import ScanResult, scan
|
|
30
33
|
from flowforge.scheduler.executor import StepState, run_workflow
|
|
34
|
+
from flowforge.security.keystore import NAME, KeyStore, report
|
|
31
35
|
from flowforge.security.logfilter import Redactor, redact_code, secret_values_for
|
|
32
36
|
from flowforge.storage import Storage, now_iso
|
|
33
37
|
|
|
@@ -42,10 +46,12 @@ INSTRUCTIONS = (
|
|
|
42
46
|
"each with the file:line it comes from, checks their health, and shows the order the code calls them, the "
|
|
43
47
|
"critical path and which calls could run together, and what is unsafe (keys in code, plain http, risky MCP "
|
|
44
48
|
"servers). It reads code without running it and sends nothing to any "
|
|
45
|
-
"LLM. Answers are masked: key values never appear. It
|
|
46
|
-
"what_changed to confirm."
|
|
49
|
+
"LLM. Answers are masked: key values never appear. It doesn't edit code for you: you make the change, "
|
|
50
|
+
"then call what_changed to confirm. When the user says \"do the FlowForge fixes\", call get_fix_requests."
|
|
47
51
|
)
|
|
48
52
|
READ = ToolAnnotations(read_only_hint=True, open_world_hint=False)
|
|
53
|
+
ASK = ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=True, open_world_hint=False)
|
|
54
|
+
MAX_WAIT_S = 110
|
|
49
55
|
CHECKS = ToolAnnotations(read_only_hint=True, open_world_hint=True) # makes requests and may start servers
|
|
50
56
|
|
|
51
57
|
|
|
@@ -160,6 +166,16 @@ def headline(tool: str, data: dict[str, Any]) -> str | None:
|
|
|
160
166
|
if tool == "get_security_findings":
|
|
161
167
|
counts = data["summary"]
|
|
162
168
|
return " · ".join(f"{counts[k]} {k}" for k in ("high", "medium", "low") if counts.get(k)) or "Nothing found"
|
|
169
|
+
if tool == "get_keys":
|
|
170
|
+
counts = data["summary"]
|
|
171
|
+
parts = [f"{counts[k]} {w}" for k, w in (("missing", "missing"), ("env_file", "in .env")) if counts.get(k)]
|
|
172
|
+
return " · ".join(parts) or ("All saved" if data["keys"] else "No keys read")
|
|
173
|
+
if tool == "request_key":
|
|
174
|
+
return f"Asked for {data['name']}"
|
|
175
|
+
if tool == "wait_for_keys":
|
|
176
|
+
return "All saved" if data["done"] else f"{_plural(len(data['waiting']), 'key')} still missing"
|
|
177
|
+
if tool == "get_fix_requests":
|
|
178
|
+
return _plural(len(data["requests"]), "fix", "fixes") + " sent to you" if data["requests"] else "Nothing sent"
|
|
163
179
|
if tool == "what_changed":
|
|
164
180
|
if not data["saved"]:
|
|
165
181
|
return "No saved map yet"
|
|
@@ -181,6 +197,29 @@ def _security_data(result: ScanResult) -> dict[str, Any]:
|
|
|
181
197
|
"note": "Catches known key formats and these patterns, not everything. Values are never shown."}
|
|
182
198
|
|
|
183
199
|
|
|
200
|
+
def _keys_data(root: Path, result: ScanResult) -> dict[str, Any]:
|
|
201
|
+
"""Names and statuses only: this server, which the agent starts, never reads a value (D36)."""
|
|
202
|
+
found = report(KeyStore(root), result, verify=False)
|
|
203
|
+
return {"keys": found, "summary": dict(Counter(k["status"] for k in found if k["status"] != "saved")),
|
|
204
|
+
"run": "flowforge run -- <the command that starts the app>",
|
|
205
|
+
"note": "saved means FlowForge has it on record; this server never reads a value, so the user's "
|
|
206
|
+
"`flowforge key list` is what confirms it can be read"}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _fix_requests_data(r: _Repo) -> dict[str, Any]:
|
|
210
|
+
"""The user's queue (D37), without the issues the code no longer has. Only when the dashboard maps this repo."""
|
|
211
|
+
if not r.shared:
|
|
212
|
+
return {"requests": [], "message": "The dashboard doesn't map this repo, so nothing was sent from it."}
|
|
213
|
+
queue = fix_requests(r.storage)
|
|
214
|
+
here = str(r.root)
|
|
215
|
+
now = {f.id for f in r.scan().findings}
|
|
216
|
+
done = [q for q in queue if q["repo"] == here and q["id"] not in now] # the code no longer has it
|
|
217
|
+
if done:
|
|
218
|
+
r.storage.set_setting(FIX_REQUESTS_SETTING, json.dumps([q for q in queue if q not in done]))
|
|
219
|
+
live = [q for q in queue if q["repo"] == here and q["id"] in now]
|
|
220
|
+
return {"requests": [{k: q.get(k) for k in ("id", "kind", "title", "ref", "fix", "prompt")} for q in live]}
|
|
221
|
+
|
|
222
|
+
|
|
184
223
|
def _flow_data(result: Any) -> dict[str, Any]:
|
|
185
224
|
if result.flow is None:
|
|
186
225
|
return {"available": False, "message": result.message}
|
|
@@ -218,7 +257,8 @@ def preview(repo: Path, storage: Storage) -> dict[str, Any]:
|
|
|
218
257
|
tone = "bad" if "down" in checked else "warn" if "warn" in checked else "ok" if checked else "plain"
|
|
219
258
|
read = r.flow()
|
|
220
259
|
flow, advice = _flow_data(read), _advice_data(read)
|
|
221
|
-
|
|
260
|
+
scanned = read.scan or r.scan()
|
|
261
|
+
security, keys = _security_data(scanned), _keys_data(r.root, scanned)
|
|
222
262
|
worst = next((lv for lv in ("high", "medium", "low") if security["summary"].get(lv)), None)
|
|
223
263
|
cards = [
|
|
224
264
|
{"tool": "get_map", "headline": headline("get_map", deps), "sub": kinds, "tone": "plain"},
|
|
@@ -228,12 +268,16 @@ def preview(repo: Path, storage: Storage) -> dict[str, Any]:
|
|
|
228
268
|
{"tool": "get_security_findings", "headline": headline("get_security_findings", security),
|
|
229
269
|
"sub": "keys in code, plain http, risky MCP servers",
|
|
230
270
|
"tone": {"high": "bad", "medium": "warn"}.get(worst or "", "ok")},
|
|
271
|
+
{"tool": "get_keys", "headline": headline("get_keys", keys),
|
|
272
|
+
"sub": "names only: the user types each key, hidden", "tone": "warn" if keys["summary"] else "ok"},
|
|
231
273
|
{"tool": "get_flow", "headline": headline("get_flow", flow) if flow["available"] else None,
|
|
232
274
|
"sub": "as written → could be" if flow["available"] else flow["message"], "tone": "plain"},
|
|
233
275
|
{"tool": "get_advice", "headline": (_plural(len(advice["advice"]), "way") + " to be faster")
|
|
234
276
|
if advice["available"] else None,
|
|
235
277
|
"sub": f"saves ~{_seconds(advice['saved_ms'])}, the change written out" if advice["available"] else None,
|
|
236
278
|
"tone": "warn" if advice["available"] and advice["advice"] else "plain"},
|
|
279
|
+
{"tool": "get_fix_requests", "headline": headline("get_fix_requests", _fix_requests_data(r)),
|
|
280
|
+
"sub": "issues you send from the Issues page", "tone": "plain"},
|
|
237
281
|
{"tool": "what_changed", "headline": None, "sub": "After your agent edits: what it added or removed",
|
|
238
282
|
"tone": "plain"},
|
|
239
283
|
]
|
|
@@ -312,6 +356,62 @@ def build_server(repo: Path, storage: Storage, *, transport: httpx.AsyncBaseTran
|
|
|
312
356
|
shown. Catches known key formats and these patterns, not everything."""
|
|
313
357
|
return r.answer("get_security_findings", _security_data(r.scan()))
|
|
314
358
|
|
|
359
|
+
@server.tool(annotations=READ)
|
|
360
|
+
def get_keys() -> dict[str, Any]:
|
|
361
|
+
"""The keys the repo's code reads from its environment (names that look like keys), each with where it is
|
|
362
|
+
read, the dependencies that use it, and its status: saved in FlowForge, only in .env, or missing. You never
|
|
363
|
+
get a value and never need one: write code that reads the name from the environment, then ask the user to
|
|
364
|
+
run the `command` given (they type the key, hidden). Their app gets the keys with `flowforge run -- <app>`."""
|
|
365
|
+
return r.answer("get_keys", _keys_data(r.root, r.scan()))
|
|
366
|
+
|
|
367
|
+
@server.tool(annotations=ASK)
|
|
368
|
+
def request_key(name: str, why: str) -> dict[str, Any]:
|
|
369
|
+
"""Ask the user for a key your change needs (D38), e.g. after adding Stripe: name="STRIPE_SECRET_KEY",
|
|
370
|
+
why="for Stripe in pay.py". It shows first on FlowForge's Keys page. You never get or send the value: write
|
|
371
|
+
code that reads the name from the environment, tell the user to paste the key on the Keys page, then call
|
|
372
|
+
wait_for_keys. Their app gets it with `flowforge run`."""
|
|
373
|
+
if not NAME.fullmatch(name):
|
|
374
|
+
raise ValueError("name must be an env var name: letters, digits and _, not starting with a digit")
|
|
375
|
+
add_key_request(r.storage, r.root, name, why.strip() or "asked by your coding agent")
|
|
376
|
+
saved = KeyStore(r.root).status([name], verify=False)[name] == "saved"
|
|
377
|
+
if saved:
|
|
378
|
+
clear_key_requests(r.storage, r.root, [name])
|
|
379
|
+
where = "" if r.shared else " (open the dashboard on this repo: `flowforge --repo " + str(r.root) + "`)"
|
|
380
|
+
return r.answer("request_key", {
|
|
381
|
+
"name": name, "status": "saved" if saved else "requested",
|
|
382
|
+
"message": f"{name} is saved already: use it." if saved else
|
|
383
|
+
f"Ask the user to paste {name} on FlowForge's Keys page{where}; it's at the top. Then call wait_for_keys."})
|
|
384
|
+
|
|
385
|
+
@server.tool(annotations=READ)
|
|
386
|
+
async def wait_for_keys(names: list[str], seconds: int = 60) -> dict[str, Any]:
|
|
387
|
+
"""Wait until the user has saved these keys on the Keys page (D38): returns as soon as all are saved, or
|
|
388
|
+
after `seconds` (at most 110; call again to keep waiting). Reads only which names are saved, never a
|
|
389
|
+
value."""
|
|
390
|
+
import asyncio
|
|
391
|
+
|
|
392
|
+
wanted = [n for n in dict.fromkeys(names) if NAME.fullmatch(n)]
|
|
393
|
+
deadline = time.monotonic() + max(1, min(MAX_WAIT_S, int(seconds)))
|
|
394
|
+
store = KeyStore(r.root)
|
|
395
|
+
while True:
|
|
396
|
+
status = store.status(wanted, verify=False)
|
|
397
|
+
saved = [n for n in wanted if status[n] == "saved"]
|
|
398
|
+
if len(saved) == len(wanted) or time.monotonic() >= deadline:
|
|
399
|
+
break
|
|
400
|
+
await asyncio.sleep(1)
|
|
401
|
+
clear_key_requests(r.storage, r.root, saved)
|
|
402
|
+
waiting = [n for n in wanted if n not in saved]
|
|
403
|
+
return r.answer("wait_for_keys", {
|
|
404
|
+
"saved": saved, "waiting": waiting, "done": not waiting,
|
|
405
|
+
"message": "All saved: run the app with `flowforge run -- <its command>`." if not waiting else
|
|
406
|
+
f"Still waiting for {', '.join(waiting)}: call wait_for_keys again, or remind the user."})
|
|
407
|
+
|
|
408
|
+
@server.tool(annotations=READ)
|
|
409
|
+
def get_fix_requests() -> dict[str, Any]:
|
|
410
|
+
"""The issues the user sent you from FlowForge's Issues page ("do the FlowForge fixes"): each with its
|
|
411
|
+
file:line, what is wrong, how to fix it and the whole change described (masked: no key value). An issue leaves
|
|
412
|
+
the list once the code no longer has it, so call what_changed after your edit to confirm."""
|
|
413
|
+
return r.answer("get_fix_requests", _fix_requests_data(r))
|
|
414
|
+
|
|
315
415
|
@server.tool(annotations=READ)
|
|
316
416
|
def what_changed() -> dict[str, Any]:
|
|
317
417
|
"""After an edit: the dependencies added or removed since the map saved in the dashboard, and the flow's
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""The Issues page (D37): every security finding with the button it gets. Fix (FlowForge changes the code on a new
|
|
2
|
+
branch) or Send to agent (queued for the coding agent's `get_fix_requests`).
|
|
3
|
+
|
|
4
|
+
GET /issues {repo, example, summary: {high, medium, low}, issues: [{...finding, action, state, branch}]}
|
|
5
|
+
(read from the code each time)
|
|
6
|
+
POST /issues/{id}/preview the change Fix will make, masked, and whether it can be made now
|
|
7
|
+
POST /issues/{id}/fix make it: a new branch, the key saved → {id, branch, commit, file, name}
|
|
8
|
+
POST /issues/{id}/send queue it for the agent → {id, state: "sent"}
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
20
|
+
|
|
21
|
+
from flowforge.api.keys import from_this_machine, learn_saved_keys
|
|
22
|
+
from flowforge.api.scan import repo_path
|
|
23
|
+
from flowforge.scan import scan as scan_repo
|
|
24
|
+
from flowforge.scan.models import Finding
|
|
25
|
+
from flowforge.security.fixes import CantFix, apply_fix, fixable, preview
|
|
26
|
+
from flowforge.state import AppState, app_state
|
|
27
|
+
from flowforge.storage import Storage, now_iso
|
|
28
|
+
|
|
29
|
+
router = APIRouter()
|
|
30
|
+
|
|
31
|
+
FIX_REQUESTS_SETTING = "fix_requests" # [{repo, id, kind, title, ref, fix, prompt, sent_at}]: what the agent is asked
|
|
32
|
+
FIXES_SETTING = "fixes_made" # [{repo, id, branch, commit, at}]: fixes made on a branch, not yet merged
|
|
33
|
+
# Issue ids repeat across repos (kind, path, line), so every entry names its folder and only counts there (Codex review)
|
|
34
|
+
EXAMPLE_WHY = "Example data: FlowForge shows the change but makes none. Map your own repo to fix it."
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _load(storage: Storage, key: str, empty: Any) -> Any:
|
|
38
|
+
try:
|
|
39
|
+
return json.loads(storage.setting(key) or "null") or empty
|
|
40
|
+
except ValueError:
|
|
41
|
+
return empty
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _entries(storage: Storage, key: str) -> list[dict[str, Any]]:
|
|
45
|
+
"""A list setting's entries that name their folder (older ones didn't, and are dropped)."""
|
|
46
|
+
found = _load(storage, key, [])
|
|
47
|
+
return [e for e in found if isinstance(e, dict) and e.get("repo")] if isinstance(found, list) else []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fix_requests(storage: Storage, repo: Path | None = None) -> list[dict[str, Any]]:
|
|
51
|
+
"""The queue, or only `repo`'s part of it."""
|
|
52
|
+
queue = _entries(storage, FIX_REQUESTS_SETTING)
|
|
53
|
+
return queue if repo is None else [r for r in queue if r["repo"] == str(repo.resolve())]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _fixes(storage: Storage, repo: Path) -> dict[str, dict[str, Any]]:
|
|
57
|
+
"""The fixes made in `repo`, by issue id."""
|
|
58
|
+
return {m["id"]: m for m in _entries(storage, FIXES_SETTING) if m["repo"] == str(repo.resolve())}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _branch_exists(repo: Path, branch: str) -> bool:
|
|
62
|
+
done = subprocess.run(["git", "show-ref", "--verify", "--quiet", f"refs/heads/{branch}"], cwd=repo,
|
|
63
|
+
capture_output=True)
|
|
64
|
+
return done.returncode == 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _repo(s: AppState) -> Path:
|
|
68
|
+
repo = repo_path(s)
|
|
69
|
+
if repo is None or not repo.is_dir():
|
|
70
|
+
raise HTTPException(409, "Map a folder first.")
|
|
71
|
+
return repo
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _finding(repo: Path, issue_id: str) -> Finding:
|
|
75
|
+
"""The issue as the code is now (read again, so a fix starts from the current file)."""
|
|
76
|
+
found = next((f for f in scan_repo(repo).findings if f.id == issue_id), None)
|
|
77
|
+
if found is None:
|
|
78
|
+
raise HTTPException(404, "That issue isn't in the code any more. Re-scan.")
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/issues")
|
|
83
|
+
def issues() -> dict[str, Any]:
|
|
84
|
+
s = app_state()
|
|
85
|
+
repo = repo_path(s)
|
|
86
|
+
if repo is None or not repo.is_dir():
|
|
87
|
+
return {"repo": None, "example": s.example, "summary": {}, "issues": []}
|
|
88
|
+
result = scan_repo(repo) # the code as it is now: a merged fix or an agent's edit shows at once
|
|
89
|
+
sent = {r["id"] for r in fix_requests(s.storage, repo)}
|
|
90
|
+
made = _fixes(s.storage, repo)
|
|
91
|
+
out = []
|
|
92
|
+
for f in result.findings:
|
|
93
|
+
branch = made.get(f.id, {}).get("branch")
|
|
94
|
+
if branch and not _branch_exists(repo, branch):
|
|
95
|
+
branch = None # the branch was deleted: the issue is open again
|
|
96
|
+
state = "fixed" if branch else "sent" if f.id in sent else "open"
|
|
97
|
+
out.append({**f.model_dump(mode="json"), "action": "fix" if fixable(repo, f) else "agent",
|
|
98
|
+
"state": state, "branch": branch})
|
|
99
|
+
return {"repo": repo.name, "example": s.example, "summary": dict(Counter(f.level for f in result.findings)),
|
|
100
|
+
"issues": out}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@router.post("/issues/{issue_id}/preview")
|
|
104
|
+
def preview_fix(issue_id: str) -> dict[str, Any]:
|
|
105
|
+
s = app_state()
|
|
106
|
+
repo = _repo(s)
|
|
107
|
+
try:
|
|
108
|
+
shown = preview(repo, _finding(repo, issue_id))
|
|
109
|
+
except CantFix as e:
|
|
110
|
+
raise HTTPException(409, str(e)) from None
|
|
111
|
+
if s.example:
|
|
112
|
+
shown.update(can_apply=False, why_not=EXAMPLE_WHY)
|
|
113
|
+
return shown
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@router.post("/issues/{issue_id}/fix", dependencies=[Depends(from_this_machine)])
|
|
117
|
+
def fix(issue_id: str) -> dict[str, Any]:
|
|
118
|
+
s = app_state()
|
|
119
|
+
repo = _repo(s)
|
|
120
|
+
if s.example:
|
|
121
|
+
raise HTTPException(409, EXAMPLE_WHY)
|
|
122
|
+
made = _fixes(s.storage, repo)
|
|
123
|
+
if made.get(issue_id) and _branch_exists(repo, made[issue_id]["branch"]):
|
|
124
|
+
raise HTTPException(409, f"Already fixed on {made[issue_id]['branch']}: merge that branch.")
|
|
125
|
+
found = _finding(repo, issue_id)
|
|
126
|
+
if not fixable(repo, found):
|
|
127
|
+
raise HTTPException(409, "FlowForge doesn't fix this one itself: send it to your agent.")
|
|
128
|
+
try:
|
|
129
|
+
done = apply_fix(repo, found)
|
|
130
|
+
except CantFix as e:
|
|
131
|
+
raise HTTPException(409, str(e)) from None
|
|
132
|
+
learn_saved_keys(s, repo)
|
|
133
|
+
here = str(repo.resolve())
|
|
134
|
+
kept = [m for m in _entries(s.storage, FIXES_SETTING) if (m["repo"], m.get("id")) != (here, issue_id)]
|
|
135
|
+
kept.append({"repo": here, "id": issue_id, "branch": done["branch"], "commit": done["commit"], "at": now_iso()})
|
|
136
|
+
s.storage.set_setting(FIXES_SETTING, json.dumps(kept))
|
|
137
|
+
return done
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@router.post("/issues/{issue_id}/send", dependencies=[Depends(from_this_machine)])
|
|
141
|
+
def send(issue_id: str) -> dict[str, Any]:
|
|
142
|
+
s = app_state()
|
|
143
|
+
repo = _repo(s)
|
|
144
|
+
found = _finding(repo, issue_id)
|
|
145
|
+
here = str(repo.resolve())
|
|
146
|
+
queue = [r for r in fix_requests(s.storage) if (r["repo"], r["id"]) != (here, issue_id)]
|
|
147
|
+
queue.append({"repo": here, "id": found.id, "kind": found.kind, "title": found.title, "ref": found.ref,
|
|
148
|
+
"fix": found.fix, "prompt": found.prompt, "sent_at": now_iso()})
|
|
149
|
+
s.storage.set_setting(FIX_REQUESTS_SETTING, json.dumps(queue))
|
|
150
|
+
return {"id": issue_id, "state": "sent"}
|