mirror of
https://github.com/langgenius/dify.git
synced 2026-11-14 07:11:06 +08:00
64b2c5e5c8ebf0e054e01acb1c27236b45c4c4df
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
59cc7043a3 |
fix(dify-builder): a rejected edit tells the next attempt what the engine said
At edit.plan_approval the gate re-reads an UNCHANGED draft, so edit_rules and graph are byte-identical on every re-approval, and _change_not_applied stored nothing about WHY the port refused the write. The live F4 user approved the same plan three times and got a byte-identical card each time. Add DifyBuilderContext.last_edit_rejection: the raw text of the DraftWouldNotStartError/ValueError the port raised, capped at 2000 chars, stored by _change_not_applied and passed into build_edit_intents, which quotes it into the user prompt as a "PREVIOUS ATTEMPT REJECTED BY THE ENGINE" block. It is engine-sourced by construction -- a PreflightError's every message is built by re-validating a credentials.redact_node_config copy of the node, and graph_ops' ValueErrors carry only paths, ids and type names -- so no model prose enters. Nothing branches on it. Cleared on a successful write, on a new edit goal, and by the recovery reset. Close the silent false success that left, rather than only recording it. new_preflight_problems now takes the ids whose NODE DATA the batch wrote and gives them no exemption at all: a node this batch wrote must be startable, not merely no worse than it was. Keyed on locations alone, a repair that swaps one bad value for another at the same field path -- or fixes one of three missing fields -- produced nothing new, was written, and the card said "Applied the changes" over a draft that dies at Graph.init. Since a repair's culprit is by definition already invalid, that was Fix's main line. Every OTHER node keeps the location-keyed exemption, so a defect the user left on the canvas still cannot veto an unrelated change. That set is deliberately narrower than a change set, on both sides (graph_ops .DryRun gains the field filter_applicable already computed and discarded; apply_repair keeps a separate written_nodes beside changed_nodes). connect is excluded, because apply_connect reports both endpoints and an edge cannot change either one's validate_node_config verdict -- counting them would let merely wiring an already-broken node veto the batch. The heal is excluded too, because heal_nodes_for_preflight scans the whole graph and a node it merely normalized is not one this batch is answerable for. Also, from the reviews of preflight.py: route an intent's config/value through redact_node_config before a structural rejection line inlines it; key a non-ValidationError crash on its truncated message as well as its class, and keep that message out of the text the model is shown; and correct the _CRASH_LOCATION comment -- verified against graphon, the rule is a config key present AND type absent-or-invalid. Finally, stop the docstrings overclaiming: redaction covers authorization, headers and params, and a secret typed into a request body value is NOT covered and reaches the stored text. Widening the profile is the wrong fix -- it shares the sentinel guard that refuses any write carrying REDACTED, so it would make http-request bodies uneditable. Recorded in credentials.py as a follow-up needing a separate persistence-only profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2458bffd23 |
fix(dify-builder): the edit agent sees the nodes it is asked to change
The Edit prompt showed each node as title + type + handles and never its `data`, so an LLM asked to add one if-else branch had nothing to copy from: it regenerated the whole `cases` array, silently turned `= 60` into `>= 60` and dropped `id` and `varType`. `diff_graphs` reported the lot as one line, "cases updated". Triage edit-branch-failure-2026-09-22 cause (c). `_graph_context` now inlines the JSON `data` of the nodes impact analysis named, plus their direct neighbours -- and one hop further past a BRANCH target's successors, because a branch's arms reconverge. In the live failure the if-else fed two template-transforms that fed a variable-aggregator, and that aggregator's `variables` is what says whether the new arm reaches the End node at all; at one hop it rendered as a bare one-liner the model could not extend, which is cause (d) and ends in "All checks passed" on empty output. The extra hop is taken only past a branch, so a long linear chain does not drag the whole graph into the prompt. Every other node keeps today's one-line form. Keys still sitting at their type's default are subtracted, with `node_defaults` as the sole source of what "default" means: without that an http-request node's two dozen method/auth/timeout/retry keys crowd the field the edit is actually about out of the prompt. A config over the 1500-character budget sheds whole trailing keys and then names them, rather than cutting the JSON mid-string where a purpose field that happened to serialize last would vanish without a trace. That subtraction is also what would have un-hidden every credential in the node: a live bearer token never equals the `no-auth` / `""` default, and Edit renders the target AND its neighbours, so the exposure would be wider than Fix's one culprit node. `credentials.redact_node_config` therefore withholds the `authorization` config and every non-empty `headers` / `params` value behind an exact sentinel, keeping the KEYS visible so the model can still reason about which header exists. Every non-empty value, not the suspicious-looking ones: deciding by name means a denylist, and a denylist misses `X-Company-Token`. Fix had the same leak and it is closed here too, not scheduled: a failing http-request node is one of the likeliest culprits there is, and `fix._culprit_config` inlined its whole `data` raw. Redaction alone would be worse than the leak -- the model reads the placeholder, rewrites `headers` around it and destroys a working credential -- so `validate_intent_args` refuses any `value` or `config` carrying the sentinel. That is the one chokepoint both the `filter_applicable` dry run and `apply_repair`'s write loop pass every intent through, so the write is rejected with a reason the existing corrective re-prompt can read and the stored secret stands. The refusal is keyed on the exact generated string, never on model prose. It also covers `create_node` / `insert_between`, which carry node data by the same route. A rewrite that merely OMITS the credential line still passes, and the comment beside the guard records why that is accepted: it fails loudly at the next verify run, where the leak closed here was silent. `build_edit_intents` grows two keyword-only, defaulted parameters: `edit_target_node_ids`, threaded from `fc.edit_target_node_ids` by `handle_plan_approval` and used here, and `last_edit_rejection`, declared but not yet read so the task that feeds a refused write back into the prompt does not have to change this method's arity a second time. Both default to empty, so an agent that passes neither gets today's behaviour. `edit_target_node_ids` is typed `Sequence[str]` rather than `list[str]`: mypy rejects `list[str] = ()` as an incompatible default, and widening the annotation keeps the `()` default and the keyword-only shape intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |