feat: unify app shell, add fuzzy command palette, harden agent_bidding commit-reveal - #466
Merged
devJaja merged 5 commits intoAug 31, 2026
Conversation
…ning (Epta-Node#350) Analyses the sealed-bid auction's attack surface and closes the gaps found. The full threat model, per-attack mitigation, and residual risks live in smart-contracts/docs/agent-bidding-security.md; each claim there names the test that proves it. Front-running (A-1): `reveal_bids` became callable the instant bidding closed, so an attacker could reveal their own poor bid and finalise in the same ledger, scoring out every bidder who had not yet landed a reveal. Adds a bounded reveal window (`reveal_deadline`) and refuses finalisation while the window is open and any bid is still sealed. Early finalisation stays allowed once `revealed_count == bid_count`, where nothing is left to race. Free last-look option (A-2): non-reveal was costless, making a sealed bid a free option to walk away after reading the field. `award_contract` now settles bonds asymmetrically — revealers are refunded, non-revealers forfeit via a new `SealedBid.forfeited` flag. Commitment replay (A-3): the pre-image bound only the bidder, so identical plaintext committed identically across auctions and deployments, leaking still-sealed bids. Adds domain separation over (domain, contract_id, task_id) and publishes `commitment_of` so off-chain tooling cannot drift from what `reveal_bid` verifies. Position-size caps (A-6): `max_price` per auction, global `MAX_BID_PRICE`, `MAX_BIDDERS`, `MAX_TERMS_LEN`, bounded phase durations, checked scoring arithmetic, and a paginated `get_bidders`. The bidder cap is what keeps the loops in `reveal_bids`/`award_contract` from being pushed past the resource limit and wedging an auction. Also adds `abort_auction` so an auction nobody reveals on cannot strand bonds forever (A-7), and `require_auth` on `reveal_bids`/`award_contract`, which previously mutated state with no caller at all (A-8). Tests grow 27 → 50, covering replay (cross-auction, cross-bidder, re-reveal), late reveal, double commit, the finalisation front-run, forfeiture, every cap, and the abort path. Cargo.lock is refreshed: it still listed three removed crates and missed agent-registry's upgrade-manager dep, which fails CI's `--locked` builds. Resolves Epta-Node#350 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`main` does not type-check or build. Several files carry two concatenated copies of themselves from bad merges — duplicate imports, duplicate state declarations, orphaned JSX closing tags, and a stray `@keyframes` tail. The syntax errors also masked the type errors behind them, so fixing the parse failures surfaced a second layer. Where a file held two versions, the one kept is the one the rest of the codebase already depends on: - `ToastContext.tsx` — kept the version exporting `ToastContext` (which `hooks/useToast.ts` imports) and delegating to the shared `ToastContainer`. - `SendXLMForm.tsx` — kept react-hook-form + zod, which the file's own imports (`@hookform/resolvers`, `walletTransferSchema`, `FormField`) point at. The balance check the schema cannot express moved into a resolver wrapper so the error still lands on the `amount` field. - `Hero.tsx` — dropped the half-removed framer-motion wrappers; the opening tags were already plain elements with CSS `slide-up` animations, only the closing tags still said `motion.*`. - `TaskSubmissionForm.tsx` — kept the i18n-aware `makeTaskSchema` factory over the static import, restored the missing `z`/`AlertCircle` imports, and dropped the duplicated hook calls. - `AgentDetailModal.tsx` — restored the `useAgentReputation` call whose results the JSX still read. - `Toast.css` — removed an orphaned `@keyframes` body (a `}` with no opening rule, which failed postcss) and the container properties that had leaked into the `.toast__dismiss` button rule. Two behaviour fixes fell out of the repair: - The submit button was gated on `!isValid`, so an untouched form could never be submitted and the user got a dead button with no explanation. Validation now runs on click and reports what is wrong. - `AgentReputationRadar.test.tsx` stubbed `ResponsiveContainer` without passing dimensions down, so recharts drew nothing and the assertion never passed — it failed on `main` too. Toast dismissal picks up an i18n label rather than hardcoded English, matching the rest of the component. Frontend is now green: `tsc --noEmit` clean, `vite build` succeeds, and the vitest suite passes. The backend is broken by the same class of bad merge but across ~12 files, which is a separate repair and is deliberately untouched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar, top nav, and mobile drawer each carried their own copy of the navigation list, which is how they drifted apart: the drawer's labels were hardcoded English while the sidebar's were translated, and the sidebar's "Dashboard" pointed at `/` — the public landing page — rather than `/dashboard`. `layout/navigation.ts` is now the single source of truth. The sidebar, the drawer, the breadcrumb labels, and the TopNav page title all read from it, so a rename moves every surface at once. - **Grouped, collapsible sidebar.** Items are grouped (Overview / Work / Account). Collapsing hides labels and group headings visually but keeps them in the accessibility tree via `.visually-hidden`, where before they were removed from the DOM entirely and the buttons lost their names. - **Animated active state.** A single shared `layoutId` pill slides between items instead of blinking out and back in. Disabled under `prefers-reduced-motion`. - **Active matching** is exact-or-descendant, so `/tasks/new/step-2` highlights "New Task" while `/tasks/abc-123` — a detail page with no nav entry — correctly highlights nothing. - **Slide-over drawer.** The drawer now enters from the left, the side the sidebar occupies on desktop, rather than sliding up as a bottom sheet from an edge nothing else in the app uses. - **Breadcrumbs** derive labels from the nav config, render structural segments like `/tasks` as plain text instead of links that 404, and wrap rather than pushing the page sideways. - **Per-user sidebar state.** The collapsed flag is keyed by wallet (`sidebar_collapsed:<publicKey>`), so two people sharing a browser profile keep separate preferences. Signed-out users keep the old unscoped key, so no existing preference is dropped. Reads and writes are guarded: private-mode browsers throw on storage access, and the shell falls back to an expanded sidebar rather than failing to render. - **Breakpoint moved 768px → 1024px**, per the acceptance criteria. The threshold lives in `MOBILE_BREAKPOINT_QUERY` and in the `@media` blocks; both are commented as needing to stay in step. The skip-to-content link had no styles at all and was permanently visible; it is now off-screen until focused. Tests cover grouping, collapsed-state accessibility, descendant active matching, per-wallet persistence, storage failure, and breadcrumb construction. Resolves Epta-Node#352 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pta-Node#348) The palette matched with `String.includes()`, which finds nothing unless you type a contiguous run, and returned results in whatever order the sources were iterated — so a page could rank above the exact agent you named. **Fuzzy matching** (`utils/fuzzy.ts`) is a scored subsequence matcher, so any subset finds its target: `dsh` → "Dashboard", `cpad` → "Copy wallet address". Scoring rewards consecutive characters, word starts (including camelCase humps), and matches near the front, and penalises gaps and a long unmatched prefix — the difference between "is a match" and "is the match the user meant". It returns the matched indices too, which the palette uses to highlight the characters that actually hit. A DP over (query × target) with a running max keeps it linear rather than quadratic. **Sources.** Pages come from the shared nav config (`Epta-Node#352`), so the palette cannot drift from the sidebar. Agents match on name *and* capabilities; tasks on id, prompt, and status. Recent history is a searchable source, not just a list shown on an empty query — so re-running an earlier query needs only a few of its characters. Field weights make a title hit outrank the same hit buried in a subtitle, and category weights put actions first, recent searches last. **Actions** are new: run new task, jump to agent, open wallet, copy address, toggle theme. Copy-address is hidden when no wallet is connected rather than offered as a dead entry. **Keyboard-first.** Arrows and Tab move the selection, Home/End jump to the ends, Enter runs, Escape dismisses. Tab is trapped, since walking focus into the page behind a modal is how a palette loses you. Focus stays on the input and the list is driven through `aria-activedescendant` over a proper listbox/option structure, so screen readers announce the highlighted row without focus moving. The selected row scrolls into view — guarded, because `scrollIntoView` does not exist outside a layout engine. Selecting a recent search now refills the query instead of firing a search whose results were discarded, and the palette stays open so you can refine it. A stale selection index is reset when the result list shrinks, which would otherwise run the wrong action on Enter. All strings are translated (en + zh); the palette was entirely hardcoded English before. Adds 25 tests for the matcher (matching, rejection, ranking, highlight indices) and 29 for the hook (shortcuts, each source, each action, history persistence and eviction, API failure). Resolves Epta-Node#348 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@kaluuba-org Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
@mananuf is attempting to deploy a commit to the Jaja's projects Team on Vercel. A member of the Team first needs to authorize it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #348, Closes #350, Closes #352.
Three issues that turned out to share a dependency: #352 introduces a single
source of truth for navigation, and #348's palette reads from it so the two can
never drift apart. #350 is independent — a security review and hardening pass on
the
agent_biddingcommit-reveal auction.One thing to flag up front:
maindoes not build. Several files carry twoconcatenated copies of themselves from bad merges. The frontend was unbuildable,
so #348 and #352 could not be written or verified without repairing it — that
repair is the first commit and is described in detail below. The backend and one
contract crate are broken the same way and are deliberately untouched; see
Pre-existing breakage.
Changes
fix(frontend): repair merge debris that broke the buildWhere a file held two versions, the one kept is the one the rest of the codebase
already depends on:
ToastContext.tsxToastContexthooks/useToast.tsimports itSendXLMForm.tsxTaskSubmissionForm.tsxmakeTaskSchemafactorytHero.tsxmotion.*AgentDetailModal.tsxregained theuseAgentReputationcall its JSX still readfrom;
Toast.csslost an orphaned@keyframesbody that failed postcss. Fixingthe parse errors surfaced a second layer of type errors that had been masked
behind them.
Two behaviour fixes fell out of this:
!isValid, so an untouched form couldnever be submitted — a dead button with no explanation. Validation now runs on
click and says what is wrong.
AgentReputationRadar.test.tsxstubbedResponsiveContainerwithout passingdimensions down, so recharts drew nothing. It failed on
maintoo.feat(frontend): unify the application shell— #352The sidebar, drawer, and top nav each carried their own navigation list, which
is how they drifted: the drawer's labels were hardcoded English while the
sidebar's were translated, and the sidebar's "Dashboard" pointed at
/— thepublic landing page — rather than
/dashboard.layout/navigation.tsis now thesingle source of truth for all of them, plus the breadcrumb and the page title.
labels visually but keeps them in the accessibility tree; previously they were
removed from the DOM and the buttons lost their accessible names.
layoutIdpill slides between itemsinstead of blinking. Disabled under
prefers-reduced-motion./tasks/new/step-2highlights"New Task", while
/tasks/abc-123(a detail page with no nav entry) correctlyhighlights nothing.
on desktop — rather than sliding up from an edge nothing else in the app uses.
/tasksas plain text insteadof links that 404, and wrap rather than pushing the page sideways.
sidebar_collapsed:<publicKey>).Signed-out users keep the old unscoped key, so no existing preference is
dropped. Storage access is guarded — private-mode browsers throw, and the shell
falls back to an expanded sidebar rather than failing to render.
feat(frontend): fuzzy command palette— #348Matching was
String.includes(), which finds nothing unless you type acontiguous run, and results came back in source-iteration order.
utils/fuzzy.ts):dsh→ "Dashboard",cpad→ "Copy wallet address". Scoring rewards consecutive characters, wordstarts (including camelCase humps) and early matches; penalises gaps and long
unmatched prefixes — the difference between a match and the match. It also
returns matched indices, which the palette uses to highlight the exact
characters that hit.
capabilities), tasks (id, prompt, status), and recent history — searchable as a
source, so re-running an earlier query needs only a few of its characters.
Field and category weights rank a title hit above the same hit in a subtitle,
and actions above recents.
theme. Copy-address is hidden with no wallet connected rather than offered dead.
Enter runs, Escape dismisses. Tab is trapped. Focus stays on the input and the
list is driven via
aria-activedescendantover a real listbox/optionstructure, so screen readers announce the highlighted row without focus moving.
Selecting a recent search now refills the query — it previously fired a search
whose results were thrown away. All strings are translated (en + zh).
feat(contracts): harden agent_bidding— #350Full threat model, per-attack mitigation, and residual risks in
smart-contracts/docs/agent-bidding-security.md;every claim there names the test that proves it.
reveal_bidswas callable the instant bidding closed, so an attacker could reveal their own poor bid and finalise in the same ledger, scoring out everyone who had not yet landed a revealcommitment_ofpublished so tooling cannot drift from whatreveal_bidverifiesreveal_bidspast the resource limit and wedge an auction, stranding every bondmax_price,MAX_BID_PRICE,MAX_BIDDERS,MAX_TERMS_LEN, bounded durations, checked arithmetic, paginatedget_biddersabort_auctionrescue pathreveal_bidsandaward_contractmutated state with no caller and norequire_authaward_contractis restricted to creator or winnerThe doc is equally explicit about what is not covered — reputation is
self-declared, reveals are public as they land, Sybil resistance is bond cost
alone, and bonds/escrow are modelled as state rather than custodied here. Those
are integration-critical.
Tests grow 27 → 50, covering replay (cross-auction, cross-bidder,
re-reveal), late reveal, double commit, the finalisation front-run, forfeiture,
every cap, and the abort path.
Cargo.lockis refreshed: it still listed three removed crates and missedagent-registry'supgrade-managerdependency, which fails CI's--lockedbuilds.
Verification & Testing
npm test(Frontend) — 35 files, 319 tests passnpm run build(Frontend) — clean,tsc --noEmitcleancargo test --locked -p agent-bidding— 50 passcargo fmt --all --check && cargo clippy --locked -p agent-bidding --all-targets -- -D warnings— cleancargo build --locked -p agent-bidding --target wasm32v1-none --release— cleannpm test(Backend) — cannot run, see belowPre-existing breakage not addressed here
Verified present on
upstream/mainbefore this branch:package.jsonis invalid JSON (two manifestsconcatenated),
tsconfig.jsonandjest.config.jslikewise, andsrc/api/app.tsholds two different
createAppfactories. Repairing the manifests leaves~45 type errors across ~12 files.
upgrade-managerdoes not compile (25+ errors), which breaksagent-registry, which the contracts CI job builds.I stopped short of both. Each bad merge means judging which half of someone
else's work is canonical, across a subsystem none of these three issues touch —
and a wrong guess there is worse than leaving the breakage visible. The frontend
was different: #348 and #352 live in it, and I could not verify my own work
without it building.
Because CI's
contractsjob is gated onneeds: [backend, frontend], thebackend failure will keep the contract checks from running on this PR. The
agent_biddingcommands above were run locally and all pass. Happy to open aseparate PR for the backend repair if maintainers want it.
Contributor Checklist
smart-contracts/docs/agent-bidding-security.md, updated contract README andfrontend/LAYOUT_IMPLEMENTATION.md