Skip to content

feat: migrate bridge backend from Bridge Hub API to aggkit bridge service (+ bridge tracker UI, rc5) - #24

Draft
arnaubennassar wants to merge 49 commits into
mainfrom
feat/aggkit-backend
Draft

feat: migrate bridge backend from Bridge Hub API to aggkit bridge service (+ bridge tracker UI, rc5)#24
arnaubennassar wants to merge 49 commits into
mainfrom
feat/aggkit-backend

Conversation

@arnaubennassar

@arnaubennassar arnaubennassar commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What & why

This PR migrates dev-ui from the legacy Bridge Hub API to aggkit's bridge service through the new aggkit module in sdk #28. The migration retains the app's bridge, claim, tracker, partial-failure, wallet/devnet, and runtime-config work, while replacing Bridge Hub-specific configuration and clients with per-network aggkit APIs.

The PR also owns a portable GHCR image with runtime /config.json validation and documents its mount/config contract. Its production build intentionally does not enable NEXT_PUBLIC_E2E_ENABLED; LocalWalletProvider is consequently compiled out. The bridge-flow suite must therefore continue to use pnpm dev, while the built image is exercised by the dedicated container suite.

Hermetic v2 E2E

e2e.yaml now tests PR source, not a stale baked application: it checks out and builds the sibling SDK at SDK_REF 5680d837b168cd3b250110660332aa110eb88aae (SDK package 1.0.0-snapshot-5680d83), builds this PR's dev-ui image, runs the six container tests against that image, then starts the vendored devnet and serves the application with pnpm dev for the bridge flow.

The vendored snapshot is the current v2 artifact from kurtosis-cdk #929: nine services, every Compose image pinned by @sha256:<digest>, no aggkit-00X-bridge service, and agglayer-dev-ui-002 behind the optional devui profile. The workflow asserts the complete digest set against summary.json; it does not use a snapshot-<sha> tag override. Default Compose starts the eight backend services, while --profile devui adds the ninth app container.

The E2E job is green at the current PR head 07e25e933b33fd40de974d29f3a8a97604c65336: run 31792978666 completed in 11m23s, with all 12 bridge tests, 6 built-image container tests, and 4 preflight tests passing. The added built-image path accounts for roughly 1m53s of the timing increase; the full job remains hermetic and uses no repository secrets.

References and merge order

Testing

  • pnpm run check is clean.
  • The current PR rollup has seven successful checks, including Docker smoke/build, CodeQL, and the E2E run above.
  • The E2E workflow validates the runtime-configured shipped image separately from the pnpm dev bridge path, preserving the production-build NEXT_PUBLIC_E2E_ENABLED limitation as an explicit, tested contract.

arnaubennassar and others added 23 commits July 14, 2026 08:08
Swap the three REST call sites to the aggkit bridge service via the
`@agglayer/sdk` AggkitBridgeAggregator:

- Config: replace `bridgeHubApiBaseUrl`/`proofApiSuffix` with per-mode
  `aggkitBridgeApis` (map networkId -> url) + `NEXT_PUBLIC_AGGKIT_BRIDGE_APIS`
  env override; Zod schema + validator updated.
- Services (transactions/claimProof/tokenMetadata) rewritten as thin SDK
  wrappers preserving the hook-facing shapes; adapter maps aggkit fields
  to the stable `Transaction`/`TransactionStatus` types.
- Fix useClaimExecution: source isClaimed's deposit_count separately from
  the claim-proof leaf index (previously conflated).
- Per-network partial-failure notice in the activity view; ready-to-claim
  badge tolerates partial failure.
- Docs, .env sample, wrangler, and CI workflow env var updated to aggkit.
- Add scripts/kurtosisDevnetEnv.mjs and retarget the Playwright E2E suite
  to a local kurtosis + aggkit devnet (env-parameterized, testnet fallback).

Local SDK linked via pnpm-workspace override (file:../sdk) for development;
switch to the published @agglayer/sdk beta at release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…tus polling

- wallet.tsx: on connect, steer the wallet to the default source chain
  (DEFAULT_WAGMI_CHAIN) instead of leaving it on whatever it connected with
  (e.g. Ethereum mainnet), so the network add/switch prompt happens at connect
  time rather than only when a bridge is initiated.
- kurtosisDevnetEnv.mjs: set appModes.default = "devnet" locally (devnet-prep
  tool; committed config keeps testnet), and write the devnet chains' rpcUrl as
  the enclave CORS proxy paths (/l1rpc, /l2rpc) instead of the direct EL ports
  (which send no CORS headers). Direct ports are still used for the script's own
  host-side chainId/bytecode checks.
- useTransactions / useReadyToClaimCount: aggkit has no push and status is
  derived per fetch, so poll to keep the activity view live — fast (5s) while any
  loaded tx is non-terminal, slower idle (10s) otherwise so newly-submitted
  deposits still appear; ready-to-claim badge polls every 15s. Fixes pending
  spinners never resolving and L2->L1 deposits taking a long time to appear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
When an autoclaim service is expected to claim a route on the user's behalf,
avoid showing "Claim tokens" immediately once a deposit is READY_TO_CLAIM.

- config: optional per-route `autoclaim` block (l1_to_l2 / l2_to_l1 / l2_to_l2),
  each { expectedAutoclaim, waitForAutoclaimMs }, Zod-validated; app/config.ts
  applies defaults for any omitted route/field. Defaults: L1->L2 expected/60s,
  L2->L1 not expected, L2->L2 expected/120s.
- Route type derived from the tx's recording source + destination network
  (L1 == networkId 0).
- transactionListItem: for a READY_TO_CLAIM deposit ->
  * expectedAutoclaim false: show "Claim tokens" as before.
  * within the grace window: show "Waiting for auto claim, claim manually now"
    (the link runs the same claim action).
  * grace elapsed: show "Claim tokens" + "auto claim is taking more time than
    expected, you can claim manually instead".
- Grace measured from when the deposit is first observed READY_TO_CLAIM
  (persisted in localStorage so it survives refreshes); flips waiting->overdue
  via a one-shot timer. Pure route/gate logic unit-tested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4tXyxxSecCKHA4uqsiN81
…dation

Discovers L2 suffixes and the haproxy proxy service from a single `kurtosis
enclave inspect` call (with --l2-suffixes/--proxy-service escape hatches)
instead of hardcoding `agglayer-dev-ui-proxy-001`, which no longer exists in
the S5 bring-up (the live service is `-002`). Every chain id / bridge-contract
check now runs through the same haproxy route that ends up in config.json
(never a direct EL/aggkit port), and the aggkit health probe -- which 404s
through the proxy -- is replaced by the per-network `sync-status?network_id=N`
check, turning "networkId = Number(deployment_suffix)" from an assumed
convention into a verified fact per run. The script's own config.json write
is now formatted through the repo's prettier config so re-runs never
introduce formatting-only diff noise.

Adds chains<->aggkitBridgeApis cross-field validation
(config/configValidator.mjs) so a chain configured without a matching aggkit
backend (or vice versa) fails config validation loudly instead of silently
dropping rows for one network.

Updates the committed config.json: devnet's placeholder block now models two
L2s (DEVNET_L2_001/DEVNET_L2_002) instead of the single stale DEVNET_L2 entry,
and the autoclaim grace windows are retuned to the measured idle-enclave
figures (l1_to_l2 120s, l2_to_l2 300s) now that the destination-injected-leaf
gate lands READY_TO_CLAIM later than before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…of literal "all"

The status Dropdown maps its null "no filter" sentinel to the string 'all'
for display, but onSelect never mapped it back -- so selecting "All
transactions" sent status: 'all' all the way through to
services/transactions.ts, which compared it against every real
TransactionStatus value (never matching) and rendered "No transactions
found" even though the total-count stat was nonzero. Manual S10 validation
reproduced this against a live devnet aggkit backend before fixing it.

Also adds the claim-button/grace-note data-test-ids design.md already
specified (claim-tokens-button, claim-manually-now-button,
autoclaim-waiting-note, autoclaim-overdue-note) but that were never wired
up -- needed to drive the manual L2->L1 claim journey and will save S11
from re-deriving selectors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…im specs

Adds E2E_TO_CHAIN_ID/E2E_L2_CHAIN_IDS consumption and two new route-budget
timeouts to app/constants/e2e.ts, additive chain-selector/status testids
(Dropdown, from/to sections, TransactionStatusBadge), and page-model support
for explicit chain-pair selection (destination excludes source, from-then-to
ordering enforced).

New specs: l2-to-l2.spec.ts (L2-1->L2-2, autoclaimed end state, 300s budget)
and manual-claim.spec.ts (L2-1->L1, non-autoclaiming by config, real
click->CLAIMED, 600s ready + 150s claim budgets). Existing bridge specs
(smoke/native-bridge/token-selector/erc20-approve-bridge/claim-autoclaim) now
click both chain selectors explicitly instead of relying on config.json
defaults. claim-autoclaim.spec.ts's obsolete 0.36s-race comment is rewritten
to point at manual-claim.spec.ts.

preflight.spec.ts now iterates every aggkitBridgeApis entry with
sync-status?network_id=N (plus network_id=0 once) and drops the health-
endpoint check, which 404s through aggkit-proxy in this topology (verified
live against the devnet enclave).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…uous Close selector

manual-claim.spec.ts's L2-1->L1 withdrawal deterministically reverted with
LocalBalanceTreeUnderflow whenever it ran right after l2-to-l2.spec.ts in the
same enclave: claim-autoclaim.spec.ts credits L2-1's AgglayerBridgeL2
LocalBalanceTree with exactly one E2E_NATIVE_BRIDGE_AMOUNT (a claimed L1->L2-1
deposit), and l2-to-l2.spec.ts spends exactly one back out, netting to zero
before this spec's own withdrawal needs to spend from it. Confirmed via `cast
4byte-decode` against the eth_estimateGas revert data during S12 triage.
Spec now funds its own L1->L2-1 top-up deposit first, making the withdrawal
assertion independent of run order and other specs' side effects.

Also fixes a strict-mode-violating `getByRole('button', { name: 'Close' })`
(substring-matched both the Modal's own "Close modal" icon button and the
CTA's "Close" text) with `exact: true`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
Document the 2-L2 devnet setup, devnet script discovery flags, proxy URL semantics,
and E2E testing across multiple chains:

README.md:
- Clarify that kurtosisDevnetEnv.mjs discovers L2 suffixes automatically (default 001,002)
- Explain proxy URL semantics: same URL for multiple networkIds, routed via ?network_id=
- Add escape hatch flags: --l2-suffixes, --proxy-service, --enclave
- Document 2-L2 E2E-specific env vars (E2E_TO_CHAIN_ID, E2E_L2_CHAIN_IDS)
- Add per-spec breakdown table and testing notes for all devnet specs
- Clarify autoclaim latencies and certificate-cadence timeout risks
- Note shared wallet state accumulation across runs (benign for current suite)

docs/config.md:
- Clarify that devnet aggkitBridgeApis uses identical proxy URL under multiple keys
- Show RPC route map for haproxy (/l1rpc, /l2rpc*, /aggkitapi)
- Distinguish between devnet mode (proxy + auto-discovery) and mainnet/testnet modes
- Document manual setup steps for advanced use cases

These docs enable E2E testing of 2-L2 bridging with minimal setup overhead via the
automated devnet script, while documenting edge cases (certificate cadence, wallet state)
found during S12 testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…uplicate networkIds

S14 adversarial review. Four findings, each verified against running code rather
than inferred.

1. l2-to-l2.spec.ts had the SAME unfixed LocalBalanceTree dependency that commit
   5138771 fixed in manual-claim.spec.ts -- the debit side of the same ledger.
   Its L2-1->L2-2 native bridge spends L2-1's origin-network-0 credit, which is
   created only by a *claimed* inbound deposit, and it silently relied on
   claim-autoclaim.spec.ts having run first (true only because the filenames sort
   that way). Standalone, sharded, -g filtered, or after any rename that
   reorders, it reverts LocalBalanceTreeUnderflow inside eth_estimateGas and
   surfaces as an opaque waitForBridgeSuccess timeout. 5138771's own commit
   message named this spec as the spender, so the ledger was understood and only
   half fixed.

   The top-up is now a shared BridgePage.fundLocalBalanceTree helper (one
   implementation, documenting why the credit cannot be inherited);
   manual-claim.spec.ts moves onto it too, replacing its inline copy.

2. E2E_L2_TO_L2_CLAIM_TIMEOUT_MS default 300s -> 600s. l2-to-l2 failed on the
   first post-fix run at 300s; the enclave showed why -- the deposit's
   /l1-info-tree-index still 500'd ("not been included on the L1 Info Tree yet")
   at 321s, and aggkit-001's certificate 14 covering the deposit's block settled
   only at ~329s. The dominant term is source-side certificate settlement, not
   autoclaim, and it grows with enclave age plus any certificate already in
   flight -- which the new top-up deliberately creates. Sampled range across
   rounds is now 87s..330s. The old comment's premise was also wrong:
   MinimumNewCertificateInterval is a maximum-idle heartbeat, not a floor on
   certificate spacing.

3. partial-failure.spec.ts asserted "Total transactions:", which
   transactionsView renders only when totalCount > 0 over a wallet-filtered
   fan-out -- another accumulated-history dependency, failing on a fresh enclave
   or when run as the only project. Now accepts either settled branch of
   transactionList (populated summary or the empty state), which is what the
   partial-failure contract actually claims: the healthy network's query
   resolves rather than hanging.

4. configValidator: two chains in one app mode could share a networkId and pass.
   networkId -- not chain id -- keys aggkitBridgeApis and the SDK's per-network
   clients, so that shape collapses two chains onto one backend and merges one
   chain's rows into the other's, while satisfying both existing cross-field
   rules (every chain finds a key, every key finds a chain). This is the failure
   the rules exist to prevent, reached from the other side, and
   kurtosisDevnetEnv.mjs deriving networkId from the deployment suffix is exactly
   how it would arise. Added the check, two unit tests, and the rule table in
   docs/config.md. L1 chains (networkId 0) stay exempt.

Verified: pnpm run check green (validate:config, lint 0 errors/3 pre-existing
BigNumber warnings, typecheck, 28 unit tests -- up from 26). Against the live
2-L2 enclave, each affected spec run STANDALONE, which is the property that was
broken: l2-to-l2 passed in 6.0m, manual-claim in 6.8m, partial-failure in 8.0s.
config.json's devnet-script mutation was reverted, not committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…mi's store chain

useEnforceCorrectChain compared useChainId() (wagmi's app-store chain)
against the target before switching. With a real extension wallet
(observed with Rabby) the store chain diverges from the wallet's
per-dapp chain, so the guard skipped the switch and the subsequent
sendTransaction threw ChainMismatch before the wallet was asked to
sign — surfacing as a generic 'Transaction failed' for any L2-origin
bridge (the connect-time auto-switch pins the wallet to L1, so only
L1-origin sends happened to match). useAccount().chainId is the same
source the send path's live check uses, so guard and enforcement now
agree. Not caught by E2E: the mock wallet keeps both in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
The transaction modal intentionally collapses every failure into a
generic message, which made real-wallet failures undiagnosable — the
thrown error was neither displayed nor logged anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCzWpoQMLj1HTXksvLbmpG
…polling

Polls AggkitBridgeAggregator.getBridgeTracking per row, keyed by the
transaction's recording network + hash. Stops once tracking_status is
'finished' or the tracker gave up ('error' with bridge_status still
null); keeps polling through step-level errors and registered-state
regressions. Only mounted for currently rendered, non-CLAIMED rows at
the same 5s cadence as useTransactions, to stay under the proxy's
50 req/IP/s budget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ooltips

Non-completed transaction rows now mount a TrackerProgressBar (wraps
useBridgeTracking) below the row body: one dot per all_steps entry, filled
green when done, pulsing blue when inProgress, hollow when pending, red on
error, with a connector line filled up to the last done dot. A new CSS-only
Tooltip primitive (app/components/ui/tooltip.tsx) shows humanized step copy
from app/utils/trackerSteps.ts on hover. Renders nothing while all_steps is
still null (registered/giving-up) and nothing for CLAIMED rows, since the
hook disables its query for those.

Verified live against the cdk enclave: a fresh L1->L2 bridge shows 4 dots,
L2->L2 shows 7, and the parked L2-1->L1 WaitingClaim row shows 6, all
advancing over successive polls; CLAIMED rows confirmed bar-free across
multiple rows. Screenshots under
/home/brolygon/repos/plans/bridge-tracker/evidence-s7/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ils modal

Adds a TrackerDetail section to TransactionDetailsModal for any non-CLAIMED
row: overall tracking status + bridge typology, then a vertical timeline of
every all_steps entry (label, status, start/end dates, per-step result
detail -- GER/LER hashes, certificate id/status/settlement tx, claim tx --
and step-level ErrorStep as a non-terminal warning). Reuses
useBridgeTracking (react-query dedupes with the row's own poll) and
trackerSteps.ts's label helper. CLAIMED rows render no tracker section and
never poll (query stays disabled).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Covers useBridgeTracking's refetchInterval terminal/non-terminal gating
(finished, giving-up error, step-level error) plus trackerProgressBar and
trackerDetail's null-steps/CLAIMED-row rendering, per-typology dot counts,
tooltip copy, and per-step result detail -- using trimmed local copies of
the captured aggkit tracker fixtures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ition

useBridgeTracking disables its query once a transaction's status is
CLAIMED, but disabling a react-query query only stops future refetches --
it does not clear already-cached data for that query key. A row that
transitions live from non-CLAIMED to CLAIMED while mounted (as opposed to
loading already-CLAIMED, which never populates the cache in the first
place) kept serving its last-fetched, fully-done all_steps from cache, so
TrackerProgressBar/TrackerDetail kept rendering instead of disappearing as
intended. Found via the S10 E2E spec exercising this transition against
the live enclave.

Guard both components on transaction.status === 'CLAIMED' directly rather
than relying on data/steps becoming falsy. Added a regression test to each
component's suite that keeps the mocked hook returning the same finished
fixture across a rerender (simulating the stale-cache behavior) and
asserts the guard hides the bar/timeline anyway; confirmed both fail
without the guard and pass with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…modal detail

Two devnet-only specs against the live enclave: L1->L2 (4-step bar renders
in order, at least one dot reaches done, bar disappears once the row
completes) and L2-1->L2-2 (7-step bar, modal tracker-detail asserted
mid-flight against the route's real step labels, bar disappears on
completion). Mid-state dot transitions are recorded as test annotations
rather than asserted, since exact per-step timing races the tracker's own
5s poll interval; the modal-detail check lives on the L2->L2 leg because
its multi-minute window is what makes catching it mid-flight reliable
(L1->L2 can autoclaim in ~35s).

Adds page-model locator factories used by the new spec: getTrackerBar,
getTrackerStep, trackerDetail/getTrackerDetailStep,
openTransactionDetails/closeTransactionDetailsModal. Purely additive --
existing bridge specs are unaffected.

This spec is what surfaced the live-CLAIMED-transition bug fixed in the
prior commit; both tracker.spec.ts tests and the full chromium project
(14 tests) pass against the live enclave with that fix in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Add a "Bridge Tracking" section covering: what useBridgeTracking polls
and at what cadence, the progress-bar/detail components, the dot
color legend (done/inProgress/pending/error), the CLAIMED and
null-all_steps render-guard rules, the step_name -> label mapping
table from trackerSteps.ts, and the data-test-id list for E2E authors.
Also adds the tracker.spec.ts row to the "E2E Specs by Mode" table.

No CHANGELOG exists in this repo, so none is touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…or' per aggkit

aggkit v0.11.0-rc4 derives tracking_status from the step at step_index
(bridgetracker/domain/tracking_data.go), so a step in 'error' reports
tracking_status 'error' with bridge_status still populated -- the
synthesized step-error fixture wrongly kept 'running', which meant the
bridge_status-null conjunct of isTrackingTerminal's giving-up check was
never exercised (dropping it survived the whole suite). With the fixture
corrected, that mutation now fails the step-error polling test.

Also corrects the hook's step-error comment to match, and rewrites the
rate-limit budget comment to account for the unvirtualized infinite
scroll accumulating mounted rows across loaded pages (the previous
per-page math understated worst-case traffic).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
A query error (react-query exhausting its default retries on a
transient proxy blip or rate-limit burst) permanently stopped that
row's tracker polling until remount, silently freezing the bar for a
passively-watching user. Drop the error early-return so refetchInterval
falls through to isTrackingTerminal, which is already undefined-safe;
only genuine terminal states (plus CLAIMED via `enabled`) stop polling
now. Accepted trade-off: a permanently-failing request would poll
harmlessly every 5s instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Measured a live L2->L1 bridge on the cdk devnet enclave: the tracker's
WaitingClaim step (previously labeled "Ready — waiting for the
claim…") reliably fires ~22s before the row's READY_TO_CLAIM status
(and thus the "Claim tokens" button) actually becomes true. Root
cause is upstream in aggkit, not a dev-ui poll/cache bug: the
tracker's WaitL1SettledGER resolver reads the settlement tx's own L1
receipt directly, while READY_TO_CLAIM and the real claim-proof fetch
both depend on the origin bridge-service's separate, slower
L1-info-tree sync. The button gating is correct and unchanged; only
the misleading "Ready" copy is fixed, to "Finalizing claim data for
{destination}". See plans/bridge-tracker/tracker-vs-claim-lag.md for
the full measured timeline and root-cause writeup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…real project id

Derives WalletConnect's metadata.url from window.location.origin (SSR-safe
fallback to the production domain) instead of a hardcoded literal, so local
dev no longer trips AppKit's "metadata.url differs from the actual page
url" warning (console-triage.md row 7).

Also implements the plan's standing decision for a placeholder/empty
NEXT_PUBLIC_PROJECT_ID: rather than requiring a real WalletConnect Cloud
id, createAppKit now passes `basic: true` when isPlaceholderProjectId()
detects one, skipping AppKit's remote-config fetch and trimming its
modal-open asset prefetch (verified against the installed @reown/appkit
sources not to touch EIP-6963/injected wallet detection, which is what
this app's connect flow actually depends on). The remaining Reown/
WalletConnect calls that no public AppKit option can suppress stay
documented as environmental in console-triage.md.

Adds tests/bridge/console-hygiene.spec.ts, a Playwright regression spec
that fails on any unexpected console error/warning during the core
transactions journey, with an explicit allowlist citing each triage row
it covers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
…ix stale docs

- Vendor the apps-team CLAUDE.md gist as docs/team-standards.md (reviewed,
  benign) and replace the live-fetch directive with a pointer + revision hash.
- Graduate plans/bridge-tracker/bridge-ui-aggkit-proxy-deploy.md into
  docs/deployment.md: re-pin all aggkit links to v0.11.0-rc5, rewrite the two
  gotchas for rc5 behavior (#1782 fixed, #1783 unenforced-by-design), fix the
  dangling bridgetracker.md link, and add a Rollback section per layer.
  Link it from README next to the Configuration section.
- Rewrite the useBridgeTracking rate-limit comment: aggkit enforces no
  in-process limit (#1783); the load math is now framed as courtesy/capacity,
  not a hard budget.
- Document NEXT_PUBLIC_PROJECT_ID's optional/degraded-mode semantics in
  .env.example, README, and docs/config.md; drop it from the E2E-required
  list. Fix .env.production's placeholder so an unset build secret degrades
  gracefully instead of shipping an invalid project id.
- Replace dangling plans/-path and design.md/enclave-notes.md references
  (README, trackerSteps.ts, tracker.spec.ts, console-hygiene.spec.ts) with
  durable upstream issue links or self-contained comments.
- Cross-link the kurtosis guide's enclave-reset recovery section from
  README's Kurtosis devnet bring-up section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

…ailure cause

The old comment said "revert to a semver range once the aggkit SDK surface
ships to npm (tracked for S15)" but didn't mention that the file:../sdk
override is the direct reason `pnpm install --frozen-lockfile` fails on
GitHub CI (ENOENT scandir on the missing sibling ../sdk checkout) for both
the lint/typecheck/test and e2e workflows on PR #24. Rewrite the comment
to state that plainly and spell out the dependency ordering to clear it:
agglayer/sdk#28 merges -> a tracker-API sdk version is published via
workflow_dispatch -> swap this override to that published semver. Local
dev and local CI-equivalent checks are unaffected since a sibling ../sdk
checkout is present here; only GitHub's runners lack one. Comment-only
change, no functional effect: lint/typecheck/test (54 tests) stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
The comment's "first-match wins" caveat about L1 (networkId 0) colliding
with the SDK's pre-registered mainnet/Sepolia defaults was already
resolved upstream by sdk commit 8094f922 (consumer-registered chains now
win networkId collisions) -- confirmed fixed and consumed here via the
file:../sdk workspace dependency. The design-doc citation sweep
(ede8c0e) kept this caveat faithful-minus-citation rather than silently
rewriting its semantics; this follow-up updates the claim itself to
match current SDK behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GNMDLHYqUSAJ3qb2GKfK3
Move configuration from a build-time import to a runtime fetch, so one
prebuilt image can be repointed at any network by replacing config.json
at container start. Add the dev-ui's own Dockerfile and GHCR publishing.

Config is now fetched from /config.json at runtime instead of being
imported and baked into the bundle. app/config.ts becomes an accessor
facade over a module store; AppConfigGate resolves config before any
provider mounts, so wagmi/AppKit construction no longer happens at
import time. A shared validate-and-normalize path serves both browser
(fetch) and Node (fs) callers. Origin-relative aggkitBridgeApis values
are supported and resolved against window.location.origin.

The container is nginx:alpine serving a prebuilt static export (106MB,
~280ms to first response). config.json is served no-store while
_next/static stays immutable; the entrypoint validates a mounted config
structurally and fails fast, warning loudly when none is mounted.

Every absolute URL in config.json is constrained to http/https. zod's
.url() accepts any scheme, so a javascript: value in explorerUrl reached
window.open() and executed with the app's origin.

Until @agglayer/sdk publishes a version carrying the aggkit APIs, the
image builds the sdk from a pinned commit staged into the build context.
See plans/dev-ui-docker-ghcr/d2-adr-dependency-strategy.md for the
removal trigger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
A workflow_dispatch or release trigger is only registered once the
workflow file exists on the repo's default branch, so dispatching
docker-publish.yaml from a feature branch fails with:

  HTTP 404: workflow docker-publish.yaml not found on the default branch

This is separate from, and blocks earlier than, the already-documented
limitation that dispatch cannot target a bare commit SHA. PR-triggered
builds are unaffected because they run from the PR head, which is why
docker-pr.yaml worked while dispatch did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
arnaubennassar and others added 6 commits August 12, 2026 16:52
Restructure e2e.yaml to check out dev-ui into a subdir with a sibling
agglayer/sdk checkout pinned at SDK_REF (mirrors docker-pr.yaml's
parity-checked pattern), so pnpm-workspace.yaml's `file:../sdk` override
resolves and `pnpm install --frozen-lockfile` no longer fails by design.

Add config/config.ci.devnet.json (devnet-mode fixture, URLs already
pointed at the future vendored bundle's 127.0.0.1:8555 proxy) and
scripts/devnetReady.mjs (a kurtosis-CLI-free readiness gate replicating
kurtosisDevnetEnv.mjs's chainId/bridge-bytecode/sync-status probes against
fixed ports, upgraded to require is_active alongside is_synced). Add a
test.skip mode-gate to preflight.spec.ts so a devnet-less run skips
instead of failing.

Neither the devnet compose bring-up nor the literal env values are wired
into the workflow yet -- that lands separately, alongside the vendored
bundle it depends on.
Vendor tests/devnet/{docker-compose.yml,summary.json} from
0xPolygon/kurtosis-cdk's snapshot-devui.yml (run 31616965584, artifact
devui-snapshot-snapshot-d5dea8709c8d): a self-contained, anvil-based
11-service devnet (L1, 2 anvil L2s, agglayer, aggkit x2 + bridge sidecars,
aggkit-proxy, the CORS haproxy, dev-ui) with no bind mounts or volumes.
Repoint the compose file's default image references at the published,
pinned GHCR tag (ghcr.io/0xpolygon/kurtosis-cdk-snapshot-<service>:
snapshot-d5dea8709c8d -- all 11 packages public, verified pullable after
`docker logout ghcr.io`) instead of the run's local build tag. Document
the regenerate-and-bump procedure in tests/devnet/README.md.

Rewire e2e.yaml to actually use it: after install, bring the bundle up
(`docker compose ... up -d --wait`), gate on scripts/devnetReady.mjs,
then stamp in the devnet config fixture (`cp config/config.ci.devnet.json
config.json && pnpm run validate:config`) before either Playwright
invocation. Replace the secrets-backed env block with literal, public
devnet-fixture values (the E2E signer key is a well-known Kurtosis/
Foundry devnet key usable only against this job's own throwaway
containers; E2E_ERC20_ADDRESS is read from summary.json), drop
`environment: production` (nothing here needs an approval gate), and
raise timeout-minutes to 75 to cover the bridge suite's slow, variable
L2->L1 certificate-settlement leg. Add an always()-run devnet-logs dump
and teardown step, uploaded alongside the Playwright report.

Verified locally: anonymous pull of all 11 pinned images, `docker compose
up -d --wait` healthy in 24s, devnetReady.mjs green against the live
bundle, and both Playwright invocations (preflight full run, bridge
suite --list) resolve cleanly under the exact literal env this workflow
now sets. actionlint clean; no `secrets.*` references remain for any
devnet value.
A fresh actions/checkout of agglayer/sdk at SDK_REF has no dist/ (it's
gitignored -- the repo ships bun.lock and TypeScript source only), so
pnpm-workspace.yaml's `file:../sdk` override resolves to a package whose
package.json `main`/`exports` point at a directory that doesn't exist
yet. `pnpm install --frozen-lockfile` doesn't catch this (it only
symlinks node_modules/@agglayer/sdk -> ../sdk), but Next's dev server
then fails every route with "Module not found: Can't resolve
'@agglayer/sdk'", timing out Playwright's webServer wait.

This was invisible in every prior local rehearsal on this dev box
because its sibling sdk/ checkout already has a built dist/ from earlier
SDK development -- a genuine scratch clone (two fresh actions/checkout-
equivalent clones, no reused sibling state) is what surfaces it, exactly
the kind of CI-only gap plans/dev-ui-ci-snapshot-plan.md warns about.

Fix: add a bun-based build step for the sibling sdk/ checkout, mirroring
the Dockerfile's sdk-builder stage (`FROM oven/bun:1 ... RUN bun install
--frozen-lockfile && bun run build`) bit-for-bit so the two build paths
for this pinned SDK_REF never diverge.

Verified: a from-scratch two-repo clone (dev-ui + sdk, no shared state)
now installs, builds the sdk, brings up the vendored devnet, passes
devnetReady.mjs, and runs both Playwright invocations under the
workflow's literal env -- see plans/dev-ui-ci-snapshot/s13-evidence/.
The checkout subdir restructuring (S12) moved package.json to dev-ui/,
but pnpm/action-setup@v4 defaults to reading package.json from
$GITHUB_WORKSPACE root, which no longer has one -- the action failed
with "No pnpm version is specified." on the first live e2e.yaml run
against this branch (run 31620612259). Point it at dev-ui/package.json
(packageManager: pnpm@10.30.3), mirroring the node-version-file:
dev-ui/.nvmrc pattern already used for actions/setup-node below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
…ink the snapshot doc (S16)

README's Testing section gains a "CI-devnet quick start" subsection for the
vendored tests/devnet/ compose bundle (complements, not replaces, the live
Kurtosis bring-up above it). docs/config.md documents
config/config.ci.devnet.json's shape and purpose. tests/devnet/README.md
gains a cross-link to kurtosis-cdk's new anvil-devui-snapshot.md doc for the
bundle's topology/hazards/summary.json reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
Independent review pass over the vendored-devnet e2e work.

CI
- e2e.yaml: add an automated parity guard asserting the job's hand-copied
  E2E_* literals and the compose file's pinned snapshot tag match
  tests/devnet/summary.json. Nothing in the repo read summary.json
  programmatically, so a half-done bundle bump would have left the suite
  silently testing stale values. Same drift class -- and same treatment --
  as the existing SDK_REF parity check. Verified to pass on the current
  bundle and to fail on a mutated address.
- e2e.yaml: upload artifacts with `if: always()`. `!cancelled()` skipped
  the upload on a timeout cancellation, which is exactly the case the
  `if: always()` devnet log dump exists for -- the logs were collected
  and then discarded.
- e2e.yaml: `|| true` on the SDK_REF grep. Under `set -e` a non-matching
  grep aborted the assignment, making the friendly ::error:: branch
  unreachable.

Docs vs behavior
- e2e.yaml: the images are pinned by a mutable TAG containing a sha, not
  by a digest -- the comment claimed "immutable sha tag".
- e2e.yaml: the readiness-gate rationale was wrong. The aggkit-proxy
  image's own healthcheck already asserts is_synced && is_active for
  networks 0/1/2, so `up -d --wait` cannot return before that holds. The
  gate's real added value is the chainId/bytecode probes THROUGH haproxy,
  which no healthcheck covers. Boot-to-ready comments said ~24s; CI
  measured ~65s including the image pull.
- docs/config.md: config.ci.devnet.json differs from the committed
  config.json in exactly ONE key (appModes.default); the doc described
  three chain entries and a whole appModes block as additions that are
  already in the committed file. Also removed a stale note asserting the
  committed config.json defaults to "devnet" -- it defaults to "testnet",
  which the section immediately below it said.
- README.md: the local quick start could not work as written --
  playwright.config.ts throws without E2E_PRIVATE_KEY /
  NEXT_PUBLIC_PROJECT_ID / NEXT_PUBLIC_AGGKIT_BRIDGE_APIS, and without
  E2E_ERC20_ADDRESS globalSetup falls back to deploying its own ERC20 via
  `sudo docker run`. Added the exports and a warning that a stale
  .env.local overrides them via loadEnvConfig().
- tests/devnet/README.md: "exactly two files pulled verbatim" contradicted
  its own next paragraph; summary.json carries no digests and no
  historical_states; `down` -> `down -v`; `jq -er` so a null erc20_address
  fails instead of printing "null" to paste.
- devnetReady.mjs: the aggkit-proxy does read network_id; 0 and 1 simply
  map to the same upstream in this bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUseLAAcYiPPkqrUqFehtY
@arnaubennassar

Copy link
Copy Markdown
Contributor Author

Final layer: hermetic e2e CI

e2e.yaml now runs the full Playwright bridge suite on every PR against a vendored devnet snapshot — no live Kurtosis enclave, no devnet secrets, ~9.5 min end to end (budget: 75 min).

Before this, the workflow had no backend at all: it referenced secrets pointing at a devnet that doesn't exist in CI, pnpm install --frozen-lockfile failed by design (pnpm-workspace.yaml's file:../sdk override), and the suite only ran on a developer machine with a warm enclave.

What changed

  • Sibling checkout of agglayer/sdk at the pinned SDK_REF (+ parity check, mirroring docker-pr.yaml) so the frozen-lockfile install resolves — plus a bun install && bun run build step, because a fresh sdk checkout has no dist/ and Next then fails every route.
  • tests/devnet/ vendors the snapshot bundle produced by 0xPolygon/kurtosis-cdk#929 (docker-compose.yml pinned to snapshot-d5dea8709c8d + summary.json + a regenerate-and-bump README).
  • scripts/devnetReady.mjs — a readiness gate replicating kurtosisDevnetEnv.mjs's probes against fixed ports, with no kurtosis CLI dependency.
  • config/config.ci.devnet.json fixture (differs from the committed config.json in exactly one key: appModes.default).
  • All devnet env values are now literals in the workflow — no repository secrets.

Verified: 12/12 bridge tests green locally and in CI, across three independent CI runs. manual-claim genuinely exercises the manual path (L2→L1 is not autoclaimed), tracker gets the exact 4-step and 7-step sequences, console-hygiene and partial-failure (:3100) both pass.

Review notes

  • A parity guard now asserts the workflow's hand-copied E2E_* literals and the compose file's pinned tag match tests/devnet/summary.json — nothing read that file programmatically, so a half-done bundle bump would have left the suite silently testing stale values.
  • Artifact upload moved from !cancelled() to if: always(): the old guard skipped the devnet log dump precisely on timeout cancellation, which is the one case it exists for.
  • Images are pinned by a mutable tag containing a sha, not a digest. Pinning by @sha256: is a worthwhile follow-up.
  • ⚠️ The CI - lint / typecheck / test check is red, and that is pre-existing — it fails with ENOENT scandir '.../sdk' because the org-shared 0xPolygon/pipelines action does no sibling-SDK checkout. Red on this branch since before this work (verified byte-identical against af571353) and untouched here; fixing it means either a repo-local pre-step or changing the shared action.

Full write-up: plans/dev-ui-ci-snapshot/RESULT.md in the companion branch.

arnaubennassar and others added 15 commits August 13, 2026 15:16
…80d83

Replaces the local file:../sdk override in pnpm-workspace.yaml (added while
the bridge tracker API was unpublished) with the now-published snapshot
build, which carries the same commit as SDK_REF in the Docker workflows
(5680d837b168c...). This is the "swap this override to that published
semver" step the override's own comment called for. pnpm install
--frozen-lockfile now succeeds without a sibling ../sdk checkout, which is
what was failing the CI - lint / typecheck / test check on PR #24 -- no
source changes were needed, the SDK's public API surface used by app/**
is unchanged between the local build and the published snapshot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…lication

Devnet was declaring the same aggkit-proxy URL under every L2 networkId in
aggkitBridgeApis ("1": url, "2": url, ...) because a per-network map was the
only field available, even though one proxy fronts every network there.
Bridge API and proxy are not the same thing -- a bridge API is one aggkit
REST backend for one network, a proxy is a superset that also fronts the
tracker and multiplexes every network behind one URL -- so introduce
aggkitProxy as a single-URL field for that shape and keep aggkitBridgeApis
for modes with genuinely distinct per-network backends (mainnet/testnet).
The two are validated as mutually exclusive per mode (never both; neither is
the existing "not configured" escape hatch), and the pre-existing
chains<->map cross-check and duplicate-networkId check now apply only to the
map form, since per-chain agreement is meaningless for a single proxy.

Devnet config.json / config.ci.devnet.json and scripts/kurtosisDevnetEnv.mjs
switch to aggkitProxy (schema-only change; hosts/ports unchanged).
kurtosis-cdk's generated config keeps using the map form for now (untouched,
cross-repo, out of scope here) since aggkitBridgeApis is retained rather than
renamed. Adds NEXT_PUBLIC_AGGKIT_PROXY as the env-override successor,
alongside the unchanged NEXT_PUBLIC_AGGKIT_BRIDGE_APIS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0b converted only devnet to the single aggkitProxy shape; the user asked
for the same treatment on mainnet/testnet so all three modes share one
shape. Every mainnet/testnet aggkitBridgeApis URL was already a
PLACEHOLDER- value (no real backend deployed), so collapsing each mode to
one aggkitProxy is lossless.

- config.json: mainnet -> aggkitProxy "https://PLACEHOLDER-mainnet-aggkit-proxy",
  testnet -> aggkitProxy "https://PLACEHOLDER-testnet-aggkit-proxy". No mode
  in config.json declares aggkitBridgeApis anymore.
- config/config.ci.devnet.json: mirrored identically, preserving the
  documented "byte-identical to config.json except appModes.default"
  invariant (docs/config.md).
- The aggkitBridgeApis map form stays fully supported in the schema
  (configSchema.mjs) and validator (configValidator.mjs) -- untouched --
  because kurtosis-cdk's dev-ui template still generates it and the
  container test fixtures deliberately exercise it. What changed is only
  which form this repo's own config.json uses.
- docs/config.md, docs/docker.md: updated the now-stale claims that
  mainnet/testnet use the map form; documented that the chains<->map and
  duplicate-networkId checks are inherently map-form-only, so with every
  shipped mode now on aggkitProxy those checks no longer guard the
  committed config.json at all -- by design, not a gap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
User directive (mid-step scope change on D0c): "you need to update
kurtosis to work with the new config. Clean the old stuff for good."
Every mode this app ships now goes through a single aggkit-proxy, so the
per-network aggkitBridgeApis map -- and the NEXT_PUBLIC_AGGKIT_BRIDGE_APIS
env override that fed it -- no longer models anything this repo's own
config.json needs.

- config/configSchema.mjs: drop aggkitBridgeApisSchema and the
  aggkitBridgeApis field; jsonAppModeConfigSchema now has only aggkitProxy
  (optional), so the mutual-exclusion superRefine is gone too (nothing
  left to be mutually exclusive with). The schema's .strict() now rejects
  aggkitBridgeApis as an unrecognized key.
- config/configValidator.mjs: remove the chains<->aggkitBridgeApis
  cross-check and the duplicate-networkId check -- both were map-form-only
  and have no target left.
- config/configLoader.mjs: resolveAggkitBridgeApiUrl -> resolveAggkitProxyUrl;
  only resolves a mode's aggkitProxy now.
- app/config.ts, app/types/config.ts: drop the NEXT_PUBLIC_AGGKIT_BRIDGE_APIS
  override path and JsonAggkitBridgeApis type. The RESOLVED runtime shape
  (AppModeConfig.aggkitBridgeApis: Record<number, string>, fanned out from
  aggkitProxy) is unchanged -- every downstream consumer (AggkitBridgeAggregator,
  app/utils/appMode.ts) still gets one URL per network; only the config.json
  surface and the env override are gone.
- scripts/kurtosisDevnetEnv.mjs: writes NEXT_PUBLIC_AGGKIT_PROXY instead of
  a hand-built per-network JSON map.
- playwright.config.ts: requires NEXT_PUBLIC_AGGKIT_PROXY instead of
  NEXT_PUBLIC_AGGKIT_BRIDGE_APIS; removed the "partial-failure" project,
  whose bogus-per-network-override mechanism no longer exists.
  tests/bridge/partial-failure.spec.ts is now skipped with a comment
  explaining why and what a redesign would need.
- tests/container/fixtures/config-{a,b,invalid-schema,invalid-structural}.json:
  migrated off aggkitBridgeApis to aggkitProxy (their unrelated deliberate
  invalidities -- bad rpcUrl, bad default mode -- are untouched). Rebuilt
  the local C-1 image and reran the full container Playwright suite (6/6
  pass) to prove the fixtures and the schema/entrypoint contract still work
  end to end.
- .github/workflows/deploy.yaml: NEXT_PUBLIC_AGGKIT_BRIDGE_APIS secret ->
  NEXT_PUBLIC_AGGKIT_PROXY (only runs on push to main / manual dispatch, not
  on this PR's CI checks). If an actual GH secret named
  NEXT_PUBLIC_AGGKIT_BRIDGE_APIS is configured, an org admin needs to add
  NEXT_PUBLIC_AGGKIT_PROXY as its replacement.
- docs/config.md, docs/docker.md, docs/deployment.md, README.md: rewritten
  to describe aggkitProxy as the only field, document the removal (with a
  "History" section keeping the retired map-form rules' rationale), and
  flag kurtosis-cdk's dev-ui template as a known, temporary, tracked skew
  -- that template still emits aggkitBridgeApis and will fail this schema
  until it's migrated kurtosis-cdk-side (tracked separately, not touched
  here per instruction).

Deliberately NOT touched: kurtosis-cdk (any repo), tests/devnet/**,
.github/workflows/e2e.yaml (still sets the now-dead
NEXT_PUBLIC_AGGKIT_BRIDGE_APIS literal -- flagged, not fixed, since that
file is D1's territory; D1's own plan note already anticipated updating
this literal for a different reason and now needs to for this one too).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… removal)

Covers both commits landed for D0c: the original scope (mainnet/testnet ->
aggkitProxy, 2da5992) and the mid-step scope change to delete the
aggkitBridgeApis map form entirely (8563dd4). Records pnpm run check output,
the rewritten validator/loader test suites, live failure-provable schema
demonstrations against the real config.json, the rebuilt local container
image + full container Playwright suite (6/6), both GHCR publish runs with
the final tag and an anonymous-pull proof, and PR #24's CI status --
including why the e2e check fails (a documented, expected cross-step skew
with .github/workflows/e2e.yaml, D1's territory, not touched here).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
…al-failure spec

D0c removed the aggkitBridgeApis map form, leaving e2e.yaml exporting the
retired NEXT_PUBLIC_AGGKIT_BRIDGE_APIS while playwright.config.ts requires
NEXT_PUBLIC_AGGKIT_PROXY, turning PR #24's e2e check red. Swap the env
literal to the single-URL form and extend the "assert workflow literals
match summary.json" step to check it against
.aggkit_proxy.rest_url_via_proxy so the assertion still checks something.

Restore tests/bridge/partial-failure.spec.ts, which D0c left silently
skipped, via Playwright route interception on the real aggkit-proxy backend
(fail /bridges?network_id=2 for the real Devnet L2-002 network) instead of
the retired per-network env override -- same partial-failure contract,
verified end to end, with the coverage delta (the 'Unknown network' label
fallback for unregistered networkIds) called out explicitly rather than
left as a silent skip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… ambiguity

First CI run of the restored spec (run 31785182452) proved the route-
interception mechanism itself works -- the banner correctly rendered "We
couldn't load activity from Devnet L2-002" -- but a loose
getByText(/devnet l2-002/i) also matched an unrelated transaction row (an
earlier spec's L2-to-L2 transfer touching network 2), tripping Playwright's
strict-mode single-element check. Scope the assertion to the banner's own
message text instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
… result

All PR #24 checks green including e2e (run 31785962610): 4/4 preflight, 12/12
bridge tests -- matching the plan's 12-tests/10-files baseline exactly, so
the partial-failure.spec.ts restoration (see partial-failure-decision.md)
carries no hidden suite-count coverage loss.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
The prior evidence commit (ad292ff) itself triggered one more CI cycle on
push (run 31786588506) -- update pr24-checks-final.txt and
bridge-suite-result-final.txt to point at that run, which is the one that
actually validates the commit these evidence files live in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0b/D0c/D0d committed 22 evidence files under
plans/snapshot-v2-aggkit-e2e/D0{b,c,d}-evidence/, which showed up in
PR #24's diff as orchestration noise unrelated to the actual code
change. Relocate that content (byte-for-byte, verified via sha256)
to /home/brolygon/repos/plans/snapshot-v2-aggkit-e2e/ outside the
repo, matching the convention already used on the kurtosis-cdk side.

Add /plans/ to .gitignore so this cannot recur.
…ocker

NEXT_PUBLIC_PROJECT_ID was inlined into the JS bundle at build time
(Next.js static export), so a published Docker image was permanently
stuck with whatever project id was present when the image was built --
docker run -e could not change it. Since the runtime nginx stage has
no Node, the fix moves this prod-required value into the existing
runtime config.json mechanism instead of adding envsubst/templating
to entrypoint.sh (which its own header comment forbids).

- config/configSchema.mjs: add a required top-level `walletConnect:
  { projectId }` field to the schema.
- app/config.ts: resolve walletConnect.projectId from the loaded
  runtime config, with NEXT_PUBLIC_PROJECT_ID as a local-dev/Playwright
  override only (same precedence pattern already used for
  NEXT_PUBLIC_AGGKIT_PROXY). New getWalletConnectProjectId() accessor.
- app/context/wallet.tsx: read the resolved projectId from config
  inside WalletProvider/ensureAppKit instead of a module-scope
  process.env read, since it's no longer available until
  AppConfigGate resolves.
- entrypoint.sh: extend the structural jq validation to require
  walletConnect.projectId as a non-empty string.
- config.json / config/config.ci.devnet.json: add the walletConnect
  block (placeholder value, preserving graceful AppKit degradation).
- .env.production: no longer sets NEXT_PUBLIC_PROJECT_ID at all, so a
  production build never has an override to freeze in -- Cloudflare's
  deploy.yaml still works since it sets the var as a real shell env,
  which Next's env loader never overrides with a .env file value.
- Docs (docker.md, config.md, deployment.md, README.md, .env.example):
  document the new field, its runtime-vs-build-time precedence, and
  fix docker.md/deployment.md's stale "no image is published yet"
  status (several dispatch-tagged images have since been published
  and verified pullable anonymously).
- Tests: cover the new field's required-ness (missing/empty/wrong
  type all rejected) and the env-override precedence, and add
  walletConnect to every existing config fixture.

Verified: one built image run twice with two different mounted
config.json files (disjoint walletConnect.projectId values) produces
two different outbound AppKit network requests carrying each run's
own project id -- proving the app reads the mounted value at runtime,
not a build-time constant. A config.json missing walletConnect (or
with an empty projectId) makes the container exit non-zero with a
FATAL log line, same as every other required field.
…-image path

Replaces tests/devnet/docker-compose.yml and summary.json with the K1-merged,
digest-pinned v2 bundle from 0xPolygon/kurtosis-cdk (publish run 31787941750):
the aggkit bridge is now a component of the main aggkit-00X process (no more
aggkit-00X-bridge), 9 services total, every image pinned by @sha256 digest
instead of a mutable snapshot-<sha> tag. agglayer-dev-ui-002 (the baked
dev-ui, manual use only) ships behind the `devui` compose profile so CI never
starts it; the proxy's depends_on edge on it is dropped and its compose-level
healthcheck overridden to stop probing bare `/` (already baked into the
emitted bundle upstream).

e2e.yaml: updates the literals-assertion step to compare the compose file's
full set of pinned image digests against summary.json's images.services[]
digests (replacing the old single SNAPSHOT_IMAGE_TAG comparison, which no
longer applies now that there's no tag-based override). Adds the built-image
path per T4's design: builds the PR's own Docker image (reusing
docker-pr.yaml's SDK_REF build-arg pattern) and runs tests/container/ against
it, with an explicit comment on why tests/bridge/* cannot run there
(build:production never sets NEXT_PUBLIC_E2E_ENABLED).

Also updates tests/devnet/README.md's regenerate-and-bump procedure for the
new digest scheme, and fixes stale -bridge/11-service/old-doc-path references
in README.md, docs/config.md, and scripts/devnetReady.mjs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
D0e (89f3917) made entrypoint.sh's structural validation require
walletConnect.projectId, but tests/container/fixtures/*.json predates that
change and none of the four fixtures carry it -- every container-suite spec
that mounts one of these configs would fail fast with "FATAL - mounted
config ... failed validation. Refusing to start." This was never caught by
CI because nothing wired tests/container/ into a workflow until this PR's
built-image step (see the previous commit). Uses the repo's own checked-in
placeholder value (YOUR_PROJECT_ID_HERE, see app/utils/reownConfig.ts) so
AppKit's basic-mode degradation kicks in and the existing console-noise
allowlist in container-app.spec.ts still holds -- an arbitrary non-placeholder
id was verified locally to 403 outside basic mode and log an unallowlisted
"[Reown Config] Failed to fetch remote project configuration" warning.
config-invalid-structural.json's own deliberate defect (appModes.default with
no matching appModes.configs entry) is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Q5xUjjWCiNQdm7cmeWYrs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant