{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"a136ca99-e312-45fa-8624-11d2c76522e3","kind":"audit","nodes":[{"acceptedSubmissionHash":"74aed55d184df5fd3b2bccf282744f62a556334c8dd4d1bc26b34771bfc8bf72","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"afe82110cf4165520b37052b0aeb53b41d38838182b14d7d03f98b095059b473","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"952764ba5be9edd84c82c82037e7598aff98d9d9a44faa1212cdcb1cb8269413","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"28db8aede05c92baf27bf891578cdeb7c62d19ff22bc9ce85e0b0dbe871aaa05","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e3debdcc182715a58f4cb6e7478924d5156573d6e6d038185fd6abbb2068f8d7","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Audit commit b157921170c318f8eeee638b06025a81cf768c02 against base 575e8a8f74da581298a8527f493e9eab8a45901f. Focus on the job-callback correction and its interactions with existing protections. Review only: no source edits, deployment or paid Intake calls.\n\nVerify against https://imdnotdead.fun/docs/: onJobResult(bytes32,bytes32,string), registered selector, exact supplied resultHash, JobDelivered and JobResultURI. Independently test canonical protocol encoding, not just agreement between consumer and mock.\n\nCheck Intake-only authorization, unknown IDs, duplicates, malformed data, late delivery and preservation of finalized NotPassed reviews. Examine untrusted URI decoding/rendering, ABI/readers and unchanged oracle callbacks. Check for regressions in payment accounting, caller price caps, replay protection and listing/ad escrow.\n\nRerun Foundry, site/ABI and isolated cold-gas tests under pinned settings. Preserve all assertions, the 200k callback cap and 24,576-byte runtime limit. Verify docs/checks/job-protocol-tuple/ evidence independently. Report any SDK receipt-wait race separately from contract failures.\n\nReturn reproducible findings with file/line, impact and tests. Distinguish material deployment blockers, disclosed owner powers and remaining live-Intake/oracle-mapping/signer-gas checks. Local tests are not proof of a working paid end-to-end service.","parentJobId":null,"planHash":"b8c60569a61ac4368ccb49feafaafb7e1f20a938f7a18dd9d7a0ac132a558473","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"a136ca99-e312-45fa-8624-11d2c76522e3","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51488","feedbackHash":"03068978a4e74f396ed00105065ed27907b7b7460073167a0818a82a8ab8d751","nodeKey":"audit_economics","submissionHash":"74aed55d184df5fd3b2bccf282744f62a556334c8dd4d1bc26b34771bfc8bf72","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51084","feedbackHash":"77fa5fd8e4f222ba14701d731bc0668956a09a6304dcf9e6ec1250a4521a6df4","nodeKey":"audit_flow","submissionHash":"afe82110cf4165520b37052b0aeb53b41d38838182b14d7d03f98b095059b473","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52266","feedbackHash":"766dcc4d5b91f7fe2eeef9a752da446a70c68a6b7622ac28fa77bffcb811a0d2","nodeKey":"audit_judge","submissionHash":"952764ba5be9edd84c82c82037e7598aff98d9d9a44faa1212cdcb1cb8269413","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52604","feedbackHash":"b8c01efc0b8587a93226056da0571e416a97bf00a38b00ed0e9747f94253426d","nodeKey":"audit_math","submissionHash":"28db8aede05c92baf27bf891578cdeb7c62d19ff22bc9ce85e0b0dbe871aaa05","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52608","feedbackHash":"43827a2fa8267c9b75306c3a27fecc783251b3adb8a36af5208a08eaab5d56c6","nodeKey":"audit_permissions","submissionHash":"e3debdcc182715a58f4cb6e7478924d5156573d6e6d038185fd6abbb2068f8d7","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"90a0b3496898222a99da21fc06af4c7881fb1d5339f51a0bf5c391a91707607d","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9e17b97b8ac7c97a","findings":[{"citation":"resolved","description":"The new 'Job result callback and URI' section (and docs/checks/job-protocol-tuple/README.md line 9) cites the protocol tuple with the URL https://imdnotdead.fun/docs/#what-comes-back. The published page defines no element with id 'what-comes-back'; the section that specifies '(bytes32 requestId, bytes32 resultHash, string uri)' and 'resultHash is the keccak-256 of its bytes' has id 'intake-what-comes-back' (the page's own in-page link is href=\"#intake-what-comes-back\"). The reference the correction is justified by therefore does not resolve to the cited section; a reader lands at the top of a 670 KB page. The tuple, hash semantics and 200,000-gas one-shot call stated in the repository docs do match the page text, so this is a citation defect only, not a protocol mismatch. Fix: change both links to https://imdnotdead.fun/docs/#intake-what-comes-back.","line":27,"path":"docs/IMD_INTEGRATION.md","reproduction":"curl -sL https://imdnotdead.fun/docs/ -o docs.html; grep -o 'id=\"[^\"]*comes[^\"]*\"' docs.html  -> only id=\"intake-what-comes-back\"; grep -c 'id=\"what-comes-back\"' docs.html -> 0. Expected: the anchor in docs/IMD_INTEGRATION.md:27 and docs/checks/job-protocol-tuple/README.md:9 exists on the page. Actual: it does not; only #intake-what-comes-back exists.","severity":"info","snippet":"The [protocol's completion tuple](https://imdnotdead.fun/docs/#what-comes-back) is","title":"Protocol doc link uses a non-existent anchor (#what-comes-back); the page's anchor is #intake-what-comes-back"},{"citation":"resolved","description":"StoreSdkTest.send submits with eth_sendTransaction and immediately calls eth_getTransactionReceipt exactly once. If the receipt is not yet available the next line raises AssertionError('local transaction failed'), which is indistinguishable from a reverted deployment. This is pre-existing harness code, not changed by commit b157921 and not a contract defect. The test starts its own Anvil with default automine, where the receipt is available synchronously, and the suite passed 10/10 in 1.9 s on this machine; no receipt-wait race was observed. The fragility only materialises if the fixture's Anvil flags change (e.g. --block-time or --no-mining) or the harness is pointed at a slower node. Fix (optional): poll eth_getTransactionReceipt until non-null with a bounded timeout, and only then check status.","line":77,"path":"test/test_store_sdk.py","reproduction":"Local result: python3 -m unittest discover -s test -p 'test_*.py' -v -> Ran 10 tests, OK (no race). Failing state: start the same Anvil with '--block-time 1' (edit test/test_store_sdk.py line 29) and run the suite; eth_getTransactionReceipt returns null for the PoolManager deployment and setUpClass raises AssertionError('local transaction failed') although the transaction later mines successfully. Expected: wait for the receipt, then check status; Actual: single read, failure misattributed to the transaction.","severity":"info","snippet":"        receipt = cls.rpc(\"eth_getTransactionReceipt\", [tx_hash])","title":"SDK integration test reads the transaction receipt once with no wait; fine under Anvil automine, misreports as 'local transaction failed' on any non-instant node (reported separately from contract beh"}],"hash":"28db8aede05c92baf27bf891578cdeb7c62d19ff22bc9ce85e0b0dbe871aaa05","nodeId":"0854e09e-84bf-4eb4-8ccd-c5b9bb9cdc9a","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result. The job-callback correction is correct and introduces no deployment blocker.\n\n**What was verified**\n\n- **Protocol conformance.** The live page at imd.fun/docs specifies `(bytes32 requestId, bytes32 resultHash, string uri)` with the hash being the keccak-256 of the result document bytes. The new `onJobResult` stores the supplied hash unchanged. Selector 0x325a77b4 and the two event topics match independent `cast` output.\n- **Canonical encoding, independently.** A scratch test hand-assembles the ABI calldata byte by byte, confirms it equals both `cast calldata` and the compiler encoding, and delivers it through a pranked Intake call. Both events carry the exact hash and URI, and the `getJob` return stays 15 words.\n- **Guards.** Intake-only access, unknown IDs, duplicate and failure replays, six malformed payloads, late delivery before and after local expiry, and a finalized NotPassed review surviving late delivery all behave as documented. Oracle callbacks are untouched in the source diff. Payment, price-cap, replay and escrow code is unchanged and its full suite and invariants pass.\n- **Reader safety.** The site renders the URI only via textContent with bounded decoding and never links or fetches it. The published ABI equals compiler output entry for entry.\n- **Pinned reruns.** Foundry suite: 252 passed on both the repo seed and the assignment seed. Isolated cold gas: 67,052 for a 4,096-byte URI under the 200k cap, all oracle receipt numbers unchanged. Runtime 24,117 bytes. `forge fmt --check`, 18 node tests and 10 Python SDK tests all pass.\n- **Evidence.** The evidence patch applied to the base commit reproduces the audited tree exactly, and I reproduced the RED state on the base tree myself: registered selector 0x5dd4c70b and the tuple fails to decode.\n\n**Findings written (both informational)**\n\n- Both new docs cite `imd.fun/docs/#what-comes-back`, an anchor that does not exist. The page's anchor is `#intake-what-comes-back`.\n- The SDK test reads the receipt once with no polling. No race occurred locally under Anvil automine. Reported separately from contract behaviour as requested.\n\n**Disclosed owner powers and open live checks** remain as documented: Intake and signer rotation for pending asks, review binding and recovery, and signer-epoch revocation. Still requiring a paid pilot: the live writer's use of the selector, request-ID routing, actual URI length (a cold one-shot call accepts up to about 20,224 URI bytes), the Intake-to-oracle mapping, and production signer gas. Local tests are not proof of a working paid end-to-end service.","treeHash":null,"usage":{"cachedInputTokens":1098478,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":27394,"runtime":"claude","turns":43,"wallClockMs":733730}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5835e48821d8827d","findings":[{"citation":"resolved","description":"onJobResult copies the whole calldata string into a LOG, at 8 gas per byte plus memory expansion, inside the Intake's fixed 200,000-gas one-shot call. The contract puts no bound on the URI length. Measured with vm.cool on an Open job allocated in an earlier transaction: a 18,432-byte URI still succeeds, a 20,480-byte URI runs out of gas. When that happens the Intake records delivered=false, keeps the fee, and the job stays Open until the requester expires it; the result is still readable by transaction hash per the protocol page. This is already disclosed in docs/IMD_INTEGRATION.md ('An excessive URI can exhaust the fixed callback gas') and the protocol specifies short URIs (ipfs://CID/result.json or the plane's /intake-results/:id), so it is not a deployment blocker. It is recorded here so the operational bound is concrete: 4,096 bytes (the tested figure, 67,052 gas) has ample headroom; the hard ceiling is between 18,432 and 20,480 bytes for a cold job.","line":337,"path":"src/imd/HookJobRequester.sol","reproduction":"State: an Open job id allocated in a prior transaction, requester storage cold. Input: msg.sender == intake, onJobResult(id, keccak256(\"h\"), string(new bytes(20480))) with gas: 200_000. Expected (if a bound mattered): delivery recorded. Actual: the call reverts out of gas; getJob(id).status stays Open and the Intake's one attempt is consumed. Same call with string(new bytes(18432)) succeeds. Scratch test test/scratch/CanonicalEncoding.t.sol::testUriLengthHeadroom prints 'fits len' up to 18432 and 'FAILS len' from 20480 (forge test --match-path test/scratch/CanonicalEncoding.t.sol -vv).","severity":"info","snippet":"        emit JobResultURI(requestId, uri);","title":"Job result URIs above ~18 KiB exceed the 200k callback stipend on a cold job and burn the single Intake attempt"},{"citation":"resolved","description":"The Python SDK test helper send() calls eth_getTransactionReceipt once, directly after eth_sendTransaction, and treats a null receipt as 'local transaction failed'. Under the suite's own disposable Anvil (default automine) the receipt is available synchronously and all 10 tests pass here, so no race was observed in the pinned configuration. The race appears only if the node is not automining (for example Anvil with --block-time, or any real RPC): the receipt is null for the first poll and the suite aborts with an AssertionError that is indistinguishable from a reverted transaction. This is a test-harness limitation, not a contract or SDK-library defect, and it does not affect the job-callback change under review. Reported separately as the assignment requested.","line":77,"path":"test/test_store_sdk.py","reproduction":"State: anvil --host 127.0.0.1 --port 18545 --chain-id 31337 --block-time 3. Input: eth_sendTransaction({from: accounts[0], to: accounts[0], value: 0x1, gas: 21000}) followed immediately by eth_getTransactionReceipt(hash). Expected by send(): a receipt with status 0x1. Actual: null (reproduced with a 12-line urllib script; output 'receipt immediately after send: None'), so send() would raise AssertionError('local transaction failed') although the transaction is mined three seconds later. With the suite's default automine Anvil the same sequence returns the receipt and python3 -m unittest discover -s test -p 'test_*.py' passes 10/10.","severity":"info","snippet":"        receipt = cls.rpc(\"eth_getTransactionReceipt\", [tx_hash])","title":"SDK integration test reads the receipt immediately after eth_sendTransaction with no wait, which fails on any non-automining node (separate from contract behaviour)"}],"hash":"74aed55d184df5fd3b2bccf282744f62a556334c8dd4d1bc26b34771bfc8bf72","nodeId":"228bfc26-ce23-4a38-805d-83ff3cab5a07","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds two informational findings. No defect was found in the job-callback correction itself, and no material deployment blocker was identified in the audited commit.\n\n**What was verified independently**\n\n- **Protocol tuple.** The imd.fun docs give the completion tuple as `(bytes32 requestId, bytes32 resultHash, string uri)` with a 200,000-gas one-shot try. The page's example selectors hash to exactly this contract's parameter types for all three callbacks: `onResult` 0x4374622e, `onFailure` 0x5a0df71e and `onOracleResult` 0x510379c7, with only the function name differing. That confirms the shape without relying on mock/consumer agreement.\n- **Canonical encoding.** A scratch test built calldata by hand from the ABI spec, matched it byte-for-byte against the compiler's encoding, and delivered it to the contract under 200k gas. The supplied hash is stored exactly and both events fire with the request ID as topic 1. A valid non-tight offset also decodes, and the event is re-emitted canonically.\n- **Guards.** Intake-only authorization, unknown IDs, duplicate delivery, six malformed payload shapes, the retired `(bytes32,bytes)` selector, late delivery before and after local expiry, and a finalized NotPassed surviving late delivery are all covered by the new suite and behave as documented. Oracle callbacks, payment accounting, price caps, Intake-ID replay protection and listing/ad escrow are untouched by the diff.\n- **Reruns under pinned settings.** Full suite 252 passed, isolated cold-gas 9 passed with the 4,096-byte URI at 67,052 gas, site/ABI node tests 18 passed, Python SDK tests 10 passed, formatter clean, vendored check 96 files verified. Runtime is 24,117 bytes, ABI JSON equals the artifact, and the fingerprints match.\n- **Evidence directory.** The red log shows the old selector 0x5dd4c70b failing on the real tuple, the green log shows 0x325a77b4 and the expected hash, and the implementation patch is identical to the real diff modulo hunk ordering.\n\n**Findings recorded**\n\n- **Info, URI gas ceiling.** A cold job accepts an 18,432-byte URI but a 20,480-byte URI exceeds the stipend and burns the single attempt. Already disclosed, and protocol URIs are short.\n- **Info, SDK receipt wait.** The Python test helper reads the receipt once with no polling. Reproduced as null on an Anvil with `--block-time 3`. No race occurs under the suite's own automining Anvil, so this is reported separately from contract behaviour.\n\n**Disclosed owner powers and open live checks.** The owner can rotate the Intake and signer for pending asks, bind and reset reviews, confirm expiry and revoke signer epochs. These stay trust assumptions, not findings. Still unverified locally: live Intake action acceptance and pricing, request-ID routing on chain, real result-document hash and URI availability, the Intake-to-oracle mapping, and production signer gas. Local tests remain no proof of a working paid end-to-end service.","treeHash":null,"usage":{"cachedInputTokens":1358699,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":25695,"runtime":"claude","turns":39,"wallClockMs":433604}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"644eb561f6d70a85","findings":[{"citation":"resolved","description":"decodeResultURI enforces canonical ABI layout, a 32 KiB bound and strict UTF-8, but valid UTF-8 includes the Unicode bidirectional overrides (U+202A-U+202E, U+2066-U+2069), zero-width characters (U+200B-U+200F, U+FEFF) and C0/C1 controls. The decoded string is written to td.textContent with no escaping and no CSS bidi isolation. textContent blocks HTML/script injection (already covered by the node tests), but not display reordering: a URI chosen by whoever controls the Intake writer or the result location can appear on the page as a different location than the bytes on chain. The contract is unaffected (the event carries the exact bytes); only the reader's display is. Not a deployment blocker. Fix (reader only): before display, map code points outside printable ranges (C0/C1 controls, U+200B-U+200F, U+202A-U+202E, U+2066-U+2069, U+FEFF) to visible escapes such as \\u{202E}, and/or style the details cell with `unicode-bidi: isolate; white-space: pre-wrap; overflow-wrap: anywhere`; add a case to the 'history exposes URI only as unverified text' test asserting the rendered textContent contains no raw bidi/control code points. Merged from audit_flow; reproduced by the judge.","line":168,"path":"site/index.html","reproduction":"Input: a JobResultURI log whose string is \"ipfs://QmSafe/result.json\\u202E nosj.tluser/moc.live//:sptth\" (data from `cast abi-encode 'f(string)' ...`), address = the requester, topics = [JobResultURI topic, request id]. Load the <script> of site/index.html in the same node vm harness test/requester-abi.test.cjs uses (window.ethereum returning that single log) and call history(\"\", addr, id, \"\"). Expected: the details cell shows the bytes unambiguously (escaped or bidi-isolated). Actual (node v22.22.1, this tree): the cell's textContent is \"Result URI (unverified): ipfs://QmSafe/result.json\\u202E nosj.tluser/moc.live//:sptth\" with the raw U+202E retained, so a browser renders the tail reversed and it reads as https://evil.com/result.json. Reproduction script: test/scratch/bidi.cjs (exits 1 listing 'U+202E' as retained).","severity":"low","snippet":"        details = \"Result URI (unverified): \" + (decodeResultURI(l.data) || \"(empty URI)\");","title":"Status page renders bidi/zero-width control code points in the result URI unescaped, allowing visual spoofing of the unverified location"},{"citation":"resolved","description":"onJobResult copies the whole calldata string into a LOG (8 gas per byte plus memory expansion) inside the Intake's fixed 200,000-gas one-shot try, and puts no bound on the URI length. When the stipend runs out the callback reverts, the Intake records delivered=false and keeps the fee, and the job stays Open until the requester expires it. This is already disclosed in docs/IMD_INTEGRATION.md ('An excessive URI can exhaust the fixed callback gas'; only 4,096 bytes measured) and the protocol specifies short URIs (ipfs://CID/result.json or the plane's /intake-results/:id), so it is not a deployment blocker and no source change is required. Recorded so the operator's pilot checklist pins the real ceiling instead of the page's 32 KiB display bound: cold, exact-200k delivery accepts 20,224 bytes and fails at 20,256 bytes (judge measurement in 32-byte steps, pinned 0.8.26/Cancun/200 runs, same result with and without --isolate). The two specialist measurements (18,432 fits / 20,480 fails; 16,384 fits / 20,480 fails) are consistent with this and are merged here. Optional: state the ceiling in the 'Callback gas budget' section of docs/IMD_INTEGRATION.md.","line":337,"path":"src/imd/HookJobRequester.sol","reproduction":"State: an Open job id allocated in an earlier transaction. Input: from msg.sender == intake, call onJobResult(id, keccak256('h'), string(new bytes(N))) with exactly 200,000 gas after vm.cool(requester), restoring state between trials. Expected per docs: unspecified ceiling. Actual: N = 20,224 succeeds (status Delivered); N = 20,256 and above runs out of gas, the job stays Open and MockIntake.reqs(id).settled is true (attempt consumed, fee retained). Run: `forge test --match-path test/scratch/CanonicalEncoding.t.sol --match-test testUriLengthCeilingUnder200k -vv` (logs 'largest URI bytes delivered under exact 200k (cold): 20224' and 'smallest ... that runs out of gas: 20256'); `--isolate` gives the same numbers.","severity":"info","snippet":"        emit JobResultURI(requestId, uri);","title":"Measured URI ceiling under the 200k callback stipend is about 20,224 bytes on a cold job; larger URIs burn the single Intake attempt and the fee (documented, not a blocker)"},{"citation":"resolved","description":"The new 'Job result callback and URI' section, and docs/checks/job-protocol-tuple/README.md line 9, justify the callback correction by linking https://imdnotdead.fun/docs/#what-comes-back. The published page defines no element with id 'what-comes-back'; the section that states '(bytes32 requestId, bytes32 resultHash, string uri)' and 'resultHash is the keccak-256 of its bytes' has id 'intake-what-comes-back'. A reader following the citation lands at the top of a ~670 KB page. The tuple, hash semantics and 'Gas 200,000 for the call, once, inside a try' stated in the repository docs do match the page text (verified by the judge), so this is a citation defect only, not a protocol mismatch. Fix: change both links to https://imdnotdead.fun/docs/#intake-what-comes-back.","line":27,"path":"docs/IMD_INTEGRATION.md","reproduction":"curl -sL https://imdnotdead.fun/docs/ -o docs.html; grep -o 'id=\"[^\"]*comes[^\"]*\"' docs.html -> only id=\"intake-what-comes-back\"; grep -c 'id=\"what-comes-back\"' docs.html -> 0 (fetched 2026-10-17). Expected: the anchor in docs/IMD_INTEGRATION.md:27 and docs/checks/job-protocol-tuple/README.md:9 resolves to the tuple section. Actual: it does not resolve; only #intake-what-comes-back exists.","severity":"info","snippet":"The [protocol's completion tuple](https://imdnotdead.fun/docs/#what-comes-back) is","title":"Protocol citation uses a non-existent anchor #what-comes-back; the published page's anchor is #intake-what-comes-back"},{"citation":"resolved","description":"StoreSdkTest.send submits with eth_sendTransaction and immediately calls eth_getTransactionReceipt exactly once; a null receipt raises AssertionError('local transaction failed'), indistinguishable from a reverted transaction. Under the suite's own disposable Anvil (default automine) the receipt is available synchronously and all 10 tests pass on this tree (judge rerun: Ran 10 tests, OK), so no race was observed in the pinned configuration. The race appears only if the node does not automine (Anvil with --block-time or --no-mining, or any real RPC). This is pre-existing harness code, not changed by commit b157921, and it does not affect the job-callback change or any contract behaviour; it is reported separately from contract failures as the assignment asked. Fix (optional): poll eth_getTransactionReceipt until non-null with a bounded timeout, then check status. Merged from audit_economics and audit_math.","line":77,"path":"test/test_store_sdk.py","reproduction":"State: anvil --host 127.0.0.1 --port 18545 --chain-id 31337 --block-time 3. Input: eth_sendTransaction({from: accounts[0], to: accounts[0], value: 0x1, gas: 0x5208}) followed immediately by eth_getTransactionReceipt(hash). Expected by send(): a receipt with status 0x1. Actual (judge's 20-line urllib script, same sequence as send()): 'receipt immediately after send: None', then 'receipt after 4s status: 0x1', so send() would raise AssertionError('local transaction failed') for a transaction that mines successfully three seconds later. With the suite's default automine Anvil, python3 -m unittest discover -s test -p 'test_*.py' -v passes 10/10.","severity":"info","snippet":"        receipt = cls.rpc(\"eth_getTransactionReceipt\", [tx_hash])","title":"SDK integration harness reads the transaction receipt once with no wait; misreports as 'local transaction failed' on any non-automining node (test harness only, not a contract defect)"}],"hash":"952764ba5be9edd84c82c82037e7598aff98d9d9a44faa1212cdcb1cb8269413","nodeId":"a1347219-3e74-4918-a330-1c7b0b95530a","outcome":"completed","summary":"Review complete. The findings file `.imd-findings.json` holds four reproduced findings, none a deployment blocker.\n\n**Verification of the correction itself (all reproduced on this tree, pinned 0.8.26/Cancun/200 runs):**\n- The published protocol page states the completion tuple as `(bytes32 requestId, bytes32 resultHash, string uri)`, hash as the keccak-256 of the document bytes, and a 200,000-gas one-shot call inside a try. The contract registers `onJobResult(bytes32,bytes32,string)` with selector 0x325a77b4, stores the supplied hash exactly, and emits `JobDelivered` plus `JobResultURI`.\n- Independent canonical-encoding check: I hand-built the raw calldata (selector, head words, offset 0x60, length, padded bytes) without `abi.encode` or the mock, and it delivers under exactly 200k gas, byte-identical to the compiler's encoding. Intake-only authorization, unknown IDs, duplicates, malformed tuples, late delivery and preservation of a finalized NotPassed review all hold.\n- Reruns: Foundry 252/252 on both the repository seed and the assignment seed with gas report, isolated cold-gas 9/9 (67,052 gas for the 4,096-byte URI, oracle callbacks unchanged), runtime 24,117 bytes (459 below EIP-170), node 18/18, Python SDK 10/10, fmt clean, vendored checksums 96 verified. The evidence logs in docs/checks/job-protocol-tuple/ match these numbers and the RED trace's old selector 0x5dd4c70b is correct.\n- No regression in payment accounting, price caps, replay protection or listing/ad escrow: the source diff touches only `onJobResult` and the new event, and the escrow invariants passed 128,000 calls each.\n\n**Findings kept (duplicates merged):**\n- Low: the status page passes bidi override and zero-width code points in the URI through to the display unescaped, so a writer-chosen URI can visually read as another location. Reader-only; reproduced with the page's own vm harness.\n- Info: the URI delivery ceiling under the 200k stipend is about 20,224 bytes cold; larger URIs burn the single attempt and the fee. Already disclosed in the docs, and protocol URIs are short.\n- Info: both protocol citations link `#what-comes-back`, which does not exist on the page; the real anchor is `#intake-what-comes-back`.\n- Info, reported separately from contract behaviour: the SDK test helper reads the receipt once with no wait, which fails on any non-automining node. It passes under the suite's own automine Anvil and is pre-existing code.\n\n**Disclosed owner powers (unchanged, trust assumptions, not defects):** the owner can rotate the Intake and signer for pending asks, bind, reset, discard and revoke review receipts, confirm expiry, set templates, payment and timeouts, and sweep tokens.\n\n**Still requiring a live pilot:** that the live writer calls the registered selector with this tuple and real request-ID routing, actual result-document hash and URI availability and length, exact Intake spending and refusal shape, the authenticated Intake-to-oracle mapping and canonical question, and production signer gas under 200k. Local tests do not prove a working paid end-to-end service.","treeHash":null,"usage":{"cachedInputTokens":1366319,"inputTokens":388,"model":"claude-fable-5-1","outputTokens":22064,"runtime":"claude","turns":33,"wallClockMs":486896}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7c0191a32541eb74","findings":[{"citation":"resolved","description":"decodeResultURI only enforces canonical ABI layout, a 32 KiB bound and valid UTF-8. Valid UTF-8 includes the Unicode bidirectional overrides (U+202A-U+202E, U+2066-U+2069), zero-width characters (U+200B-U+200F) and C0/C1 controls. The decoded string is assigned to td.textContent with no escaping and no CSS bidi isolation, so a URI chosen by whoever controls the Intake writer or the result document location can render visually as a different location than the bytes on chain. textContent prevents HTML/script injection (covered by the existing node tests) but not display reordering. The task asked for untrusted URI rendering to be examined; this is the remaining gap. Fix: before display, escape code points outside printable ranges (e.g. map C0/C1 controls, U+200B-U+200F, U+202A-U+202E, U+2066-U+2069 and U+FEFF to \\\\u{...} escapes) and/or style the details cell with `unicode-bidi: isolate; white-space: pre-wrap; overflow-wrap: anywhere`. Add a case to the 'history exposes URI only as unverified text' node test asserting the rendered textContent contains no raw control/bidi code points.","line":168,"path":"site/index.html","reproduction":"Input: a JobResultURI log whose string is \"ipfs://QmSafe/result.json\\u202E nosj.tluser/moc.live//:sptth\" (ABI-encode with `cast abi-encode 'f(string)' ...`). Load site/index.html's script in the same node vm harness used by test/requester-abi.test.cjs with window.ethereum returning that log and call history(\"\", addr, id, \"\"). Expected: the details cell shows the bytes unambiguously (escaped or bidi-isolated). Actual (verified with node v24): the cell's textContent is \"Result URI (unverified): ipfs://QmSafe/result.json\\u202E nosj.tluser/moc.live//:sptth\" with the raw U+202E retained, which a browser renders with the tail reversed so it reads as https://evil.com/result.json. The contract is unaffected; only the reader's display is.","severity":"low","snippet":"        details = \"Result URI (unverified): \" + (decodeResultURI(l.data) || \"(empty URI)\");","title":"Status page passes bidi/control code points in the result URI through to the display, allowing visual spoofing of the 'unverified' location"},{"citation":"resolved","description":"docs/IMD_INTEGRATION.md says an excessive URI can exhaust the stipend and that only a 4,096-byte URI was measured; the page's 32 KiB display bound is described as not a contract restriction. Isolated cold measurements on this commit (pinned 0.8.26/Cancun/200 runs) show a 16,384-byte URI still delivers under an exact 200,000-gas call while a 20,480-byte one reverts, leaving the job Open, the Intake attempt consumed and the fee spent. This is documented behavior and the protocol's own URIs (ipfs://CID/result.json or the plane's /intake-results/:id) are far shorter, so it is not a deployment blocker. It is recorded so the operator's pilot checklist can pin the actual ceiling instead of the display bound. No source change is proposed; optionally record the ceiling in the callback gas budget section.","line":337,"path":"src/imd/HookJobRequester.sol","reproduction":"State: an Open job from HookJobRequesterFixture._open(). Call from the Intake address with gas 200,000: onJobResult(id, keccak256('result document'), string(new bytes(N))) after vm.cool(requester), restoring state between trials. Expected per docs: unspecified. Actual: N = 4096, 8192, 12288, 16384 succeed (job Delivered); N = 20480 and 32768 revert (job stays Open). Run: `forge test --isolate --match-path test/scratch/UriGasCeiling.t.sol -vv`.","severity":"info","snippet":"        emit JobResultURI(requestId, uri);","title":"Measured URI delivery ceiling under the 200k stipend is between 16,384 and 20,480 bytes; the page's 32 KiB display limit and docs do not state it"}],"hash":"afe82110cf4165520b37052b0aeb53b41d38838182b14d7d03f98b095059b473","nodeId":"1e96012a-2c29-4f6c-9fbb-f9b943254a5b","outcome":"completed","summary":"Review complete. The findings file holds two entries, neither a deployment blocker.\n\n**Outcome.** The job-callback correction in `src/imd/HookJobRequester.sol` is minimal and correct. The source diff touches only the callback body and one new event, so payment accounting, price caps, replay protection, oracle callbacks and listing/ad escrow are unchanged, and the full suite confirms no regression.\n\n**Protocol encoding, verified independently of the mock.** The imd.fun docs publish `onResult` as selector 0x4374622e and `onFailure` as 0x5a0df71e. Recomputing those with cast from the types `(bytes32,bytes32,string)` and `(bytes32,uint8,bytes32,uint16,uint16,bytes)` reproduces them exactly, which fixes the parameter types without reference to this repo. The docs' `onOracleResult` selector 0x510379c7 also reproduces from the repo's Attestation struct, so the unchanged oracle tuple matches too. A scratch test with hand-built calldata from the ABI spec delivers through the registered selector 0x325a77b4, stores the supplied hash exactly, and emits JobDelivered and JobResultURI with canonical event data.\n\n**Reruns under pinned settings, all matching the recorded evidence:**\n\n| Check | Result |\n|---|---|\n| forge test, repository seed | 252 passed, 27 suites |\n| forge test, assignment seed | all passed |\n| isolated cold-gas tests | 9 passed, 67,052 gas for 4,096-byte URI, runtime 24,117 bytes |\n| node site/ABI tests | 18 passed |\n| Python SDK tests on local Anvil | 10 passed |\n| forge fmt --check | clean |\n| evidence patch vs real diff | identical file list, identical source hunk |\n\n**Protections confirmed by existing tests and my own.** Intake-only authorization, unknown IDs, duplicates, six malformed payload shapes, the removed legacy selector, late delivery before and after local expiry, and preservation of a finalized NotPassed review all behave as documented. Review-ask IDs cannot be settled through the job callback because their job status is None.\n\n**Findings written:**\n- **Low, site/index.html line 168.** The URI reaches the page as raw text, so bidirectional override characters such as U+202E render a different location than the bytes on chain. Verified in the same node harness the repo's tests use. Fix is escaping or bidi isolation in the details cell.\n- **Info, HookJobRequester.sol line 337.** Measured cold delivery ceiling under the 200k stipend lies between 16,384 and 20,480 URI bytes. Documented as unbounded, protocol URIs are short, recorded so the pilot checklist can pin the real number.\n\n**SDK receipt-wait, reported separately.** No race occurred. The Python harness reads the receipt immediately after sending, but it launches Anvil in automine mode where sends mine synchronously, so there is no contract failure and nothing to substantiate as a defect.\n\n**Disclosed owner powers, unchanged.** The owner can rotate Intake and signer for pending asks, bind and reset reviews, confirm expiry and revoke signer epochs. These are design trust assumptions, not bypasses.\n\n**Still open for a live pilot, not provable locally.** Live `job.open` action and body acceptance, request-ID routing by the real writer, result-document hash and URI availability, authenticated Intake-to-oracle mapping, and production signer gas under 200k. Local tests do not prove a working paid end-to-end service.\n\nNo repository files were modified. Scratch tests live under test/scratch/ and the findings are in .imd-findings.json.","treeHash":null,"usage":{"cachedInputTokens":1598302,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":28864,"runtime":"claude","turns":36,"wallClockMs":615628}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c9b660ee1938912","findings":[],"hash":"e3debdcc182715a58f4cb6e7478924d5156573d6e6d038185fd6abbb2068f8d7","nodeId":"2ef79ab4-f11c-4307-a581-6738836e60b6","outcome":"completed","summary":"I found no defects in commit `b157921` that I could reproduce, so `.imd-findings.json` holds an empty list. That means nothing broke in the checks I ran. It does not prove the paid end-to-end service works, because no live Intake call was made.\n\n**The callback change matches the protocol**\n- The docs give a job result callback as `(bytes32 requestId, bytes32 resultHash, string uri)`, with `resultHash` being the keccak-256 of the result document. The contract now matches this at `src/imd/HookJobRequester.sol:331-338`. The function name is up to the contract, and the docs say only the parameter types are fixed.\n- I checked the identifiers with `cast` rather than trusting the repo's own tests:\n\n  | Item | Value |\n  |---|---|\n  | `onJobResult(bytes32,bytes32,string)` | `0x325a77b4` |\n  | old `(bytes32,bytes)` selector | `0x5dd4c70b` |\n  | `JobResultURI` event topic | `0xb702…0b` |\n  | `JobDelivered` event topic | `0xcf35…f3` |\n\n  `openJob` registers `0x325a77b4` as the callback (line 312).\n- I built the callback calldata with `cast calldata`, not the mock Intake, and sent it from the Intake address with the contract cold and a 200,000-gas cap. It succeeded, stored exactly the supplied hash (`keccak256(\"result document\")`), and marked the job Delivered.\n- The previous bug was real: running the protocol-tuple test against the base commit fails, because the old `(bytes32,bytes)` callback can't decode the real tuple.\n\n**Permissions and edge cases**\n- **Who can call it:** only the configured Intake (`onlyIntake`). The callback makes no outside calls, so it can't be re-entered.\n- **Unknown or wrong IDs:** a result for an ID the contract never opened is refused. So is a job result aimed at a review request's ID; I added a test for that.\n- **Duplicates:** a second result or failure for the same job is refused.\n- **Malformed data:** bad payloads revert inside the Intake's single attempt. The job stays open and its requester can expire it.\n- **Late delivery:** a result can still arrive after the job has locally expired. Job status is only read in the two callbacks, `expireJob` and `requestReview`, so a late delivery can't change a review. I tested it both while a review was pending (the NotPassed verdict still lands afterwards) and after a NotPassed verdict was final (it stays NotPassed).\n- **Unchanged areas:** the oracle callbacks, payment pulls, price caps, ID-replay protection, and all listing/ad code (`src/store`) have no changes in this commit.\n- **Status page:** the result URI is decoded with size limits and strict checks. It is shown only as plain text, never as a link, and never fetched.\n\n**Tests rerun under the pinned settings** (solc 0.8.26, cancun, optimizer 200)\n- **Foundry:** all 252 tests pass with the repo's fuzz seed. The run with the assignment seed also passed, including my 3 scratch tests.\n- **Site/ABI:** 18 of 18 node tests pass.\n- **Cold gas (isolated):** the numbers match the evidence README exactly. A job result with a 4,096-byte URI needs 67,052 gas; the worst oracle receipt needs 192,286, under the 200k cap.\n- **Contract size:** the runtime is 24,117 bytes, under the 24,576 limit. Both bytecode hashes match `docs/build-fingerprints.json`.\n- **Evidence directory:** applying `implementation.patch` to the base commit reproduces the commit's source, site, test and docs files byte-for-byte.\n\n**SDK receipt wait (separate from the contracts):** the Python test helper (`test/test_store_sdk.py:76-77`) asks for the transaction receipt immediately after sending, with no retry. It relies on Anvil mining instantly. It passed 23 out of 23 runs, so I couldn't reproduce a race and didn't file it. If it ever flakes, that's a test-harness problem, not a contract failure.\n\n**Owner powers (disclosed, not defects):** the owner can replace the Intake or the oracle signer even while requests are pending. After an Intake replacement, callbacks from the old Intake are refused, so those jobs can only be expired. The owner also controls revie","treeHash":null,"usage":{"cachedInputTokens":2011175,"inputTokens":54,"model":"claude-opus-5-5","outputTokens":16597,"runtime":"claude","turns":29,"wallClockMs":447973}}],"verification":[]}