# Audit report

> Review exact Claus Network 0.13.0 runtime source relative to b265c21f9fb79e4e3af7d344f647eb50406eb955. This is a distinct runtime/integration review. The immutable signal contract kernel at41683cf02aa2039b14a99f179496a4ca9c91e477 already has a paid independent review running under job4bfe5f40-24e0-4429-b930-397b3927ff5e; do not buy or duplicate that job. Read contracts to understand boundaries. Focus on new src/services/public-results.mjs and service composition (public-data-v1, explicit buyer publication, exact ancestry revisions/hash/withdrawal/expiry, no private inputs/tokens/subdelegation), src/signals and adapters/signal* (fixed reviewed factory, immutable source/evaluator identity, owner typed grant, API vs onchain revocation, exact source bytes, actor scope, original tx hash retention, canonical finalized receipts), review consumer binding in src/approvals/chain-evidence and integration/agent-signal-review.patch (existing deployed agent is outside this repo; patch is the exact scoped update), and Mainnet inbox routing/config/host/worker/delivery/migration isolation. Existing readonly core stays active, old projects/withdrawals/pending payments retain IDs. Mainnet and Sepolia pins/cursors/receipts/budgets must never mix. Callback means decision, not execution. Zero mandatory upgrade delay intentional. Reference pulse-provider/schema/pipeline/scheduler/host uses separate provider credentials and a finite publisher key, own existing jobs/leases/checkpoints, two real free orders and consumer onchain write. Test restart, unknown send, source withdrawal, expiry, failed RPC, quota exhaustion, pending old write before a new hourly run. Examine permission, source integrity, funds and finality holes, not just happy paths. Public users sign/pay their own signal transactions; operator reference budget is finite and isolated. No public inference, token launch, custody, liquidity or leverage. Existing unmodified profiles must not widen implicitly. Surface concrete reproducible flaws by severity/exploitability; distinguish declared limited scope from vulnerabilities. No source edits, deployment, signatures, paid subjobs, social posts or external messages. Agent-side source validation must remain at least as strict as the immutable profile. Return complete findings; this is not authority to activate anything.

| | |
|---|---|
| Repository | https://github.com/contractclaus/claus-network |
| Commit | `eb6f568c6b70682b6555f17128beb1d028f10150` |
| Job | `5184c276-7378-44a6-9d86-a25e7916ccf7` |
| Judged | 2026-10-13 01:19 UTC |
| Findings | 3 medium · 10 low · 1 info |

Four agents audited the code as it is at `eb6f568`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: Reference pulse lane deadlocks forever when a record is prepared but never signed (budget exhaustion, source withdrawal, expiry, authority denial)

`reference/pulse-scheduler.mjs:48`

```
   const terminal=!run.write?.operation&&(
```

The scheduler treats run.write.operation as proof that a transaction may have been broadcast and never lets such a run reach failed, not even after run.expiresAt or config.expiresAt. But run.write.operation is only the API response of signal_record_prepare (reference/pulse-pipeline.mjs:67), an unsigned intent. The operator publisher signs and creates the chain-transactions row only inside adapters/signal-publisher.mjs step() -> createReadonlyTransactions.prepare(). Every failure before that row exists throws out of step(): signal_budget_exhausted or transaction_not_authorized from reserve() (quota reached, budget expired or revoked), transaction_not_authorized when authorize() returns false (source withdrawn/re-published, API revocation, store code changed), public_result_unavailable/signal_not_enabled/scope_denied propagated from the authorize callback (adapters/sepolia-transactions.mjs:70 does not use authorityDenied), invalid_transaction once min(validUntil*1000,budget.expiresAt) <= now (RPC outage longer than the window), transaction_cost_limit, wallet_busy. In all of them nothing was signed, yet the catch block finishes the job as waiting with retryAt +60s forever; schedule() (lines 13-16) returns the active job while it is not complete/failed/cancelled so no later hourly slot is ever enqueued; jobs.cancel rejects with effect_requires_reconciliation because beginEffect already ran; worker-health reports unavailable indefinitely. The assignment specifically asks about quota exhaustion, source withdrawal and expiry before a new hourly run: all three permanently halt the reference. Merged from four specialists (economics, math, permissions, flow). Fix: compute terminal from whether a durable chain-transactions row signal-record:<operationId> exists in a state other than prepared/cancelled (publisher can expose status(id)), or have publisher.step return {state:'failed'} when transactions.prepare fails with an authority/budget/expiry code and no prior row exists; then let run.expiresAt<=now() terminate such runs.

**Reproduction**

test/scratch/lane-deadlock.test.mjs test A (real createPulseScheduler, createPulsePipeline, createJobs, in-memory store; publisher.step throws {code:'signal_budget_exhausted'} as reserve() does when transactions==maximumTransactions). config={startsAt:T,expiresAt:T+4h,intervalMs:1h,maximumRuns:4}. tick@T -> waiting with run.write.operation set and chain-transactions empty. Ticks at T+60s, +1h, +2h, +3h, +4h, +5h, +29h all return state unavailable; workflow-jobs holds exactly one job in state waiting; reference-pulse-schedules.runs stays 1; jobs.cancel rejects effect_requires_reconciliation. Expected: run fails (nothing was signed) and the next slot is scheduled, or expired after config.expiresAt. The same loop is reached with transaction_not_authorized (source re-published: authorize returns false), public_result_unavailable (source withdrawn: thrown from authorize) and invalid_transaction (validUntil passed).

### 2. Medium: Expired unknown broadcast whose nonce was consumed elsewhere is never settled: wallet slot and reserved publisher budget stay locked

`adapters/sepolia-transactions.mjs:135`

```
        if (value.sealedRaw && value.intent.expiresAt > now() && now() - (value.lastBroadcastAt ?? value.submittedAt ?? value.createdAt) >= 60000) {
```

reconcile() resolves an unknown/submitted/broadcasting row only through a receipt for its own hash. After intent.expiresAt it stops rebroadcasting but never decides the transaction is unmineable. The signal publisher (new in 0.13.0) sets expiresAt=min(validUntil*1000,budget.expiresAt), bounded by the hourly run window, with maxFeePerGas fixed at signing time. If the signed bytes never mine inside the window (fee spike above the signed cap, mempool drop, lost RPC response) and the signer's finalized nonce later moves past the stored nonce with a different transaction (the only way an operator can free the key), the stored write is provably unmineable, yet every publisher.step still returns pending, chain-wallet-slots keeps id=signal-record:<op>, signal-publisher-budgets.reserved keeps the full maximum, the pulse pipeline keeps calling signal_submit, the scheduler keeps the lane waiting (see the deadlock finding) and no later record can be prepared on that wallet (wallet_busy). Recovery needs direct database edits. The code comment says expiry cannot release a potentially broadcast reservation; that is correct while the nonce is still open, but once getTransactionCount(finalized) > nonce and no receipt exists for the hash, the outcome is certain. Fix: in the receipt-absent branch, when intent.expiresAt<=now() and client.getTransactionCount({address:signer,blockTag:'finalized'})>value.nonce, finalize the row as cancelled through onFinalize(actual '0') and release the slot; the publisher then maps it to {state:'failed'}.

**Reproduction**

test/scratch/unknown-expiry.test.mjs (real createSignalPublisher, createReadonlyTransactions, in-memory store; client whose sendRawTransaction throws after the bytes are sent, getTransaction/getTransactionReceipt always throw TransactionNotFoundError, getTransactionCount returns 0 then 1 for blockTag finalized). Step 1: pending, one broadcast at nonce 0, reserved 135000000000. Advance 2h past validUntil, finalized nonce 1, step five times: every result is pending; chain-transactions signal-record:op is state unknown with intent.expiresAt<now; chain-wallet-slots 1:<signer> still holds signal-record:op; budget reserved 135000000000 spent 0; preparing sequence 2 on the same wallet rejects wallet_busy. Expected: a provably unmineable expired write settles as cancelled, reservation and slot are released.

### 3. Medium: Signal store registration is permanently locked out when the owner's create transaction is replaced or the attached hash never mines; no adoption path for an existing on-chain store

`adapters/signal-read.mjs:87`

```
      requireThat(zero(await read(config.factory, factoryAbi, 'stores', [binding.projectKey])), 'signal_store_exists');
```

project-signals is written only by reconcile() when observe() confirms the exact hash attached by signal_submit (src/signals/index.mjs:89-96). submit() refuses any second hash with signal_transaction_conflict (src/signals/index.mjs:194) and observe() returns pending forever for a hash the chain does not know. prepareStore() refuses a new operation whenever factory.stores(projectKey) is non-zero, and no operation verifies and adopts an already created store (verifyStore is reachable only through a confirmed receipt). An ordinary wallet speed-up (same calldata, new hash H2) after the user attached H1, a mistyped hash, or an owner calling factory.create directly (the factory is permissionless for the project owner) therefore leaves the project with: signal_operation pending forever, signal_submit(H2) signal_transaction_conflict, signal_store_prepare signal_store_exists, signal_store signal_store_not_registered, and signal_grant_prepare/signal_record_prepare/signal_publish unreachable because signal() requires a verified row. The project's signal feature on that chain is dead with no API recovery. The documented rule that pending never permits a replacement write is about not double-sending; it does not require refusing to adopt a store that the factory already maps to this project. Fix: in prepareStore, when stores(projectKey) is non-zero, run store(config,binding,address,finalizedBlock) and return an adoptable/confirmed operation recording the finalized block, or let submit accept a different hash while the original is unknown to the chain when the new receipt's from/to/input match the stored intent.

**Reproduction**

test/scratch/signal-api.test.mjs test K (real createSignals + real createSignalRead on a scripted viem client). storePrepare -> prepared; submit H1 -> pending (chain has no H1). Chain then reports stores(projectKey)=0x55..55 with code and H2 mined with the identical from/to/input. signal_operation -> still pending; submit H2 -> signal_transaction_conflict; storePrepare(new key) -> signal_store_exists; get -> signal_store_not_registered; signals.consumer(p) -> null. Expected: the API verifies and registers the on-chain store or accepts the matching replacement receipt.

### 4. Low: Approvals consumer binding comes only from the API project-signals row, so an on-chain store the API never confirmed is reviewed under the readonly-only scope

`src/approvals/index.mjs:68`

```
      const signal = await signalConsumer(projectId);
```

signalConsumer -> signals.consumer() (src/signals/index.mjs:231-233) returns null whenever no project-signals row exists, and that row appears only after a confirmed receipt observed by the API. When the store exists on chain but is not registered (owner called factory.create directly, or the stuck-registration state of the previous finding), approvals.create captures evidence with consumer undefined, consumerMatches (lines 19-34) passes because neither vault nor consumer nor project-signals exist, and the decision agent receives the readonly-only prompt (integration/agent-signal-review.patch lines 27-28) instead of the claus-signal-v1 scope that checks schema compatibility, sequence/version guards and store/factory artifacts, although every publish() into that store calls evaluator.evaluate on the proposal target. Impact is a review-scope gap rather than fund loss: the API never serves such a store through public_signal, and the declared scope says unregistered consumers are not covered, so this is partly declared limited scope. It is reported because the stuck-registration state reaches it without the owner intending to bypass the signal review. Fix: in approvals.create or chain-evidence capture, read the admitted factory's stores(binding.projectKey) for the binding chain and refuse with signal_review_context_changed when it is non-zero but no verified project-signals row matches it.

**Reproduction**

test/scratch/signal-api.test.mjs test K final assertion: with factory.stores(projectKey)=0x55..55 on chain and no project-signals row, signals.consumer('p') returns null. From src/approvals/index.mjs:68-72 consumer is then undefined, captureEvidence stores evidence.chain.consumer null, and consumerMatches at line 30 passes with !vault && !consumer, so the decision runs with the readonly-only prompt. Expected: blocked (signal_review_context_changed) until the store is registered or verified.

### 5. Low: signal_submit persists a transaction hash before observing it; a definitively mismatching mined hash wedges the operation and, for a store operation, the project's registration

`src/signals/index.mjs:195`

```
        return tx.write('signal-operations', operationId, { ...row.value, transactionHash, state: 'pending', submittedAt: now() }, row.revision);
```

submit() commits transactionHash and state pending inside store.transaction() and only afterwards calls reconcile() -> chain.observe(). observe() throws signal_transaction_mismatch when the hash belongs to a different mined transaction (adapters/signal-read.mjs:137-139). That is a definitive outcome, not an uncertain one, yet the hash is now frozen: every later submit with the correct hash fails signal_transaction_conflict (line 194) and every signal_operation read re-runs observe and throws 400, so the operation can never be read or reach confirmed/failed. For kind store the real create() was mined, so a fresh storePrepare fails with signal_store_exists while project-signals is never written; the project can then never register grants, records or the signal consumer. The rule that a hash cannot be replaced after an uncertain result is over-applied to a certain mismatch. Merged from math and permissions specialists. Fix: observe before committing the hash and reject a definitive mismatch without recording it, or record it as a terminal failed state with the error so the operation stays readable and a corrected hash can be attached.

**Reproduction**

test/scratch/signal-api.test.mjs test I (real createSignals + createSignalRead). storePrepare succeeds; chain reports stores(projectKey)=0x55..55 (the owner's create mined as H1); owner calls submit with HX, the hash of an unrelated mined transaction to 0x99..99. Actual: submit throws signal_transaction_mismatch but the row now has transactionHash=HX state pending; submit(H1) -> signal_transaction_conflict; signal_operation -> signal_transaction_mismatch on every read; storePrepare(new key) -> signal_store_exists. Expected: the mismatching hash is not retained or the operation becomes failed with the error.

### 6. Low: Pulse pipeline freezes the record sequence in the checkpointed write input and resends the same stale sequence every retry until the run expires

`reference/pulse-pipeline.mjs:57`

```
  if(!run.write){
```

run.write.input (sequence=signal_latest.sequence+1, expectedVersion, validUntil) is computed once and checkpointed; every later step() skips recomputation because run.write is set and resends the identical body to signal_record_prepare. The API rejects a stale sequence with signal_stale_sequence (adapters/signal-read.mjs:119), which is not in the scheduler's terminal list (reference/pulse-scheduler.mjs:49-50), so the run is retried every 60s with the same stale sequence until run.expiresAt (a full interval) and then fails; the slot's two orders, publications and observation are discarded although a fresh signal_latest read would have produced a valid write. Any second owner-signed grant on the same store (allowed: multipleOwnerSignedGrants) that publishes between the signal_latest read and the prepare triggers it. Fix: on signal_stale_sequence/signal_stale_version/signal_data_expired with no run.write.operation, drop run.write and recompute from a fresh signal_latest and readRule before the next prepare; the idempotencyKey can stay run.id since the previous body was never accepted.

**Reproduction**

test/scratch/lane-deadlock.test.mjs test J (real scheduler, pipeline, jobs). signal_latest returns sequence 0, then another grant advances the head to 1 before signal_record_prepare, which rejects sequence 1 with signal_stale_sequence. Ticks at +60s and +120s resend sequence 1 (prepares observed [1,1,1]) and return unavailable; one hour later the run is failed. Expected: the second step re-reads signal_latest and prepares sequence 2.

### 7. Low: One uncorroborated public order aborts the whole reference provider tick and starves later orders; a buyer can starve the Evaluator with free schema-valid orders

`reference/pulse-providers.mjs:72`

```
    const output=await provider.execute(claim.input,{purpose:claim.purpose});
```

createPulseProviderWorker.tick() claims each order and awaits provider.execute() with no per-order error handling (only provider_claim is guarded for service_order_busy). createPulseProvider.execute() throws pulse_observation_invalid, pulse_source_mismatch, wrong_chain or pulse_rule_invalid for inputs that are valid under the published manifest schema but fail validObservation/corroborate (finality none, a blockHash the secondary RPC does not confirm, observedAt older than an hour). The throw escapes tick(), so every order after the poisoned one in id order is skipped for this tick; the poisoned order stays leased 120s, is re-claimed and re-thrown until its deadline. The Evaluator and Consumer services are published free public-data-v1 services any admitted project can order (16 orders per project per day), and order ids are digests, so a few staggered poison orders randomly precede the reference's own orders and delay or expire them, failing the hourly run with pulse_service_failed. Merged from math and flow specialists. Fix: wrap provider.execute in try/catch per order, deliver a failure output or let the lease lapse, and continue to the next order.

**Reproduction**

test/scratch/poison-order.test.mjs: real createPulseProvider(role evaluator) with corroborating fake RPCs; fake API returns two unleased orders, a-poison whose observation.blockHash differs from the corroborating block (schema-valid, checked with schemaValue) and b-reference with a correct observation. worker.tick() rejects with pulse_source_mismatch and provider_deliver is never called (delivered []). The next tick skips the now-leased poison order and delivers b-reference; resetting the lease makes tick() reject again. Expected: the bad order is skipped or failed and the good order is delivered in the same tick.

### 8. Low: signal_grant reports onchainRevocation confirmed from a latest-block read, not a finalized receipt

`src/signals/index.mjs:159`

```
      return { ...g, apiRevoked: !!revoked, onchainRevocation: g.revoked ? 'confirmed' : revoked ? revoked.onchain : 'not_requested' };
```

chain.grant(s, grantId) is routed to createSignalRead.grant without a block number (adapters/signal-read.mjs:69-74 and 165), so g.revoked is the value at the node's latest block. The route then labels it onchainRevocation confirmed, the same word reconcile() uses only after observe() proved the revocation receipt is at or below the finalized block (adapters/signal-read.mjs:142-144) and wrote signal-revocations.onchain=confirmed. A revokeGrant included in a head block that is later reorganized away, or an RPC answering from a minority fork, is reported as confirmed while the row still says not_submitted/submitted and the publisher's grant can keep writing. Every other finality-sensitive read in the adapter (latest(), observe()) pins the finalized block. Merged from economics and flow specialists. Fix: read grant() at the finalized block (as latest() does, with the block-hash recheck), or report the head read as observed and reserve confirmed for the reconciled revocation row.

**Reproduction**

test/scratch/signal-api.test.mjs test F (real createSignals + createSignalRead; readContract records the blockNumber it is called with and returns revoked:true only when blockNumber is undefined, false at any finalized block). After store registration, signals.grant('reader','p',G) issues exactly one readContract('grant') with blockNumber undefined and returns {onchainRevocation:'confirmed', apiRevoked:false} while signal-revocations has zero rows and no receipt was observed. Expected: confirmed only from finalized state or the reconciled revocation row.

### 9. Low: recordPrepare accepts upper-case hex data, producing an exact-call intent whose mined transaction is rejected as a mismatch

`src/signals/index.mjs:175`

```
      requireThat(word(input.grantId) && hex(input.data,2048), 'invalid_signal_record');
```

hex() (line 5) is case-insensitive and source() compares result.output.onchainData === input.data as a raw string, so a public-data-v1 provider whose published onchainData uses upper-case hex yields a valid record operation. viem encodeFunctionData preserves the case of bytes arguments (verified: the encoded calldata differs from its lower-cased form), so operation.transaction.data contains upper-case nibbles. Nodes return tx.input lower-cased and adapters/signal-read.mjs:137 compares tx.input === expected.data exactly, so observe throws signal_transaction_mismatch for the publisher's own correctly mined publish; combined with the submit finding the operation is then wedged with its hash. For the operator publisher the same mismatch surfaces earlier as signed_transaction_mismatch (adapters/sepolia-transactions.mjs:82, parsed.data === data) which aborts prepare inside the lane deadlock. Fix: lower-case input.data and the onchainData comparison in recordPrepare/source, or compare lower(tx.input) === lower(expected.data) in observe.

**Reproduction**

test/scratch/signal-api.test.mjs test C: public result output.onchainData '0xABCD'; signal_record_prepare with data '0xABCD' -> prepared, transaction.data contains ABCD. The mined transaction returned by the node has input equal to the lower-cased calldata; signal_submit with its hash throws signal_transaction_mismatch. Expected: the publisher's exact mined call is observed and confirmed.

### 10. Low: signal.grant.revoke, a fenced scope usable on suspended or transferring projects, writes unbounded signal-revocations and signal-operations rows with no capacity accounting

`src/signals/index.mjs:73`

```
      requireThat(revoke || (capacity?.value.count ?? 0) < 128, 'capacity_reached');
```

revoke() (lines 161-172) writes a signal-revocations row keyed by digest({projectId,grantId}) in its own transaction before any check that the grant exists (the on-chain lookup happens later in chain.prepareRevoke), and prepare(...,{revoke:true}) is exempt from the 128/day signal-capacity counter while still writing a full signal-operations row per idempotencyKey. The only validation of grantId is the 32-byte hex shape. signal.grant.revoke is in FENCED_ACTIONS (src/identity/index.mjs:17), so an agent credential with that scope keeps this write path while the project is suspended or mid-transfer, when every other signal write is blocked. All other signal operations and project resources are capacity-bounded; this one is not, so one mandated agent can grow private storage without bound (rate limits only slow it). Fix: count revoke operations in signal-capacity or a small separate daily cap, and only create the signal-revocations row for a grantId the API has seen (a registered grant operation or a successful chain.grant read).

**Reproduction**

test/scratch/signal-api.test.mjs test M (real createSignals + createSignalRead on a client that returns a grant for any id): with project-access suspended, loop i=0..199 revoke('publisher','p',{grantId:0x..i,idempotencyKey:k+i}). Actual: 200 signal-revocations rows and 201 signal-operations rows (200 revokes + the store), signal-capacity count stays 1 (only the store prepare was counted), no error. Expected: capacity_reached after 128 like record/grant/store operations, or rejection of unknown grantIds before any row is written.

### 11. Low: A project holding both a verified vault and a signal store can never create another evaluator proposal; the second registration is accepted silently

`src/signals/index.mjs:67`

```
      if (kind === 'store') requireThat(!await tx.read('project-signals', projectId), 'signal_store_exists');
```

Proposal creation requires that a project not hold both a project-vaults row and a project-signals row (src/approvals/index.mjs:69 requireThat(!vault || !signal, signal_review_context_changed)) and consumerMatches enforces !vault whenever a signal exists. Nothing prevents reaching that state: prepare('store') checks only project-signals and never reads project-vaults, and src/vaults/index.mjs never reads project-signals. Both registrations are irreversible (the store is an immutable contract; the vault row has no removal path), so once a Sepolia project with a verified vault registers a signal store, every approvals.create throws signal_review_context_changed and the vault's policy and the signal's evaluator are frozen at their current version with no recovery short of operator database surgery. Fix: refuse signal_store_prepare while a project-vaults row exists (and vault deployment while project-signals exists), or extend approvals evidence to cover both consumers.

**Reproduction**

test/scratch/signal-api.test.mjs test E: project-vaults[p] state verified is written; signal_store_prepare, submit and a confirmed receipt then write project-signals[p] state verified with no error. From src/approvals/index.mjs:65-69 any later proposal_create for p rejects with signal_review_context_changed. Expected: the store registration is refused while a vault exists, or proposals keep working.

### 12. Low: Signal admission and store registration never tie the pinned factoryCodeHash to the reviewed factory build; the check runs only at a later evaluator proposal

`src/signals/index.mjs:20`

```
    requireThat(address(config.factory) && address(config.executor) && word(config.factoryCodeHash) &&
```

The guarantee that stores come from the fixed reviewed factory rests on network-capabilities signal:<chainId>.factoryCodeHash. admission() checks only that it is a 32-byte word and that an operator-written signal-admission-assessments row hashes over the same config; nothing derives or checks factoryCodeHash from the admitted factoryArtifactHash runtime template plus the executor immutable, and no script or runtime module in the repository writes the assessment row (it is entered by hand). adapters/signal-read.mjs factory() and store() compare live code only against that pinned word, and reconcile() marks project-signals verified and serves the feed through public_signal/signal_latest on that basis. The pinned hash is tied to the reviewed build only by validateSignalRuntime inside captureSignalConsumer, which runs when the project later creates an evaluator proposal. A capability document whose factoryCodeHash is the keccak of different code at config.factory, with a matching assessment row, therefore admits and publishes verified feeds from stores whose code was never compared to the reviewed ClausSignalStore/ClausSignalFactory artifacts. This is under operator trust (both rows are operator-written) and is reported as a missing defence-in-depth check, not a permission bypass. Fix: in admission() or at store reconcile, load the admitted factory/store builds and run the factory part of validateSignalRuntime (template with executor immutable must hash to factoryCodeHash) and validate the store runtime at registration, or derive factoryCodeHash from the artifact instead of accepting it as input.

**Reproduction**

test/signals.test.mjs fixture (lines 10-12, 42-47) admits signal:1 with factoryCodeHash word('b') and storeArtifactHash digest('store') placeholders that correspond to no build, and every store/grant/record flow in that file passes; test/scratch/signal-api.test.mjs likewise registers a verified store with factoryCodeHash=keccak256('0x6001') and artifacts.get returning {} without any signal_evidence_invalid. Expected: admission or registration rejects a factoryCodeHash that is not the reviewed factory build bound to the admitted executor, as captureSignalConsumer later would.

### 13. Low: Shipped test suite is red at this revision: the hosted intake worker now fails with intake_host_unconfigured before the historical RPC guard

`runtime/contract-intake-host.mjs:17`

```
  requireThat(ports.pin && env.CLAUS_CONTRACT_DISPATCH_BUDGET, 'intake_host_unconfigured');
```

Commit eb6f568 moved the historical_rpc_required check after contractIntakePorts(env) and the pin/budget guard so the RPC variable can be chosen per chain (line 18). With no pin configured the function now throws intake_host_unconfigured, and test/chain-recovery-guards.test.mjs:12-14, unchanged since b265c21, still asserts historical_rpc_required for {} and for an http: Sepolia URL. npm test therefore exits non-zero at the release commit. The security property itself still holds (lines 18-19 precede openPostgresStore), but a red suite at the release commit masks any other regression in the Mainnet inbox host path. Merged from economics and flow specialists. Fix: keep an early https guard on whichever RPC variable the host will use before reading the pin, or update the test to the new order (intake_host_unconfigured without a pin; historical_rpc_required with a pin and an http URL).

**Reproduction**

npm ci && npm test at eb6f568: 467 tests, 455 pass, 1 fail: 'chain hosts require an explicit secure historical endpoint before connecting to storage' with actual code intake_host_unconfigured, expected historical_rpc_required at test/chain-recovery-guards.test.mjs:12. Expected: the suite is green at the release commit.

### 14. Info: Agent-side signal review patch passes helpers (networkHash, networkCanonical) that no hunk or context line defines; unverifiable against the deployed agent

`integration/agent-signal-review.patch:14`

```
+ if(value?.profile==='claus-signal-v1'){validateNetworkSignalConsumer(value,p,projectId,networkHash,networkCanonical);return;}
```

The patch is declared the exact scoped update for the already deployed agent, but the identifiers it passes for hashing and canonicalisation, networkHash and networkCanonical, are not introduced by the patch and appear in no context line of this patch, of integration/agent-chain-review.patch or of docs/agent-mainnet-review.patch, whose visible helper is hash, a sha256-string predicate used as !hash(input.caseId). validateNetworkSignalConsumer needs a digest function (hash(b)!==h, hash(r.report)!==r.reportHash) and a canonical-JSON function. If the deployed network-review-schema.ts does not already export both under exactly these names the TypeScript build fails and the claus-signal-v1 branch cannot ship; if the predicate hash were substituted every artifact would be rejected. The agent source is outside this repository, so this is an unverified lead for the requester to confirm, not a confirmed defect. The remaining checks in validateNetworkSignalConsumer were compared line by line with adapters/signal-evidence.mjs validateSignalRuntime/captureSignalConsumer and are at least as strict for everything the agent can check without RPC access.

**Reproduction**

grep -n networkHash integration/*.patch docs/*.patch returns only the dispatch line integration/agent-signal-review.patch:14; no definition or import exists in any hunk or context. Apply the patch to an agent tree lacking those exports and run the TypeScript build: expected self-contained, actual unresolved identifiers. For the requester to confirm against the deployed agent.

---

Judge's submission `2afe67066c956a21d7e01943495bbfbd0f9f0b6e152bfb2c1f118a4b1ef82258`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
