{"jobId":"7d1c7153-8e83-40de-b701-cd268a94ecab","repoUrl":"https://github.com/contractclaus/claus-network","baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","count":5,"submissions":[{"hash":"87a0fc814de3fe54507e120310525dc39e35b974d94d199e28ad4bc70983ee4c","nodeKey":"audit_judge","role":"review","attempt":1,"deviceKey":"3a40eaafbd83a6bc57b859dab02a7e0ae1fcd12ca7afee73c0d6c380b94178e9","seat":{"tokenId":"244","agentId":"51494"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":46,"runtime":"claude","inputTokens":802,"wallClockMs":671122,"outputTokens":34973,"cachedInputTokens":2867491},"baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","startCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete. The findings file holds four entries, each reproduced on commit a689ccf with scratch tests under test/scratch/, and no tracked file changed.\n\n**What I verified independently**\n\n- Full suite on this tree: 423 pass, 10 environment skips, 0 fail, matching the stated gate. I also built the contracts and ran the real anvil integration flow myself for chain ids 11155111 and 1, including the delta's hidden-execution-event and preserved-cursor checks. Both pass.\n- Delta items 2, 3 and 5 behave as described in the code and the regressions. Empty pages before the executor existed no longer read projects(), the binding code hash is read at the receipt block and must be nonempty, and a failed callback after a finalized resolve keeps phase, resolutionFinalized and the resolve hash without claiming delivery.\n- The ten specialist reports reduce to four distinct mechanisms. Four reports describe the same execution-recovery mechanism and were merged. Four describe the same intake endpoint default and were merged. The two remaining notes are kept as info.\n\n**Findings kept, ranked**\n\n1. **Medium, code defect in delta item 4.** The new version checks at chain-executor lines 250 and 257 assume only this approval can raise the target version. On the real contract, any still-valid Claus approval at the same fromVersion can be executed by anyone, and the owner can then cancel the newer nonce without touching the version. In that state the worker throws recovery_logs_inconsistent on every tick, never writes a cursor, never consults ApprovalCancelled, and the project's active proposal slot is locked until an operator edits the database. Liveness only. I reproduced it at the worker level with a mock RPC and confirmed the onchain preconditions with a Foundry test on the real executor.\n2. **Low, configuration default in delta item 1.** The intake signing host still defaults to the public Sepolia endpoint when the variable is unset, while the chain hosts refuse to start. I measured that endpoint today failing historical eth_getCode at about 20,000 blocks depth. The intake path pins reads to receipt blocks and resumes old cursors, so a stale cursor stalls silently.\n3. **Info.** A scheme-less URL escapes both guards as a Node TypeError instead of the documented codes. Fail-closed, no safety impact.\n4. **Info, classification note.** The code checks only the URL scheme. A pruned endpoint surfaces as a raw numeric error retried every 60 seconds without bound. This is the accepted operational prerequisite, recorded so the conclusions below are read with it.\n\n**Scope B conclusion.** No finding affects spend, custody, signing, authority or the documented one-attempt limits. Finding 1 is a real regression against the delta's own claim that genuine owner cancellation is handled, but it needs the owner's own cancel plus a stale-approval relay, and it only locks one project's upgrade path. Finding 2 is a deployment-time omission with a documented operator workaround. Both should be fixed before public B, with finding 1 the blocking one, since the lock has no operator-free recovery. The archive endpoint remains a required configuration, verified by the operator's sampled checks, not by code.\n\n**Prepared-C conclusion.** The same code defects apply to the mainnet profile, and the mainnet archive prerequisite is equally unverified by code. Nothing here changes the requirement for a separate exact human approval, and a favorable result never authorizes Mainnet activation.","createdAt":"2026-10-12T18:47:04.284Z","verdict":null,"findings":[{"severity":"medium","title":"Execution recovery treats any target version above fromVersion as missing evidence, so a genuine owner cancellation after another valid approval upgraded the target is retried every 60 s forever and t","path":"adapters/chain-executor.mjs","line":250,"description":"Delta item 4 adds two checks to execution recovery: validateEmpty (line 250) before an empty page is committed, and the finalized-head check (line 257) once the scan is complete. Both encode the invariant 'version(target) > proposal.fromVersion implies this approval executed, so a missing UpgradeExecuted log is lagging evidence'. That invariant only holds while this approval is the only thing that can raise the version. It is not: ClausApprovalExecutor.execute (contracts/src/ClausApprovalExecutor.sol:154-164) is permissionless after readyAt and accepts any unexpired Claus-signed approval whose fromVersion equals the current version, and cancelApproval (lines 176-182) consumes a nonce without touching the version. Two approvals for one project at the same fromVersion exist whenever an earlier queued proposal A0 became terminal in the database without being consumed onchain: its execute attempt reverted (state 'failed', line 217-218 returns terminal because the nonce is unconsumed) or its local validate failed with an authority-denied code such as proposal_stale, dependency_code_changed or executor_not_enabled (state 'blocked' via authority_or_window_closed, same lines). src/approvals/index.mjs:45 and :82 then allow a new proposal A1 at the same fromVersion, and A0's full Approval struct and Claus signature are public in the calldata of the worker's own queue() transaction. Sequence: A1 is signed and queued; anyone executes A0 (version 1 -> 2, UpgradeExecuted for A0's approvalHash); the owner calls cancelApproval(projectKey, A1.nonce). On A1's next tick line 235 sees executionNonces true, recoverChainEvent fetches the page for A1's approvalHash, it is legitimately empty, validateEmpty reads version 2 > 1 and throws recovery_logs_inconsistent before the cursor is written (adapters/chain-event-recovery.mjs:15 runs before the commit). If the page had already been committed, line 257 throws the same code at the finalized head. src/approvals/index.mjs:212 maps that code to state 'waiting' with a 60 s retry and no bound, so the same page is fetched on every tick forever. The chain-proposals row never becomes terminal, active-proposals keeps A1, and every approvals.create for the project fails with proposal_in_progress (lines 45, 82). jobs.cancel cannot clear it because the job's effect started when Claus queued A1 (src/jobs/index.mjs:50 requires effect not_started). The ApprovalCancelled(projectId, nonce) event that proves the cancellation is never consulted. Baseline 2b31b66 returned blocked/approval_cancelled here; the delta's own brief says genuine owner cancellation is checked, but it is checked only for the unchanged-version case. Impact: liveness and state accuracy only. No spend, custody, signing or authority effect; one project's upgrade path is locked until an operator edits the database, and the worker burns one RPC page per minute per stuck job. Preconditions need the project owner's own cancelApproval plus a permissionless relay of a stale approval, so it is not an outsider attack, but both actions are legitimate and documented. This is a code defect in the delta, not an archive-endpoint matter: it reproduces on a perfect archive RPC. Minimal fix that preserves the intended protection: when the nonce is consumed and the version exceeds fromVersion, query ApprovalCancelled with args {projectId} over the same finalized range and match the nonce (and/or UpgradeExecuted for the projectId without the approvalHash filter); a found ApprovalCancelled for this nonce is terminal approval_cancelled, a found UpgradeExecuted for another approvalHash explains the version step so the page may be committed, and only the absence of both remains recovery_logs_inconsistent. Merged from four specialist reports (audit_math, audit_economics, audit_flow, audit_permissions) describing the same mechanism at lines 250/257.","reproduction":"Worker level: test/scratch/judge-exec-cancel.test.mjs (node --test test/scratch/judge-exec-cancel.test.mjs). State: in-memory store with network-capabilities 'chain' enabled (profile claus-readonly-v1, chainId 11155111, executor 0xee..ee, approvalSigner = a generated key, executorPayer = signer); claus-signed-approvals 'proposal-A' holding a real EIP-712 Approval signature from that key over approvalMessage(p, evidenceHash) with p.fromVersion 1, nonce '12345'; chain-bindings 'project'; artifacts.get returns evidence.chain.blockNumber 100; no chain-transactions rows. Mock RPC: finalized head 200; getLogs always [] (no UpgradeExecuted for A's approvalHash); getCode '0x1234'; executionNonces true; target version() 1n below block 150 and 2n from 150 and at latest. Expected: execution.step returns {state:'blocked', error:'approval_cancelled'} (the control case in the same file, version fixed at 1n, returns exactly that on the first call). Actual on a689ccf: three consecutive execution.step calls reject with code 'recovery_logs_inconsistent', store.list('chain-event-recovery') stays [], getLogs is called with exactly ['UpgradeExecuted', 100n, 200n] on every call and never for 'ApprovalCancelled'. Contract level: test/scratch/JudgeStaleApproval.t.sol (cd contracts && FOUNDRY_TEST=../test/scratch forge test --match-path '*JudgeStaleApproval*'): on the real ClausApprovalExecutor two Claus-signed approvals with fromVersion 1 and nonces 1 and 2 are both queued; after 1 hour an arbitrary address executes the first (version becomes 2); the owner calls cancelApproval(ID, 2); executionNonces(ID, 2) is true, version stays 2, and execute of the second reverts InvalidApproval, so no UpgradeExecuted for its hash can ever exist. That is the exact chain state the worker mock encodes. Reachability of the stale approval: chain.validateProposal (adapters/chain-read.mjs:122-126) throws proposal_stale / dependency_code_changed, both in adapters/authorization.mjs authorityDenied, so a queued A0 whose dependency snapshot drifts before execute is returned as blocked at chain-executor.mjs:217-218 with its nonce unconsumed and its signature public.","snippet":"            requireThat(await versionAt(toBlock) <= BigInt(p.fromVersion), 'recovery_logs_inconsistent');","citation":"resolved","blocking":false},{"severity":"low","title":"The intake signing host silently falls back to the pruned public Sepolia endpoint when CLAUS_SEPOLIA_RPC_URL is unset, although its event cursor and receipt-pinned Inbox reads need the same historical","path":"runtime/contract-intake-config.mjs","line":14,"description":"Delta item 1 makes the controller and executor refuse to start without an explicit https CLAUS_SEPOLIA_RPC_URL (runtime/chain-worker.mjs:28-29, historical_rpc_required), and docs/operations.md (Historical RPC requirement section) states that the intake host 'uses the same Sepolia setting for its original event cursor' and that the endpoint must serve historical logs, eth_call and eth_getCode. The intake host does not enforce this: contractIntakePorts defaults to https://ethereum-sepolia-rpc.publicnode.com, and runtime/contract-intake-host.mjs:15 calls it with no further check, so a hosted intake deployment that omits the variable starts normally on the provider the 0.12.3 judge measured as pruned. The intake path is not latest-only: scan() (adapters/contract-intake.mjs:54-57) fetches Requested logs from pin.startBlock or the stored cursor forward, and observe() pins getCode, dispatcher() and requests(id) to each request's receipt block (lines 25-31, 91, 105). Measured today (2026-10-12, head 11,900,735) the default endpoint answers eth_getCode at block 11,880,702 (about 20,000 blocks deep) with error -32000 'historical state ... is not available' while the same call at 11,895,830 (about 5,000 deep) succeeds. So an intake host without the variable whose startBlock or cursor is more than roughly 10,000 blocks (about 1.4 days) behind head, or that ingests a Requested event that old, fails every tick: scan() or ingest() throws, runtime/contract-intake-worker.mjs records scanError and never saves the cursor, and no later request on that inbox is admitted until an operator notices. The hosted health row still reports state 'active', so contractIntakeHealth keeps answering public_readonly. This is a configuration-default defect in the delta (the requirement was enforced in one host and defaulted away in the other), not an archive-provider guarantee question and not a spend or authority issue. Fix: require CLAUS_SEPOLIA_RPC_URL with the same https check and historical_rpc_required code in contractIntakePorts, or at least in createHostedContractIntakeWorker, and drop the publicnode default; optionally map pruned-history RPC errors to a named scanError so the health row is diagnosable. Merged from four specialist reports (audit_math, audit_economics, audit_flow, audit_permissions).","reproduction":"test/scratch/judge-config.test.mjs, first two tests (node --test test/scratch/judge-config.test.mjs). Input: contractIntakePorts({ CLAUS_CONTRACT_INTAKE_PROFILE: JSON.stringify(validPin) }) with no CLAUS_SEPOLIA_RPC_URL. Expected: the same historical_rpc_required refusal that createChainWorker('executor', {}) gives (second test, passes). Actual: ports.enabled === true and ports.client.transport.url === 'https://ethereum-sepolia-rpc.publicnode.com'. Live boundary measured on 2026-10-12 at head 0xb5973f: curl -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6\",\"0xb548fe\"]}' https://ethereum-sepolia-rpc.publicnode.com returns {\"error\":{\"code\":-32000,\"message\":\"historical state 7fc5f85d... is not available\"}}; the same call at 0xb58396 returns a result.","snippet":"  const rpcUrl = env.CLAUS_SEPOLIA_RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';","citation":"resolved","blocking":false},{"severity":"info","title":"A scheme-less CLAUS_SEPOLIA_RPC_URL escapes the new configuration guards as a Node TypeError (ERR_INVALID_URL) instead of the documented historical_rpc_required / invalid_sepolia_rpc codes","path":"runtime/chain-worker.mjs","line":29,"description":"The guard evaluates new URL(value) before comparing the protocol, so a value that is a string but not a parsable URL (for example 'sepolia.drpc.org' or 'gateway.tenderly.co/public/sepolia', plausible copy-paste forms of the two endpoints the operator tested) throws TypeError [ERR_INVALID_URL] out of createChainWorker rather than the 'historical_rpc_required' code that test/chain-recovery-guards.test.mjs asserts for the empty and http:// cases. The identical construction is at runtime/contract-intake-config.mjs:15 ('invalid_sepolia_rpc'). The worker still fails closed, so there is no safety impact; only release tooling or code matching on error.code sees an unexpected exception. Fix: guard with URL.canParse(value) && new URL(value).protocol === 'https:' so every malformed value yields the documented code. From audit_math, reproduced.","reproduction":"test/scratch/judge-config.test.mjs, third test. Input: createChainWorker('executor', { CLAUS_SEPOLIA_RPC_URL: 'sepolia.drpc.org' }) and contractIntakePorts({ CLAUS_CONTRACT_INTAKE_PROFILE: validPin, CLAUS_SEPOLIA_RPC_URL: 'sepolia.drpc.org' }). Expected: rejection with code 'historical_rpc_required' (respectively 'invalid_sepolia_rpc'). Actual: both throw TypeError: Invalid URL with code 'ERR_INVALID_URL'.","snippet":"    new URL(env.CLAUS_SEPOLIA_RPC_URL).protocol === 'https:', 'historical_rpc_required');","citation":"resolved","blocking":false},{"severity":"info","title":"Required archive coverage is neither probed nor diagnosed by the code: a pruned or lagging endpoint produces an unbounded 60-second retry loop whose only trace is a raw numeric RPC error code in the j","path":"src/chain-deployments/index.mjs","line":190,"description":"Classification note for the B and prepared-C conclusions, not a delta defect. 0.12.4 resolves the prior Finding 1 by configuration: the chain hosts require an https CLAUS_SEPOLIA_RPC_URL and docs/operations.md tells the operator to verify historical eth_getLogs, eth_call and eth_getCode coverage before activation. The code checks only the URL scheme. It does not probe historical coverage at startup and does not map provider pruning errors (code 4444 'pruned history unavailable' or -32000 'historical state ... is not available') to a named state. Such errors fall through to this line (and src/approvals/index.mjs:212 for executions), are stored as result.error 4444 / -32000, and retry every 60 s with no bound while worker-health shows 'waiting'. Every receipt-pinned and page-end read added by 0.12.3 and 0.12.4 (projects()/getCode at relay receipt and page ends, version()/executionNonces at page ends and the finalized head, target code at the registration receipt) therefore depends entirely on the operator's endpoint selection. The operator's sampled checks at 50,000 and 500,000 blocks depth on two providers show availability on the day of the test, not a guarantee, exactly as the brief says. The same applies to the prepared-C CLAUS_MAINNET_RPC_URL prerequisite. This is an accepted operational prerequisite; it is recorded so the acceptance conclusion is read with it. Optional hardening: at worker start, read executor code and one projects() value at the oldest retained module receipt block and refuse to start with a coded error on failure; map 4444/-32000 to a named job error so a misconfigured endpoint is visible in worker-health. From audit_permissions, reproduced.","reproduction":"test/scratch/judge-pruned-rpc.test.mjs (node --test test/scratch/judge-pruned-rpc.test.mjs). State: registration job for a module with receipt block 80, head 30,000, mock RPC that throws Error code 4444 for any getLogs/getCode/readContract request below head-10,000. Expected: a diagnosable coded state naming the endpoint problem. Actual: three consecutive deployment.step calls reject with code 4444 and chain-event-recovery stays empty; the runner at this line is outside its blocked list for that code, so it records { state: 'waiting', retryAt: now()+60000, result: { error: 4444 } } and re-queues indefinitely.","snippet":"          result: { error: error.code ?? 'chain_execution_unavailable' } });","citation":"resolved","blocking":false}],"coverage":null},{"hash":"7d238e10cf7d8a8c02c93441eeb74936cc10edb6a67d385cda6f7f5c0169f035","nodeKey":"audit_permissions","role":"review","attempt":1,"deviceKey":"8a05ec1ea94411eaf038689a13f0d1dc66fd2d45ade68f8a7f21077e6a87085e","seat":{"tokenId":"694","agentId":"52686"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":35,"runtime":"claude","inputTokens":1090,"wallClockMs":781230,"outputTokens":48224,"cachedInputTokens":3643392},"baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","startCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete. The findings file at the repository root holds three entries, each tied to a concrete state, and no tracked file was changed. The scratch tests under test/scratch/ reproduce the first two on commit a689ccf.\n\n**What I verified**\n\n- The full node suite on this tree gives 423 pass and 10 skips, matching the stated gate.\n- With anvil and a fresh forge build, the real local-chain integration test passes on both chain ids with the relay path on, including the new hidden-execution-event step and cursor preservation.\n- Each of the five delta items does what the brief says: the chain hosts refuse to start without an https Sepolia endpoint, empty registration pages before the executor existed are skipped by a code check, the binding code hash is read at the receipt block and must be nonempty, execution recovery uses a fresh cursor with a version guard at page ends and at the finalized head, and failed callbacks carry phase, resolutionFinalized and the resolve hash through the worker into the stored record.\n- All seven prior findings are closed or explicitly accepted as documented limits.\n\n**Findings**\n\n- **Medium, code defect in the delta.** The new execution guard infers cancellation from the target version, which belongs to the target rather than to the approval. If the owner cancels an approval on chain and any other still-valid Claus-signed approval for the project is then executed, the version exceeds fromVersion with no event for the cancelled hash. Recovery throws on every tick, the cursor is never written, the proposal never becomes terminal, and the project's one active-proposal slot stays held. The owner cannot cancel the job because its effect already started. The executor emits ApprovalCancelled, so positive evidence exists for a fix. Reproduced in test/scratch/execution-cancel-recovery.test.mjs with a control case and a guard-preservation case.\n- **Low, configuration gap.** The intake signing host still defaults to the pruned public endpoint when the new variable is unset, while its ingest pins inbox reads to the request's receipt block. One event older than the retention window stalls the entire intake cursor on every tick. Reproduced in test/scratch/intake-rpc-default.test.mjs.\n- **Info, classification note.** The archive requirement is enforced only as a URL scheme check. A pruned endpoint still produces an unbounded 60-second retry loop with a numeric error in the job result. This is the accepted operational prerequisite, recorded so the conclusions below are read with it.\n\n**Scope B conclusion (Sepolia, finite sponsored reviews, bounded Inbox/callback delivery)**\n\nThe delta closes the previously blocking defects. One new code defect remains, the medium above. It affects liveness of a single project's upgrade path and has no spend, custody or authority effect, but it is permanent without operator intervention and is reachable through legitimate owner actions. I would fix it before public B activation. B otherwise depends on the operator installing and verifying an archive endpoint on every host, including the intake host, which the code does not check.\n\n**Prepared C conclusion (mainnet profile, preparation only)**\n\nThe same executor code serves chain id 1, so the medium finding applies there too. The mainnet client also lacks the cache setting the Sepolia client received, which I did not find to have a material effect. C remains preparation only. Nothing here authorizes deposits, token launches, public inference, new signer powers, automatic budget renewal or mainnet activation, and a favorable result on its own does not change that.","createdAt":"2026-10-12T18:35:41.474Z","verdict":null,"findings":[{"severity":"medium","title":"Execution recovery infers cancellation from the target version, so an owner cancellation followed by any other valid upgrade leaves the proposal non-terminal forever and holds the project's single act","path":"adapters/chain-executor.mjs","line":250,"description":"The 0.12.4 execution:v2 recovery treats 'nonce consumed, no UpgradeExecuted for this approvalHash, target version > proposal.fromVersion' as missing evidence (line 250 before an empty page is committed, line 257 again at the finalized head). That is correct for a lagging log index, but the version is a property of the target, not of this approval: ClausApprovalExecutor.cancelApproval(id, nonce) consumes the nonce without touching the version (contracts/src/ClausApprovalExecutor.sol:176-182), and any other still-valid Claus-signed approval for the same project at the same fromVersion can be executed by anyone afterwards (execute requires only readyAt, the signature and version == fromVersion; the signature is public in the queue calldata), which moves the version to fromVersion+1 with an UpgradeExecuted for a different approvalHash. Such a stray approval exists whenever an earlier queued proposal became DB-terminal without onchain cancellation, e.g. blocked by scope_denied after the owner revoked the agent credential that created it (src/approvals/index.mjs:205-210), or cancelled through job_cancel before dispatch. Once that happens the page that contains the other execution ends with version 2 > 1, validateEmpty throws recovery_logs_inconsistent, recoverChainEvent never writes the cursor (adapters/chain-event-recovery.mjs:15 runs before the commit), and src/approvals/index.mjs:212 re-queues the job every 60 s with no bound. The proposal stays 'executing', so active-proposals keeps it as the project's one active proposal and every new create() fails proposal_in_progress (lines 42-45, 79-82). The owner cannot clear it: jobs.cancel requires effect === 'not_started' (src/jobs/index.mjs:50) and the job began its effect when Claus queued the approval. No spend, custody or authority effect; the lock-out is permanent for that project until an operator edits the database. The executor already emits ApprovalCancelled(projectId, nonce) (ClausApprovalExecutor.sol:181), so cancellation has positive evidence. Minimal fix: in validateEmpty and at the finalized head, when the version exceeds fromVersion, scan the same range for ApprovalCancelled with this projectId and nonce (and/or UpgradeExecuted for the projectId without the approvalHash filter) and return blocked/approval_cancelled when the cancellation event is present; keep recovery_logs_inconsistent when neither this approval's execution nor its cancellation is in the logs. The scratch test's third case keeps that protection. This is a code defect in the delta, not an archive-endpoint configuration matter.","reproduction":"test/scratch/execution-cancel-recovery.test.mjs (node --test test/scratch/execution-cancel-recovery.test.mjs). State: network-capabilities chain enabled with a generated approvalSigner; claus-signed-approvals row for proposal A (nonce 12345, fromVersion 1) with a real EIP-712 signature; chain-bindings for the project; artifacts.get returns evidence.chain.blockNumber 100; no chain-transactions rows; finalized head 200. Mock client: executionNonces(projectKey,12345) true at latest and at every block >= 150; ApprovalCancelled(projectKey, 12345) log at block 150; UpgradeExecuted log at block 160 for a different approvalHash 0xff..ff (version 2); no UpgradeExecuted for A's approvalHash; target version() = 1 below block 160 and 2 from block 160. Expected: execution.step returns {state:'blocked', error:'approval_cancelled'} (the control case with no other upgrade returns exactly that on the first step). Actual on a689ccf: step throws code recovery_logs_inconsistent on every call (three calls in the test), chain-event-recovery stays empty, so the job is retried every 60 s forever and the proposal never becomes terminal.","snippet":"            requireThat(await versionAt(toBlock) <= BigInt(p.fromVersion), 'recovery_logs_inconsistent');","citation":"resolved","blocking":false},{"severity":"low","title":"The intake host still defaults to the pruned public Sepolia endpoint when CLAUS_SEPOLIA_RPC_URL is unset, although its ingest reads inbox state and code at the request's receipt block; one old event t","path":"runtime/contract-intake-config.mjs","line":14,"description":"Configuration gap, not a logic defect: the controller and executor hosts now refuse to start without an https CLAUS_SEPOLIA_RPC_URL (runtime/chain-worker.mjs:28-29), but the separately deployed intake signing host (runtime/contract-intake-host.mjs) keeps the hard-wired pruned provider as a silent default, and docs/operations.md says only that it 'uses the same Sepolia setting'. The intake path is not latest-only: ingest pins requests(id), getCode and dispatcher() to the request's receipt block (adapters/contract-intake.mjs:23-31, 91, 105) and the first scan after a new pin or a worker outage starts at the pinned startBlock or the saved cursor. On the pruned provider any event older than roughly 10,000 blocks (about 1.4 days on Sepolia) fails those reads with code 4444, which is not in the worker's deterministic-rejection list, so runtime/contract-intake-worker.mjs:118 sets scanError, breaks, and line 133 never saves the cursor; the same event is replayed every tick and no later request on that inbox is ingested. The hosted health row shows state 'active' with a numeric scanError, so contractIntakeHealth still reports public_readonly. Fix: require the variable in contractIntakePorts (or in createHostedContractIntakeWorker) exactly as createChainWorker does, and document the intake host in the historical-RPC checklist; optionally map pruned-history errors (4444, -32000 missing trie node) to a named scanError so the health row is diagnosable.","reproduction":"test/scratch/intake-rpc-default.test.mjs (node --test test/scratch/intake-rpc-default.test.mjs). Case 1: contractIntakePorts({ CLAUS_CONTRACT_INTAKE_PROFILE: <valid pin JSON> }) with no CLAUS_SEPOLIA_RPC_URL returns enabled: true and ports.client.transport.url === 'https://ethereum-sepolia-rpc.publicnode.com' (expected: refusal with a coded error, as createChainWorker('executor', {}) gives historical_rpc_required). Case 2: createContractIntakeWorker with an admitted binding, a chain.scan that returns one event and a cursor 10,000 blocks ahead, and an intake.ingest that throws Error code 4444 ('pruned history unavailable') as the live endpoint does for a receipt-pinned eth_call: three ticks each return scanError 4444 and afterBlock 10099, and contract-intake-cursors stays empty.","snippet":"  const rpcUrl = env.CLAUS_SEPOLIA_RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';","citation":"resolved","blocking":false},{"severity":"info","title":"Required archive configuration is not verified or diagnosed by the code: a pruned or lagging endpoint still produces an unbounded 60-second retry loop whose only trace is a numeric RPC error code in t","path":"src/chain-deployments/index.mjs","line":190,"description":"Classification note for the B and prepared-C conclusions. 0.12.4 resolves the prior Finding 1 by configuration: createChainWorker requires an https CLAUS_SEPOLIA_RPC_URL and the docs tell the operator to verify historical eth_getLogs/eth_call/eth_getCode coverage before activation. The code itself checks only the URL scheme; it does not probe historical coverage at startup, and it does not map provider pruning errors (viem RpcRequestError with numeric code 4444 'pruned history unavailable', or -32000 'missing trie node') to a named state. Those errors fall through to this line (and src/approvals/index.mjs:212-213 for executions), are stored as error: 4444 in the job result, and retry every 60 s with no bound, while worker-health shows 'waiting'. The receipt-pinned reads added by 0.12.3/0.12.4 (projects()/executor()/projectId()/getCode at the relay receipt block, projects()/getCode at every empty page end, version()/executionNonces at page ends and the finalized head) therefore depend entirely on the operator's endpoint selection; the operator's sampled checks at 50,000 and 500,000 blocks depth on two providers show availability on the day of the test, not a guarantee, exactly as the brief says. This is an accepted operational prerequisite rather than a code defect; it is recorded so the acceptance conclusion is read with it. Optional hardening: at worker start, read executor code and one projects() value at the oldest retained module receipt block and refuse to start with a coded error on failure; map 4444/-32000 to a named job error so a misconfigured endpoint is visible in worker-health.","reproduction":"State: any registration or execution job whose scan or receipt-pinned read reaches a block the configured endpoint does not serve (for the prior hard-wired provider, measured on 2026-10-12 as any block deeper than about 10,000; for the newly required endpoint, whatever its retention turns out to be). Expected: a diagnosable state naming the endpoint problem. Actual: runOne/runExecution record { state: 'waiting', result: { error: 4444 } } and re-queue at now()+60000 indefinitely; no cursor is written and the registration row stays queued so prepareRegistration answers registration_in_progress. The mechanism is the baseline code path; the prior job's scratch test test/scratch/judge-registration.test.mjs cases (2)-(4) reproduce it with a mock client that answers code 4444 below latest-10,000, and nothing in the 0.12.4 delta changes that path.","snippet":"          result: { error: error.code ?? 'chain_execution_unavailable' } });","citation":"resolved","blocking":false}],"coverage":null},{"hash":"c738bf9f7f1d3ee3a5270eea7ee161346c9dadae8eb81137db6303918e471882","nodeKey":"audit_math","role":"review","attempt":1,"deviceKey":"776c025db24644e83396ec3b43a4f8b9aa10b354c5b76483681c847e3c8e29bd","seat":{"tokenId":"507","agentId":"52685"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-fable-5-1","turns":54,"runtime":"claude","inputTokens":514,"wallClockMs":775851,"outputTokens":52459,"cachedInputTokens":1859692},"baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","startCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"The review is complete. No tracked file changed; the findings file and two scratch tests are the only additions.\n\n**Result: three findings, none blocking. One low code defect in the new execution recovery guard, one low fail-open configuration default in intake, one info boundary in the new URL check.** The 0.12.4 delta does close all five items it set out to close, and I reproduced the operator's gate locally.\n\n| Check | Result |\n|---|---|\n| Full node suite on this tree | 423 pass, 10 skipped, 0 fail |\n| Prior findings 1 to 5 from the 0.12.3 report | each addressed as described in the delta |\n| Scratch reproductions | 3 of 3 fail on this tree, control passes |\n\n**Findings written to `.imd-findings.json`**\n\n- **Low, execution recovery version check.** The new guard treats a target version above the proposal's starting version as proof of a missing event. That inference is false when the owner cancelled this approval onchain and a different Claus-signed approval advanced the version. The signature of that other approval is public in the worker's own queue calldata, and execute is permissionless. The stuck job then retries every minute forever, never reaches approval_cancelled, and every new proposal for that project is refused with proposal_in_progress. The owner's own cancel is required, so this is owner-inflicted liveness, not an outsider attack. The fix is to consult the ApprovalCancelled event for that nonce instead of inferring from the version.\n- **Low, intake endpoint default.** Controller and executor now fail closed without an archive endpoint. Intake silently defaults to the pruned public provider, while the new operations doc says it uses the same setting. Intake does read state at request receipt blocks and scans logs from its saved cursor. I measured the default provider today returning error 4444 at a depth of 20,000 blocks. An intake host lagging more than about 1.4 days cannot resume until someone sets the variable.\n- **Info, URL guard.** A scheme-less endpoint string escapes as a Node TypeError instead of the documented historical_rpc_required code. The worker still fails closed.\n\n**Scope B conclusion.** For finite sponsored reviews and noncustodial Sepolia readonly projects, I found no code defect in the delta that affects spend, custody, authority or signing. The registration, binding-hash and delivery-receipt fixes hold under the boundary cases I tried. The two low findings are liveness edges. The archive endpoint remains a required deployment configuration, not a code property, and the delta correctly refuses to run the chain hosts without one. The one-attempt delivery limits are documented and unchanged, and I treat them as accepted bounded limits.\n\n**Prepared C conclusion.** The same code serves chain id 1, so the version-check finding applies there too, with the same owner-triggered precondition. The mainnet client is built without the cache setting the delta added for Sepolia, which only affects a cached block number for a few seconds and has no correctness effect. Nothing here authorizes mainnet activation. A favorable result remains subject to the separate exact human approval and to verifying the chosen mainnet endpoint against the oldest retained receipt.\n\n**Not re-audited.** Unchanged source outside the delta, the Solidity contracts, and the sponsored review economics. Operator test evidence was reproduced, not accepted as independent acceptance.","createdAt":"2026-10-12T18:35:35.447Z","verdict":null,"findings":[{"severity":"low","title":"Execution recovery treats 'target version > fromVersion' as proof of a missing event, so a genuinely cancelled approval whose target was upgraded by a different approval never reaches approval_cancell","path":"adapters/chain-executor.mjs","line":250,"description":"Boundary x invariant seam in the 0.12.4 execution-recovery guard (delta item 4). The new validateEmpty (line 250) and the finalized branch (line 257) both encode the invariant 'version(target) > proposal.fromVersion implies this approval executed, so an empty page is missing evidence'. That invariant is only true while nothing else can move the version. It is false in a reachable state: the project's version was advanced by a *different* Claus-signed approval A0, and this approval A1 (same fromVersion) was cancelled onchain by the owner. A0's full Approval struct and clausSignature are public in the calldata of the worker's own queue() transaction, and ClausApprovalExecutor.execute is permissionless once readyAt passes, so anyone can execute A0 after the worker blocked A0 locally (e.g. authority_or_window_closed when the chain capability was briefly disabled between queue and execute). Sequence: A0 signed and queued by the worker, A0 blocked locally, owner creates A1 (allowed because A0 is terminal), Claus signs and the worker queues A1, a relayer executes A0 from the public calldata (version 1 -> 2), the owner calls cancelApproval(projectId, nonce(A1)) to tidy up (the only remaining owner action for an approval that can no longer execute). A1's next tick: line 235 sees executionNonces true, line 242 scans from the evidence block, the page is legitimately empty for approvalHash(A1), line 250 reads version 2 > 1 and throws recovery_logs_inconsistent before any cursor is written. src/approvals/index.mjs:212 maps that code to state waiting with a 60 s retry and no bound, so the same page is fetched on every tick forever, the chain-proposals row never becomes terminal, and src/approvals/index.mjs:45/82 refuse every new proposal for the project with proposal_in_progress. Even if the page were ever committed, line 257 throws the same code at the finalized head. The ApprovalCancelled(projectId, nonce) event that proves the cancellation is never consulted. Impact: liveness only (no spend, custody or authority effect); one project's upgrade path is permanently stuck until an operator edits the database, and each such job costs one executor tick per minute. The trigger needs the project owner's own cancelApproval, so it is owner-inflicted or an owner reacting to an opportunistic relay, not an outsider attack. The version comparison is the wrong discriminator; the definitive evidence exists onchain. Fix: when the nonce is consumed and the version is beyond fromVersion, query ApprovalCancelled with args {projectId, nonce} over the same finalized range (and/or UpgradeExecuted for the projectId without the approvalHash filter): a found ApprovalCancelled is terminal approval_cancelled, a found UpgradeExecuted for another approvalHash explains the version change so the page may be committed, and only the absence of both remains recovery_logs_inconsistent.","reproduction":"Scratch test test/scratch/exec-recovery-version.test.mjs (node --test test/scratch/exec-recovery-version.test.mjs). State: in-memory store with network-capabilities 'chain' {enabled:true, profile:'claus-readonly-v1', chainId:11155111, executor:0x11..11, approvalSigner:<address of key 0x11*32>, executorPayer, artifactProjectId, targetArtifactHash}; claus-signed-approvals 'proposal-1' holding a real EIP-712 Approval signature from that key over approvalMessage(p, evidenceHash) with p = {fromVersion:1, nonce:'5', target:0xaa..aa, executor:0x11..11, expiresAt:5000, ...}; chain-bindings 'project'; artifacts.get returns {proposal:p, proposalHash, evidence:{chain:{blockNumber:100}}}; no chain-transactions rows. Mock client: finalized head 200; getLogs always []; getCode '0x1234'; readContract executionNonces -> true; version -> 1n for blockNumber < 150 and 2n from 150 on (the other approval's upgrade). Control in the same file with version fixed at 1n: execution.step returns {state:'blocked', error:'approval_cancelled'} on the first call. Actual with version 2n: three consecutive execution.step calls reject with code recovery_logs_inconsistent, store.list('chain-event-recovery') stays [], getLogs is called with exactly [100n,200n,'UpgradeExecuted'] on every call and never with 'ApprovalCancelled', and the fourth call still rejects. Expected: {state:'blocked', error:'approval_cancelled'} (nonce consumed, no UpgradeExecuted for this approvalHash, ApprovalCancelled present).","snippet":"            requireThat(await versionAt(toBlock) <= BigInt(p.fromVersion), 'recovery_logs_inconsistent');","citation":"resolved","blocking":false},{"severity":"low","title":"The intake host silently falls back to the pruned public Sepolia endpoint when CLAUS_SEPOLIA_RPC_URL is unset, although its observe() pins getCode/eth_call reads to the request receipt block and its c","path":"runtime/contract-intake-config.mjs","line":14,"description":"Delta item 1 replaced the hard-wired pruned provider with a required archive endpoint for controller and executor (runtime/chain-worker.mjs:28-29 throws historical_rpc_required). The same delta made intake *optionally* use that variable and kept https://ethereum-sepolia-rpc.publicnode.com as the default. docs/operations.md (added in this delta) states 'The intake host uses the same Sepolia setting for its original event cursor', but runtime/contract-intake-host.mjs:15 calls contractIntakePorts(env) with no check, so a hosted intake deployment that omits the variable runs against the pruned provider with no warning. Intake does depend on historical data: adapters/contract-intake.mjs:91 calls verify(receipt.blockNumber) which does eth_getCode and eth_call dispatcher() at the request's receipt block, line 105 reads requests(id) at that block, and scan() (lines 54-57) fetches Requested logs from the saved cursor forward. Measured today (2026-10-12, head 11,900,702) the default endpoint answers eth_getCode at depth 20,000 with error 4444 'pruned history unavailable: requested 11880702, earliest available 11890626' while depth 5,000 succeeds; the prior report measured the same for eth_call and eth_getLogs. So an intake host that is more than about 10,000 Sepolia blocks (roughly 1.4 days) behind its contract-intake-cursors row, or that ingests a Requested event mined that long ago, fails every tick: scan() throws, runtime/contract-intake-worker.mjs:99 records scanError, the cursor is never advanced (and operations.md forbids discarding it), and no new request is admitted until the operator discovers the cause. Boundary: the operator-facing requirement is enforced in one host and defaulted away in the other. This is a configuration-default defect in code, not an archive-provider guarantee question. Fix: require CLAUS_SEPOLIA_RPC_URL in contractIntakePorts (or at least in createHostedContractIntakeWorker) with the same historical_rpc_required code, and drop the publicnode default.","reproduction":"Scratch test test/scratch/config-boundaries.test.mjs, first test. Input: contractIntakePorts({ CLAUS_CONTRACT_INTAKE_PROFILE: JSON.stringify(pin) }) with a valid pin and no CLAUS_SEPOLIA_RPC_URL. Expected: a thrown historical_rpc_required (the chain hosts' behaviour for the same omission). Actual: no throw, and ports.client.transport.url === 'https://ethereum-sepolia-rpc.publicnode.com'. Live boundary: curl -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getCode\",\"params\":[\"0x01a8d2e011dcb3fa65395e0343e1ab73d562b0f6\",\"0xb548fe\"]}' https://ethereum-sepolia-rpc.publicnode.com at head 11,900,702 returns {\"error\":{\"code\":4444,\"message\":\"pruned history unavailable: requested 11880702, earliest available 11890626\"}}; the same call at 0xb58396 (depth 5,000) succeeds.","snippet":"  const rpcUrl = env.CLAUS_SEPOLIA_RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';","citation":"resolved","blocking":false},{"severity":"info","title":"A scheme-less CLAUS_SEPOLIA_RPC_URL escapes createChainWorker as a Node TypeError (ERR_INVALID_URL) instead of the documented historical_rpc_required code","path":"runtime/chain-worker.mjs","line":29,"description":"Boundary of the new configuration guard. The requireThat argument evaluates new URL(value) before the protocol comparison, so a value that is a string but not a parsable URL (for example 'sepolia.drpc.org' or 'gateway.tenderly.co/public/sepolia', both plausible copy-paste forms of the endpoints the operator tested) throws TypeError [ERR_INVALID_URL] out of the guard rather than the 'historical_rpc_required' code the test chain-recovery-guards.test.mjs asserts for the empty and http:// cases. The worker still fails closed, so there is no safety impact; the only consequence is that the operator's release checklist and any code matching on error.code see an unexpected exception. The identical construction exists at runtime/contract-intake-config.mjs:15 ('invalid_sepolia_rpc'). Fix: wrap the URL parse (e.g. URL.canParse(value) && new URL(value).protocol === 'https:') so every malformed value yields the documented code.","reproduction":"Scratch test test/scratch/config-boundaries.test.mjs, second test. Input: createChainWorker('executor', { CLAUS_SEPOLIA_RPC_URL: 'sepolia.drpc.org' }). Expected: rejection with code 'historical_rpc_required'. Actual: rejection with TypeError: Invalid URL, code 'ERR_INVALID_URL'.","snippet":"    new URL(env.CLAUS_SEPOLIA_RPC_URL).protocol === 'https:', 'historical_rpc_required');","citation":"resolved","blocking":false}],"coverage":null},{"hash":"d2341080f9cef0af009dfafa53d50788d7ec1f9d3101200c72060f91b216a919","nodeKey":"audit_flow","role":"review","attempt":1,"deviceKey":"4ca9ed4f0937da89830a0ebc4138194d204c23116ac7ce5bf6be3985f50f0dc3","seat":{"tokenId":"801","agentId":"52220"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":43,"runtime":"claude","inputTokens":84,"wallClockMs":333748,"outputTokens":24812,"cachedInputTokens":3855543},"baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","startCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"I reviewed the 0.12.4 delta against `2b31b66` and found two low-severity defects, both written to `.imd-findings.json`. Neither affects spending or permissions.\n\nThe rest of the delta does what the brief says. Here is how each of the prior report's seven findings and the RPC item stand:\n\n| 0.12.3 finding | Status in 0.12.4 |\n|---|---|\n| 1. Hard-wired pruned endpoint | Fixed for controller and executor: they now refuse to start without an `https` `CLAUS_SEPOLIA_RPC_URL`, and the Mainnet setting is unchanged. Whether that endpoint really keeps old history is still a deployment check. The code can't confirm it. |\n| 2. Reads at a block before the executor existed | Fixed: a page ending before the executor existed is skipped without calling `projects()`, and the existing conflict checks remain. |\n| 3. Execution recovery advancing past a missing event | Fixed for its stated case, but it introduces finding 1 below. |\n| 4. Target code hash read at latest | Fixed: it is read at the receipt block and must be nonempty, otherwise the retryable `registration_state_unavailable`. |\n| 5. Callback failure without the resolve hash | Fixed: the record keeps `phase`, `resolutionFinalized` and the resolve transaction hash, an undelivered callback gets `dispatch_callback_not_delivered`, and the worker passes only those fields through. |\n| 6–7. One-attempt limits | Unchanged, and now documented as accepted limits. |\n\nThe full source gate gives 423 pass and 10 skips, matching your number. I did not rerun the real local chain1 and Sepolia flows.\n\n## Findings\n\n1. **Low – a real owner cancellation can lock the project's proposal slot forever** (`adapters/chain-executor.mjs:250` and `:257`).\n   - **Cause:** the new execution check assumes that if the target's version went up, this approval must have executed. But the executor contract raises the version for any valid queued approval with the same starting version, and an older signed approval stays usable on-chain for at least 24 hours after its local job ends.\n   - **Trigger:** approval P0 is queued and then blocked locally. Approval A is created and queued with the same starting version. Someone executes P0, which makes A stale, and the owner cancels A's nonce.\n   - **What happens:** every check throws the retryable `recovery_logs_inconsistent`, so A is retried every 60 seconds indefinitely. The job can't be cancelled because its effect already started, and `approvals.create` returns `proposal_in_progress` for that project permanently. The baseline correctly ended this case as `approval_cancelled`.\n   - **Evidence:** a scratch test with a real EIP-712 signature and a mocked RPC reproduced it. The control case (version unchanged) passes, and the failing case throws on all three ticks. The test source is in the finding.\n   - **Fix:** before refusing to treat this as a cancellation, check the version step against `ApprovalCancelled` events for A's nonce, or `UpgradeExecuted` events with other approval hashes.\n\n2. **Low – the intake host still quietly uses the pruned endpoint** (`runtime/contract-intake-config.mjs:14`).\n   - **What happens:** without `CLAUS_SEPOLIA_RPC_URL`, the signing intake host starts normally on publicnode, even though `docs/operations.md:116` says it uses the same setting. Its event cursor and receipt-block reads then fail on every tick once they are more than about 10,000 blocks old, for example on first start or after an outage longer than about 1.4 days.\n   - **Fix:** require the variable on the intake host, as the chain workers now do.\n\n## Conclusions\n\n- **B (Sepolia):** I found no blocking code defect. Finding 1 is a narrow liveness regression that needs an out-of-band execution of a stale approval. It should be fixed, but it does not touch spending, custody or permissions. Configuration is a separate requirement: install and verify an archive endpoint on the controller, executor and intake hosts (the intake host is finding 2).\n- **Prepared C (Mainnet readonly):** the same code ","createdAt":"2026-10-12T18:28:16.353Z","verdict":null,"findings":[{"severity":"low","title":"Execution recovery treats any target version increase as the missing event, so a genuine owner cancellation after another approval upgraded the target is never concluded and the project's active-propo","path":"adapters/chain-executor.mjs","line":250,"description":"0.12.4 item 4 adds validateEmpty (line 250) and a final check (line 257) to execution recovery: when an empty page ends at a block where target.version() > proposal.fromVersion, or the nonce is consumed at the finalized scan head while the version there exceeds fromVersion, it throws retryable recovery_logs_inconsistent. The assumption is that only this approval can raise the version. It cannot be assumed: ClausApprovalExecutor.execute increments version for ANY valid queued Claus approval whose fromVersion matches, and two signed approvals can share a fromVersion with different nonces (src/approvals allows a new proposal once the prior one is terminal locally; the prior approval's signature stays valid onchain until expiresAt, >= 24h, and is public once its queue tx was broadcast). Path: proposal P0 is queued onchain, then its job ends locally as blocked (e.g. an authority-denied code such as scope_denied/project_admission_paused at execute time -> authority_or_window_closed). The project creates proposal A with the same fromVersion=1, and A is queued. Anyone (e.g. the owner) executes P0 with its public signature: version 1 -> 2, A can no longer execute, and the owner calls cancelApproval(projectKey, A.nonce). A's worker (queue row confirmed) reaches line 235: executionNonces is true, no UpgradeExecuted log exists for A's approvalHash, every empty page ending after P0's execution has version 2 > 1, so validateEmpty throws on every tick and the cursor never advances; even when the scan completes, line 257 throws the same code. runExecution maps it to waiting with a 60 s retry indefinitely. Because A's queue tx was dispatched, jobs.cancel rejects with effect_requires_reconciliation, and active-proposals keeps A non-terminal, so approvals.create returns proposal_in_progress for the project permanently. Baseline 2b31b66 concluded blocked/approval_cancelled here (the delta's own stated goal also includes checking genuine owner cancellation, which it does only for the unchanged-version case). Liveness/state-accuracy only; no spend or authority effect. Fix: positively attribute the version change before refusing cancellation, e.g. when version > fromVersion query ApprovalCancelled(projectId) logs for A's nonce (or UpgradeExecuted logs for the project without the approvalHash filter) in the same range; a matching ApprovalCancelled, or an UpgradeExecuted with a different approvalHash that accounts for the version step, is conclusive cancellation evidence; only an unexplained version step should remain recovery_logs_inconsistent.","reproduction":"node --test test/scratch/exec-cancel.test.mjs (node:test; source appended below; mocks only the RPC). State: capability with a generated approvalSigner and a real EIP-712 Approval signature for proposal A (fromVersion 1, nonce 12345, evidence block 100); no chain-transactions rows; RPC truth: P0 (different approvalHash) executed at block 150 so version() is 1 before 150 and 2 from 150; cancelApproval for A's nonce at 160 so executionNonces is true from 160 and at latest; getLogs for UpgradeExecuted filtered by A's approvalHash returns []; finalized head 200. Expected: execution.step returns {state:'blocked', error:'approval_cancelled'}. Actual: three consecutive steps throw recovery_logs_inconsistent (no cursor written). Control test with version staying 1 returns blocked/approval_cancelled.\n\n--- test/scratch/exec-cancel.test.mjs ---\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { hashTypedData } from 'viem';\nimport { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';\nimport { openStore } from '../../src/storage/index.mjs';\nimport { digest } from '../../src/shared/index.mjs';\nimport { createChainExecutor } from '../../adapters/chain-executor.mjs';\nimport { approvalMessage, approvalTypes, domain } from '../../adapters/chain-read.mjs';\n\nconst executor = '0x' + 'e1'.repeat(20), target = '0x' + 'a7'.repeat(20);\n\nasync function fixture(t, { version }) {\n  const store = openStore(':memory:'); t.after(() => store.close());\n  const clausKey = privateKeyToAccount(generatePrivateKey()), payer = privateKeyToAccount(generatePrivateKey());\n  const cap = { enabled: true, profile: 'claus-readonly-v1', chainId: 11155111, executor, executorCodeHash: '0x' + '01'.repeat(32),\n    approvalSigner: clausKey.address, executorPayer: payer.address, artifactProjectId: 'p', targetArtifactHash: 'sha256:' + '0'.repeat(64) };\n  await store.transaction(tx => tx.write('network-capabilities', 'chain', cap, 0));\n  const p = { profile: 'claus-readonly-v1', chainId: 11155111, executor, target, projectKey: '0x' + '22'.repeat(32), fromVersion: 1,\n    fromCodeHash: '0x' + '33'.repeat(32), implementation: '0x' + '44'.repeat(20), toCodeHash: '0x' + '55'.repeat(32),\n    configuration: '0x', dependencySnapshotHash: '0x' + '66'.repeat(32), policyVersion: '0x' + '77'.repeat(32),\n    policyGeneration: 0, authorityGeneration: 0, clausGeneration: 0, mandateHash: '0x' + '88'.repeat(32),\n    nonce: '12345', notBefore: 1000, expiresAt: 999999 };\n  const value = { id: 'proposal-A', projectId: 'project', moduleId: 'm', proposal: p, proposalHash: digest(p),\n    reviewArtifactHash: 'sha256:' + '9'.repeat(64), inputArtifactHash: 'sha256:' + 'a'.repeat(64),\n    decision: { decision: 'approve', decisionHash: 'sha256:' + 'b'.repeat(64) } };\n  const evidenceHash = '0x' + digest({ proposalHash: value.proposalHash, reviewArtifactHash: value.reviewArtifactHash,\n    decisionHash: value.decision.decisionHash }).slice(7);\n  const message = approvalMessage(p, evidenceHash);\n  const typed = { domain: domain(executor), types: approvalTypes, primaryType: 'Approval', message };\n  const signature = await clausKey.signTypedData(typed);\n  await store.transaction(async tx => {\n    await tx.write('claus-signed-approvals', value.id, { proposalId: value.id, proposalHash: value.proposalHash,\n      decisionHash: value.decision.decisionHash, message, approvalHash: hashTypedData(typed), signature, signer: clausKey.address }, 0);\n    await tx.write('chain-bindings', value.projectId, { executor, projectKey: p.projectKey, target, verification: { state: 'verified' } }, 0);\n  });\n  // Onchain truth: an earlier stale Claus approval P0 (same fromVersion, other nonce/approvalHash)\n  // was executed by a relayer at block 150 (version 1 -> 2), then the owner called\n  // cancelApproval(projectKey, A.nonce) at block 160. No UpgradeExecuted log exists for A's approvalHash.\n  const client = {\n    getBlock: async ({ blockTag, blockNumber }) => ({ number: blockTag === 'finalized'","snippet":"            requireThat(await versionAt(toBlock) <= BigInt(p.fromVersion), 'recovery_logs_inconsistent');","citation":"resolved","blocking":false},{"severity":"low","title":"Intake host still silently falls back to the pruned publicnode Sepolia endpoint when CLAUS_SEPOLIA_RPC_URL is unset, though its original event cursor and receipt-pinned reads need the same historical ","path":"runtime/contract-intake-config.mjs","line":14,"description":"0.12.4 makes chain workers fail closed without an explicit archive endpoint (runtime/chain-worker.mjs:28, historical_rpc_required) and docs/operations.md:116 says the intake host 'uses the same Sepolia setting for its original event cursor'. contractIntakePorts, used by the signing intake host (runtime/contract-intake-host.mjs:15), instead defaults to https://ethereum-sepolia-rpc.publicnode.com, the endpoint the 0.12.3 judge measured answering error 4444 'pruned history unavailable' for eth_getLogs/eth_call/eth_getCode more than ~10,000 blocks (~1.4 days) behind head. The intake worker resumes from pin.startBlock (or its stored cursor) with eth_getLogs (adapters/contract-intake.mjs:54-57) and pins Inbox reads to each request's receipt block (verify(receipt.blockNumber), state(id, receipt.blockNumber), lines 91 and 105). So a host deployed without the variable, with a startBlock or cursor older than the pruning window (first start after the Inbox deployment block aged, or any outage longer than ~1.4 days), throws on every tick and never ingests the stored public requests; nothing tells the operator the configuration is wrong because the host starts normally. This is the same configuration hazard item 1 removed for controller/executor, left as an implicit default on the one Sepolia host that also signs. No spend or authority effect; retryable liveness only, and it disappears once the operator sets the variable as the docs instruct. Fix: in createHostedContractIntakeWorker require CLAUS_SEPOLIA_RPC_URL (https) the same way createChainWorker does, keeping the default only for the public API gateway, which does not resume historical cursors.","reproduction":"node -e \"import('./runtime/contract-intake-config.mjs').then(m=>{const p=m.contractIntakePorts({CLAUS_CONTRACT_INTAKE_PROFILE:JSON.stringify({chainId:11155111,inbox:'0x'+'11'.repeat(20),codeHash:'0x'+'22'.repeat(32),dispatcher:'0x'+'33'.repeat(20),startBlock:1,reviewProjectId:'p',reviewCaseId:'c',reviewArtifactHash:'sha256:'+'0'.repeat(64)})});console.log(p.client.transport.url)})\" prints https://ethereum-sepolia-rpc.publicnode.com, whereas createChainWorker('executor', {}) rejects with historical_rpc_required (test/chain-recovery-guards.test.mjs). Expected for the intake host: the same historical_rpc_required refusal. Actual: the host starts on the pruned endpoint; with a stored cursor/startBlock more than ~10,000 blocks behind head every eth_getLogs page answers 4444 and the tick throws each time.","snippet":"  const rpcUrl = env.CLAUS_SEPOLIA_RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';","citation":"resolved","blocking":false}],"coverage":null},{"hash":"abf9e8b45db8d66ed476cb85232d1b6e8518ef47d21f1ec337c26ee5522cb60a","nodeKey":"audit_economics","role":"review","attempt":1,"deviceKey":"e2a4a53638df3fc6dce8d6f323df7160f7f280da87173f0cb0e41c8f708c525f","seat":{"tokenId":"1778","agentId":"52215"},"outcome":"completed","accepted":true,"failureReason":null,"failureClass":null,"safetyRefusal":false,"usage":{"model":"claude-opus-5-5","turns":20,"runtime":"claude","inputTokens":38,"wallClockMs":192786,"outputTokens":13166,"cachedInputTokens":1221986},"baseCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","startCommit":"a689ccff06648d9d979cbc661a4b3d03dac65bf4","resultTreeHash":null,"bundleHash":null,"bundleBytes":0,"artifacts":[],"changedPaths":[],"allowedPaths":[],"dependencyBundleHashes":[],"verificationErrors":0,"summary":"I found two low-severity defects in the 0.12.4 delta and nothing higher. Both are recorded in `.imd-findings.json`. I couldn't run the 423-test suite: `node_modules` isn't installed, so every file fails on a missing `viem` package, and I didn't install anything into the tree. The first defect was reproduced by running the real recovery function against a mocked chain; the second is from reading the code and the docs.\n\n**Scope:** I read only the code that changed since 2b31b66 (`adapters/chain-executor.mjs`, `adapters/contract-delivery.mjs`, `runtime/chain-worker.mjs`, `runtime/contract-intake-config.mjs`, `runtime/contract-intake-worker.mjs`, and their tests). I traced into unchanged code only where those changes depend on it, and checked each change against the seven entries of the 0.12.3 report.\n\n**Status of the 0.12.3 findings**\n- **Archive RPC (medium #1):** the controller and executor now refuse to start without `CLAUS_SEPOLIA_RPC_URL` set to an `https:` address. This is a deployment requirement, not a code defect. The code only checks the URL scheme; it can't tell whether the endpoint actually serves old history. The operator has to confirm that before activation. The intake host doesn't enforce the setting (finding 2).\n- **Reading the executor before it existed (#2):** fixed. The empty-page check now looks for executor code at the end of the page first. The check for a registration hidden inside a page still holds.\n- **Empty code hash in the binding (#4):** fixed. Target code is read at the receipt block, and empty code stays a retryable `registration_state_unavailable`.\n- **Execution recovery with no empty-page check (#3):** a missing log can no longer be read as a cancellation. But the new check introduces finding 1.\n- **Lost resolve hash on callback failure (#5):** fixed. Failures keep the phase, `resolutionFinalized` and the resolve transaction hash, and \"delivered\" stays a separate state. The hosted worker only exposes those whitelisted fields.\n- **Allocation lapse and pause (#6, #7):** unchanged and now documented as accepted one-attempt limits, as you described. I don't count them as defects.\n\n**Findings**\n1. **Low — a genuinely cancelled approval can get stuck permanently (`adapters/chain-executor.mjs:250` and `:257`).** The new check assumes that if the target's version is above `fromVersion`, this approval's execution event must be missing. That breaks when an older Claus-signed approval with the same `fromVersion` is still valid onchain. Such an approval can be marked failed or blocked locally while its signature stays public in its queue calldata, and a new proposal is allowed once the old one is failed or blocked.\n   - **Trigger:** someone relays the old approval, then the owner cancels the new one.\n   - **Result:** every tick throws `recovery_logs_inconsistent` and retries after 60 s with no limit. The job can't be cancelled because its effect has already started. The project gets `proposal_in_progress` on every new proposal, indefinitely.\n   - **Reproduction:** three ticks of the recovery function all threw, and no cursor was written.\n   - **Fix:** only call it missing evidence when the target's implementation and configuration match this approval, or when the version bump has no event under a different approval hash.\n   - This happens even with a perfect archive endpoint.\n2. **Low — the intake host silently falls back to the pruned public RPC (`runtime/contract-intake-config.mjs:14`).** `docs/operations.md:116` says intake uses the same historical endpoint. But if the variable is unset, intake quietly uses the pruned publicnode provider. Its scan and its reads at each request's receipt block then stall once it falls about 11,000 blocks behind, roughly 1.5 days. The fix is to require the variable here too, as the chain workers do.\n\n**Scope B (finite sponsored reviews, readonly Sepolia projects):** no blocking defects. I'd call it acceptable once finding 2 is fixed or an archive endpoint is installed on ev","createdAt":"2026-10-12T18:25:55.202Z","verdict":null,"findings":[{"severity":"low","title":"Execution recovery treats any later target upgrade as missing evidence, so a genuinely cancelled approval is stuck forever and blocks the project's proposal pipeline","path":"adapters/chain-executor.mjs","line":257,"description":"The 0.12.4 guard (line 250 for each empty page, line 257 at the finalized head) concludes that version(target) > proposal.fromVersion means this approval's UpgradeExecuted log is missing. That only holds if this approval is the only thing that can bump the version. It is not. ClausApprovalExecutor.execute accepts any unexpired Claus-signed approval whose fromVersion matches, and anyone can relay it. An older approval A0 keeps working onchain after it is terminal locally. Two ways it ends up terminal: 'failed' from transaction_reverted, or 'blocked' from authority_or_window_closed when the local tx is cancelled. Its signature is public in its queue calldata, and approvals.create accepts a new proposal once the prior one is in 'blocked' or 'failed'. So a new approval A1 can be signed with the same fromVersion while A0 still works. If A0 is relayed and A1's nonce is then consumed by cancelApproval, A1 never reaches approval_cancelled. Every tick throws recovery_logs_inconsistent and no cursor is written. runExecution maps that error to 'waiting' with a 60 s retry, without bound. The job has effect='outcome_unknown', so jobs.cancel rejects it with effect_requires_reconciliation. The proposal stays non-terminal, so approvals.create returns proposal_in_progress for the project indefinitely. The owner cancelling the stuck approval does not help, because the scan logic itself produced the stuck state. This is a code defect in the new guard. It is not an archive-endpoint problem: it reproduces on a perfect archive RPC.","reproduction":"State: target at version 1. Proposal A0 (fromVersion 1) was queued onchain, then went locally terminal ('failed'/'blocked') before its expiresAt. Proposal A1 (fromVersion 1, nonce n1) was created and queued at block 100. A relayer executes A0 from its public queue calldata at block 160, so version becomes 2 and A1 can no longer execute. The owner calls cancelApproval(projectKey, n1) at block 170. Expected: A1's execution step scans, finds no UpgradeExecuted for A1's approvalHash, and returns {state:'blocked', error:'approval_cancelled'}, freeing the project for a new proposal. Actual: the first empty page [100..10099] runs validateEmpty, which reads version(10099)=2 > 1 and throws recovery_logs_inconsistent. The cursor never advances, and the same happens on every later tick. If the scan had already completed, line 257 throws instead, because version(finalized)=2 > 1. A standalone run of recoverChainEvent with the same validateEmpty: mock client with no logs, getCode '0x1234', version 2 from block 160. Three ticks gave 'tick 0 throws recovery_logs_inconsistent', the same for ticks 1 and 2, and 'cursor rows 0'. Fix: attribute a version change to this approval only when the target matches it. At the page end, require that implementation()==p.implementation and configuration()==p.configuration (and version==fromVersion+1) before calling it missing evidence. Alternatively, query UpgradeExecuted by projectId alone and accept a cancellation when the version bump has a different approvalHash.","snippet":"          if (consumed) requireThat(await versionAt(found.finalizedBlock.number) <= BigInt(p.fromVersion), 'recovery_logs_inconsistent');","citation":"resolved","blocking":false},{"severity":"low","title":"Intake host silently falls back to the pruned publicnode Sepolia RPC although its receipt-pinned reads and original event cursor need history","path":"runtime/contract-intake-config.mjs","line":14,"description":"0.12.4 makes CLAUS_SEPOLIA_RPC_URL mandatory for the controller and executor (chain-worker.mjs:28, historical_rpc_required). docs/operations.md:116 says the intake host uses the same setting for its original event cursor and that the endpoint must serve historical logs/eth_call/eth_getCode. contract-intake-config.mjs does not enforce this: if the variable is unset, it quietly uses the same publicnode endpoint that the 0.12.3 review measured returning error 4444 'pruned history unavailable' at about 11,000 blocks or deeper. The intake scan reads logs from the stored cursor (startBlock-1 at first activation). It also reads getCode, dispatcher() and requests(id) pinned at each request's receipt block (adapters/contract-intake.mjs:25-31, 91, 105). So an intake host deployed without the variable, or one that falls more than about 1.5 days behind, stalls on pruned-history errors and does not fail fast at startup. Scope: this is a configuration-enforcement gap. It is not a fund or authority issue.","reproduction":"Start the intake host with CLAUS_SEPOLIA_RPC_URL unset and a pinned startBlock about 20,000 blocks behind head (or let the cursor lag that far during an outage). Expected, matching the chain workers: startup fails with historical_rpc_required, as createChainWorker('executor', {}) does in test/chain-recovery-guards.test.mjs. Actual: contractIntakePorts builds a client on https://ethereum-sepolia-rpc.publicnode.com, and every scan/verify call at the old block fails with a pruned-history RPC error, so no Inbox request past that point is admitted until an operator notices. Fix: require CLAUS_SEPOLIA_RPC_URL here as well, with the same https check and error code, and remove the default.","snippet":"  const rpcUrl = env.CLAUS_SEPOLIA_RPC_URL ?? 'https://ethereum-sepolia-rpc.publicnode.com';","citation":"resolved","blocking":false}],"coverage":null}]}