| 1 | # Attachment admission and request isolation (#10457) |
| 2 | |
| 3 | [中文](ATTACHMENT_REPAIR_10457.zh-CN.md) |
| 4 | |
| 5 | The attachment source is PR head |
| 6 | `b582ebabe089fb75a6ee8ba23dddcb0a48a873ca`. It was semantically replayed on |
| 7 | `main-v2` `93fb72870758f2ff60ae24386406b0e5f0782f0e`, which already contains the |
| 8 | durable-draft work from #10469 and the fixed-snapshot export implementation |
| 9 | from #10471, then rebased onto `main-v2` |
| 10 | `9b0cc56e5c0e60d92cb447b360ef8fa018c7c72b`. That base also includes #10500's |
| 11 | model-scoped reasoning capability resolution. `git range-diff` against the prior |
| 12 | `bd0733b9` replay changes only the generated Desktop contract digest, so the |
| 13 | attachment patch and later mainline behavior are both preserved. No changes |
| 14 | from #10346 are incorporated. The design borrows admission, |
| 15 | capability ownership and independent cancellation from deepseek-harness |
| 16 | `ddefc45fbc7f8e46dd73185e68295696d1297887`, without importing its image |
| 17 | normalization, Sharp dependency or global attachment store. |
| 18 | |
| 19 | ## Behavior and ownership |
| 20 | |
| 21 | An explicit image is part of the submission, not a suggestion for a tool to |
| 22 | search the filesystem. Preparation validates the complete ordered batch and |
| 23 | persists originals before admission. Failure leaves the draft intact and does |
| 24 | not start the provider, tools, a user message or Goal setup. Content objects |
| 25 | published before a later failure are harmless unreferenced objects; rollback |
| 26 | never removes originals that another submission may already reference. |
| 27 | |
| 28 | `PreparedSubmission` is a host-only candidate. The final admission gate checks |
| 29 | its controller and storage scope again and looks up the receipt before source |
| 30 | access. A versioned fingerprint describes text, action and logical attachment |
| 31 | identities; it excludes temporary credentials and accepted content digests. |
| 32 | The acceptance event contains the frozen image references. Deferred turn |
| 33 | bodies capture those references before parking and never read temporary |
| 34 | submission state asynchronously. Legacy gate-owned synchronous adapters remain. |
| 35 | |
| 36 | The target RPC captures tab generation, controller, workspace, host/session |
| 37 | storage identity and runtime epoch. Capture precedes file reads and hashing. |
| 38 | Changing focus does not retarget work; retiring the target invalidates it. |
| 39 | The controller service owns draft credentials; the host shares only immutable |
| 40 | content and discardable variants. Same-session runtime replacement transfers |
| 41 | credential proofs. Validation on an explicit user retry renews credentials; |
| 42 | repeated renewal returns the same credential. Releasing either proof revokes |
| 43 | that draft family. Rebinding never sends a message. Successful Composer cleanup |
| 44 | is conditional on the exact submitted draft version, including queue receipts. |
| 45 | |
| 46 | Late session publication also binds the attachment store to that runtime's |
| 47 | authoritative content graph. Creating a controller before its session must not |
| 48 | pin attachments to a legacy fallback directory. `attachment_owner_test.go` |
| 49 | reproduces that construction order and verifies subsequent runtime renewal. |
| 50 | |
| 51 | ## Finding-to-fix map |
| 52 | |
| 53 | | Finding | Shared repair | Regression evidence | |
| 54 | | --- | --- | --- | |
| 55 | | Accepted images absent from actual requests | Carry `ImageInputs` through ordinary, edited, Goal, invocation and queued turns; resolve before protocol normalization | `attachment_endpoints_test.go`, `attachment_routing_test.go`, loopback provider capture | |
| 56 | | Legacy workspace path escape | Open under `os.Root`, refuse leaf links, compare opened/path identities before and after reading; nonblocking Unix open | `isolation_regression_test.go`, workspace/permission identity tests | |
| 57 | | Export omits images or damages an existing target | Extend #10471 `ExportSnapshot` with the authorized ordered image closure and task-private rendering; retain its no-replace publication and recovery journal | `sessionexport/build_test.go`, session attachment closure and #10471 publication tests | |
| 58 | | Queue depends on temporary draft | Persist ordered inputs and source aliases; validate immutable objects before claim; prepare edits before replacing old envelopes | Queue endpoint, corruption-block and mixed-source edit tests | |
| 59 | | Retry creates a second turn or rereads deleted sources | Lookup durable versioned receipt before preparation and again under admission gate | Draft-release retry and canonical credential fingerprint tests | |
| 60 | | Mixed attachments bypass batch limits | Merge structured, legacy and authorized sources; validate all originals with one policy | Invalid endpoint batches, mixed-source/dedup, 20/21 and byte-boundary tests | |
| 61 | | Service/draft lifetime follows focus or runtime accidentally | Owner-created service, lifecycle-bound contexts, opaque target tokens and validated credential renewal | Desktop target, replacement, cancellation and Composer tests | |
| 62 | | Canceled transform captures a later request | Remove last-waiter inflight entry under lock; only its own task may finish/remove it | Deterministic independent-waiter and successor tests with race detector | |
| 63 | | Encoding/upload follows wrong model/account | Capture provider configuration; resolve by actual parent/child/understanding model; propagate cancellation | Upload route interception, real boot child and text-to-vision provider tests | |
| 64 | |
| 65 | Test paths are under `internal/control`, `internal/attachment`, `internal/boot`, |
| 66 | `internal/session`, and `desktop`. They assert actual provider messages in |
| 67 | addition to preparer return values. Tool originals are persisted before an |
| 68 | understanding request. A persistence failure preserves completed tool text and |
| 69 | diagnostics rather than replaying an action that may have side effects. |
| 70 | |
| 71 | ## Compatibility and cache contract |
| 72 | |
| 73 | - Originals, `.content-v1`, revision 3 and inbox schema 3 stay in place. |
| 74 | - Legacy `Images []string`, system prompts, tool schemas and history order are |
| 75 | not rewritten. Three loopback protocol tests compare the actual serialized |
| 76 | OpenAI, Responses and Anthropic bodies before/after legacy resolution. |
| 77 | - New images use the existing deterministic policy v1 (1568-pixel bound, |
| 78 | JPEG quality 85 where applicable); the 512 MiB variant cache never deletes |
| 79 | original objects. Both source validation and encoding use the same host |
| 80 | heavy-work limit of two. |
| 81 | - New image request bytes differ from the broken request that omitted images; |
| 82 | the first prompt-cache miss for those submissions is expected. |
| 83 | - Unknown receipt versions and unverifiable legacy receipt mismatches fail |
| 84 | explicitly instead of guessing and replaying. |
| 85 | - `attachments-v2` target operations are negotiated explicitly; unsupported remote hosts do not |
| 86 | silently fall back to another workspace. |
| 87 | - Schema compatibility tests prove that unsupported revision 3 data is rejected |
| 88 | without an in-place downgrade. Packaged previous-binary validation remains a |
| 89 | separate release-qualification step and is not inferred from source tests. |
| 90 | |
| 91 | ## Reproducible qualification |
| 92 | |
| 93 | Qualification has three risk-based layers. |
| 94 | |
| 95 | The PR gate runs after code changes and blocks a push on failure. It covers the |
| 96 | focused attachment, submission, queue, history-preview and variant-cache Go |
| 97 | tests; deterministic concurrency tests and relevant race suites; Desktop and |
| 98 | Composer attachment tests; type checking, contract freshness, `repolint`, |
| 99 | inventory, the preview bundle budget, actual provider image digests, zero |
| 100 | provider/tool/Bash calls after explicit-image rejection, and unchanged legacy |
| 101 | `Images []string` bytes for OpenAI, Responses and Anthropic. A substantial shared |
| 102 | code change also runs the root, Desktop and frontend suites once. Later changes |
| 103 | limited to documentation, mocks or package splitting rerun their affected suites |
| 104 | and static gates. |
| 105 | |
| 106 | The patch-id-equivalent integration candidate completed the root and Desktop Go |
| 107 | suites, focused race suites, all 409 frontend suites, frontend type checking, |
| 108 | the production build and bundle budgets, generated-contract freshness, inventory |
| 109 | and repository lint. The final rebase preserves the attachment patch according |
| 110 | to `range-diff` and reruns the affected model-routing, Desktop and frontend gates. |
| 111 | Desktop Linux cross-lint still reports four unchanged tray stubs from the |
| 112 | `main-v2` baseline; it is not an attachment regression. |
| 113 | |
| 114 | The final-SHA package gate runs once before pushing. Build with |
| 115 | `scripts/desktop-build.sh`, extract the **finished** ZIP into a new directory, |
| 116 | and run: |
| 117 | |
| 118 | ```sh |
| 119 | node desktop/packaging/smoke.mjs /isolated/Reasonix.app |
| 120 | node desktop/packaging/attachment-native-smoke.mjs \ |
| 121 | /isolated/Reasonix.app /isolated/evidence |
| 122 | ``` |
| 123 | |
| 124 | The attachment fixture launches the production Electron shell and bundled |
| 125 | service with a disposable data home and a loopback provider. It checks exact |
| 126 | image bytes with a process directory distinct from the workspace under |
| 127 | workspace-write permission, `view_image` parity, cancellation/retry without a |
| 128 | duplicate turn, and real Composer preservation after an image read fails. It |
| 129 | writes JSON evidence and a screenshot without installing or replacing an app. |
| 130 | |
| 131 | The source branch previously produced an isolated preview package from |
| 132 | `86fd76f20d7d` and loopback evidence for its attachment flow. That evidence is |
| 133 | historical input, not qualification of the integrated head. Generated package |
| 134 | evidence remains uncommitted; a release candidate must rerun the ordinary package |
| 135 | smoke and `attachment-native-smoke.mjs` against its own SHA before making a |
| 136 | packaged-runtime claim. |
| 137 | |
| 138 | Release and high-risk gates are conditional. Storage revision or inbox schema |
| 139 | changes require previous-version upgrade/downgrade checks; shell, signing or |
| 140 | service-layout changes require native signing and startup matrices; permission |
| 141 | model changes require workspace-write/full-access comparison; provider, Files |
| 142 | API or upload-cache changes require live-provider and account-isolation checks. |
| 143 | This PR keeps revision 3 and inbox schema 3 and does not change Files API or |
| 144 | upload caching, so previous-binary and live-network checks are not applicable. |
| 145 | |
| 146 | Loopback capture proves image transport, routing and permission behavior, not |
| 147 | semantic recognition by a paid remote model. No live credential is required or |
| 148 | read by these fixtures. Crash publication recovery runs when the same destination |
| 149 | is reopened for publication; there is no scan of arbitrary export directories at |
| 150 | application startup. |
| 151 | |
| 152 | The repair does not merge or release the PR, overwrite an installed application, |
| 153 | change Bash search behavior, add upload caching or collect original objects. |
| 154 |