fix(#7249): repair malformed pi edit tool arguments - #7250
Conversation
pi 0.85.0 rejects two edit argument shapes models send in practice: edits as a JSON string with raw control characters (pi#8521) and edits items as JSON strings (pi#8962). Each fails with "edits.0: must be object", and the retries inflate run cost. Add a fullsend-embedded pi extension that wraps pi's own edit tool and repairs only those shapes in prepareArguments, using pi's parseJsonWithRepair. Security hooks see the repaired arguments. The extension loads only when the agent's tools include edit, because --no-builtin-tools does not filter extension tools. Sub-agents get it through a separate editRepairExtension manifest field for the same reason. It carries the same sha256 launch guard (exit 93) and child digest re-check as the other embedded extensions. Remove it once the pinned pi fixes both shapes; the check is noted next to ARG PI_VERSION in the sandbox Containerfile. Assisted-by: Claude Signed-off-by: Wayne Sun <gsun@redhat.com>
PR Summary by QodoRepair malformed pi edit arguments with a guarded extension
AI Description
Diagram
High-Level Assessment
Files changed (17)
|
|
🤖 Finished Review · ✅ Success · Started 9:36 PM UTC · Completed 9:55 PM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $9.28 |
Code Review by Qodo
1.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
ReviewFindingsMedium
Previous runReviewFindingsMedium
Fix verification: No new correctness, security, style, or documentation issues were found across the full diff. The edit-repair extension's trust boundary was independently re-verified this round: the tool-list gate ( Previous run (2)ReviewFindingsMedium
Low
Fix verification: The two prior low-severity comment-formatting findings ( Previous run (3)ReviewFindingsMedium
Low
Next steps:
|
|
🤖 Finished Fix · ✅ Success · Started 9:57 PM UTC · Completed 10:06 PM UTC Commit: Runtime: claude · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $0.91 |
Reflow two doc-comment blocks left overlong after the edit-repair additions (internal/pluginformat/pi.go's PiReservedExtensionNames, internal/runtime/pi_bootstrap.go's piAgentExtensionDigests) to match the ~72-76 col wrap of the surrounding lines, and add the missing hooks-on/edit-off case to TestPiAgentExtensionDigests. Not addressed by code change (see structured output for reasoning): - protected-path note on images/sandbox/Containerfile — the comment change there is intentional and requires human approval per policy, not a defect to fix. - process-attestation on the PR body checklist — editing the PR description is a PR mutation outside this agent's scope. golangci-lint was unavailable in this sandbox (network-restricted `go install` timed out); go vet ./... and go test on the affected packages both pass, and pre-commit's go-fmt/go-vet hooks and the always-run lint-broken-symlinks hook were run directly after `pre-commit run` failed on infrastructure (git fetch of hook repos returned 403). Addresses #7250
🔧 Fix agent — iteration 1 (bot-triggered)Fixed the two comment-formatting findings and the missing-test finding with minimal, targeted edits (reflow two doc comments, add one test case). Recorded reasoned disagreements for the protected-path and process-attestation findings, which are not code defects: the former is expected policy gating for a protected-path change, and the latter is a PR-description item outside this agent's scope. Fixed (3):
Disagreed (2):
Tests: passed Decision points
Next steps:
|
|
🤖 Finished Review · ✅ Success · Started 10:08 PM UTC · Completed 10:24 PM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $5.80 |
|
Risk Assessment: moderate (2/5) DetailsTier 1 metadata is unchanged from the prior assessment (17 files, 916 lines, protected-path count 1, no dependency changes, non-first-time human author); git history reconfirms previously-noted high churn/fix-revert rate on pi_bootstrap.go, pi_run.go, and the Containerfile alongside the still-absent feature flag; the linked issue remains a well-documented, scope-matched fix with no unresolved discussion. Score anchors to the prior moderate rating of 2. Previous runRisk Assessment: moderate (2/5) DetailsTier 1 metadata is unchanged from the prior assessment (17 files, protected-path count 1, no dependency changes, same non-first-time human author) and the HEAD delta since the prior run is limited to doc/comment tweaks and edit-repair.js polish; git history confirms previously-noted high churn on pi_bootstrap.go/pi_run.go/Containerfile and lack of a feature flag, but issue context shows a well-documented, scope-matched fix with no unresolved discussion, so the score is anchored to the prior moderate rating of 2. Previous run (2)Risk Assessment: moderate (2/5) DetailsA tightly-scoped, well-tested compatibility shim matching its linked issue proposed design with no unresolved discussion, but it touches historically high-churn runtime files and a protected path, and ships with no feature flag to disable the unconditional behavior change, keeping it moderate rather than low. |
Superseded by updated review
An inline code span wrapped across two lines left `<path>` at the
start of a line, which VitePress parsed as an unterminated HTML tag
("docs/runtimes/pi.md: Element is missing end tag"). Rephrase so the
span stays on one line. Verified with a local `vitepress build docs`.
Assisted-by: Claude
Signed-off-by: Wayne Sun <gsun@redhat.com>
|
🤖 Review · Commit: |
Trim the pi.md behaviour bullet and the extension header, reflow the paragraphs the review flagged as overlong, and keep one canonical removal check (the comment above ARG PI_VERSION) that the bump table and the runtime notes point at instead of repeating. Assisted-by: Claude Signed-off-by: Wayne Sun <gsun@redhat.com>
|
🤖 Review · Commit: |
The repair record interpolated args.path verbatim, so a crafted path could plant a newline and forge a second [fullsend-edit-repair] record in the captured stderr, or carry a terminal escape into whoever reads it. Render it with JSON.stringify and cover newline, carriage return and escape in a test. Reported by the Qodo review on PR #7250. Assisted-by: Claude Signed-off-by: Wayne Sun <gsun@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 10:59 PM UTC · Completed 11:16 PM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $8.71 |
|
/fs-fix Do not sign off commits: no Two statements about pi behaviour in this PR are wrong. Both were checked against the pi source at tag v0.85.0 ( 1. A declared extension that also registers
Actual behaviour: Change: reword both texts to say pi rejects two extensions that register the same tool name, so a declared extension must not register 2. "pi silently skips a missing
Actual behaviour: a missing local Change: reword all three places. The guard exists for a rewritten copy, which pi would load and run, and for a consistent exit code; a deleted copy already fails closed in pi itself. Keep the Run |
|
🤖 Finished Fix · ✅ Success · Started 12:35 AM UTC · Completed 12:43 AM UTC Commit: Runtime: claude · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $1.00 |
Two statements about pi's behaviour, checked against pi v0.85.0's ResourceLoader.detectExtensionConflicts and reproduced on real pi, were wrong: - A declared extension that also registers edit does not win over the edit-repair extension; pi refuses to start, since the conflict check flags any tool name a second extension registers regardless of -e order. Reworded the comments in pi_run.go and pi_edit_repair_test.go, and added the constraint to the Pick a free name bullet in docs/runtimes/pi.md and the Reserved names bullet in docs/reference/harness-reference.md. - pi does not silently skip a missing -e path; it exits 1. The piEditRepairGuard exists for a rewritten copy, which pi would still load and run, and for a consistent, distinguishable exit code, not for a deleted one, which already fails closed in pi itself. Reworded the guard comment in pi_edit_repair.go and the two references in docs/contributing/runtime-implementation.md. Text-only changes; no logic, ordering, or test-structure changes. Addresses #7250
🔧 Fix agent — iteration 2 (human-triggered)Verified both behaviour claims against pi v0.85.0's ResourceLoader.detectExtensionConflicts as directed, confirmed they were wrong, and reworded the two comments, one test assertion message, and two docs bullets to state the actual behaviour (order-independent tool-name conflict rejection; missing -e path already fails closed in pi, guard is for a rewritten copy). Added one sentence each to docs/runtimes/pi.md and docs/reference/harness-reference.md where a harness author picks an extension name. No logic, ordering, or test-structure changes. Ran go test ./internal/runtime/... ./internal/pluginformat/... and node --test internal/runtime/pi_extension/*.test.mjs, both pass. Committed without git commit -s or a Signed-off-by trailer per the human instruction. Fixed (7):
Tests: passed Decision points
Next steps:
|
|
🤖 Review · Commit: |
The rule landed in the plugin-authoring bullets, but someone meets it as a pi startup error first, so give that error its own entry. Also correct the extension header: pi#8521 and #8962 are issues closed as not planned, and pi#8513 was the fix offered for the first. Assisted-by: Claude Signed-off-by: Wayne Sun <gsun@redhat.com>
|
🤖 Review · Commit: |
Add the bootstrap case for an explicit tools list naming Edit and Task (manifest field set, digest recorded, upload written, never in the shared extensions list), the persona path through childTools, and a 93 from an agent whose tools omit edit, which must pass through rather than be reported as tampering. Correct two comments against measured behaviour on pi 0.85.0: --tools '' and --no-tools drop extension tools too, which is why the gate compensates for --no-builtin-tools keeping them; and pi's own preparation repairs a bare edit object as well as a stringified array. Assisted-by: Claude Signed-off-by: Wayne Sun <gsun@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 1:07 AM UTC · Completed 1:36 AM UTC Commit: Runtime: pi · Model: sonnet → claude-sonnet-5 · Effort: high · Cost: $13.20 |
Summary
On the pi runtime, pi's
edittool rejects two malformed argument shapes that models send in practice (edits.0: must be object). Onecoderun on #7218 hit 46 of these and cost more than twice a clean run. This PR adds a fullsend-embedded pi extension that repairs just those two shapes before validation. It covers every model on pi, not only grok, and is meant to be removed once the pinned pi version fixes them upstream.Related Issue
Fixes #7249
Changes
internal/runtime/pi_extension/fullsend-edit-repair.js(new). Registers anedittool that wraps pi's owncreateEditToolDefinitionand only replacesprepareArguments. It repairseditssent as a JSON string with raw control characters (pi#8521) andeditsitems sent as JSON strings (pi#8962), using pi's ownparseJsonWithRepair. Input it can't improve passes through unchanged, and each repair is logged to stderr.internal/runtime/pi_edit_repair.go). The extension loads only when the agent's tools includeedit(the pi default does). On real pi,--no-builtin-toolsdoes not filter extension tools, so loading it for atools: []agent would grantedit. The same gate applies to sub-agents: the manifest carries it as a separateeditRepairExtensionfield, andfullsend-agent.jspasses it with-eonly to a child whose tools includeedit. It is kept out of the sharedextensionslist..envsha256 launch guard (exit 93) and a child digest re-check before each dispatch. The drift message now names the file that changed.fullsend-edit-repairis added toPiReservedExtensionNames, so a plugin can't shadow it.docs/runtimes/pi.md(behaviour and troubleshooting),runtime-implementation.md,harness-reference.mdandarchitecture.md. A comment next toARG PI_VERSIONin the Containerfile gives the removal check.Known limitation — one
edittool per run. pi refuses two extensions that register the same tool name, whichever order they load in, and exits 1 at startup (Tool "edit" conflicts with ...). So while this stopgap is loaded, a harnessplugins:entry that registers its ownedittool cannot be used by an agent that has theedittool. Incidence today is zero — no harness in the org registersedit. Documented indocs/runtimes/pi.md(plugin naming rule plus a troubleshooting entry naming the error) anddocs/reference/harness-reference.md.A runtime alternative was measured and declined: registering from
session_start(after pi's one-shot conflict check) and standing down whengetAllTools()showseditalready owned by another extension. It works, but it adds three more pi API surfaces to a stopgap whose success condition is deletion, and it trades a loud startup failure for a silent degrade. Detection on the Go side is not viable:pluginformatreads onlypackage.jsonpaths and names, and this PR's own extension registerseditwithout the literal string appearing anywhere.Security property: pi runs
prepareArgumentsbefore validation and before the extensiontool_callevent, so fullsend's security hooks inspect the repaired arguments, which are exactly what gets applied.Testing
make lintpasses (stage changes first, then run)Real pi 0.85.0 (the pinned version), driven end to end by pi's faux provider. Opt-in with
FULLSEND_TEST_PI_BIN:Without the extension, each shape fails with
Validation failed for tool "edit": - edits.0: must be object. With it, the file is edited, atool_callhook sees the repairededits, and the repair is logged. The allowlist test shows--tools read,grepfilters the extension'seditand--no-builtin-toolsdoes not, which is why the gate exists.node --test internal/runtime/pi_extension/*.test.mjs, withFULLSEND_TEST_PI_BINset).go test ./internal/runtime/... ./internal/pluginformat/...passes. New functions are at 100% coverage. The only uncovered new statement is the extension-upload error return inBootstrap, which is also untested for the existing hooks and Agent uploads.TestListTriggeredHarnesses_BaseComposition(internal/harnessdispatch) fails locally on unmodified main 773149d too. It is unrelated to this change.Removal: on a
PI_VERSIONbump, runfullsend-edit-repair.test.mjswithFULLSEND_TEST_PI_BINpointing at the new pi. When the without-extension controls stop failing, delete the extension andinternal/runtime/pi_edit_repair.go.Checklist
!for breaking changes)