Skip to content

squad: security review + honest arrival denominator - #650

Open
nategeier wants to merge 4 commits into
mainfrom
feat/squad-security
Open

squad: security review + honest arrival denominator#650
nategeier wants to merge 4 commits into
mainfrom
feat/squad-security

Conversation

@nategeier

@nategeier nategeier commented Aug 18, 2026

Copy link
Copy Markdown
Member

Security lane, rounds 1+2 of the 2026-08-18 standing squad.

A. Security review — desk (#638/#644), inbox human-send (#645), HL delegated sign (#647)

Core still HOLDS (guarded builds, signature-gated money, no tx material over MCP, HMAC webhooks with SSRF fence, recover-verified HL consent + delegation activation). Fixed in-lane, each pinned:

  1. broker_execute bound jobs to an UNPROVEN wallet. Any unauthenticated agent could broker_open naming a stranger's wallet + any agent_key string, then broker_execute → a jobs row owned by that wallet, listed in their Jobs rail with a needs-you badge ("swap … then send … to 0xATTACKER"). Now broker_execute requires wallet_signature — personal_sign over deskExecuteConsentMessage(intent_id, wallet) — recovered server-side before any row exists; bound to intent + wallet, single-use. Drill/docs updated.
  2. broker_send sender_label could impersonate a claimed handle ("@nategeier"). cleanSenderLabel strips leading @ and 0x-address shapes.
  3. Rule 7 — third-party financial brands on public pages (/l/yeet was live wearing Robinhood from a scan drill). lib/brand-denylist.ts: POST /api/intent-links/brand refuses denied hosts (uniswap.org, cow.fi, robinhood.com, metamask.io, coinbase.com, aave.com, lido.fi, hyperliquid.xyz, opensea.io, snapshot, near, li.fi, morpho, wallets + venue policy hosts, incl. subdomains) by name BEFORE any fetch and again on the post-redirect host; brandFromRow() is the ONE render-time reader on /l page + OG and /i page + OG — a denied stored row renders house at deploy, data untouched.
  4. POST /api/hl/delegation idempotent per wallet+chain within 20 min (same pending row + byte-identical typed data, reused:true); superseded pending rows are deleted, never accumulate (QA's retry-spam finding).
  5. Public /agents records exclude internal-run intents (harness identities have no record — 404).

Accepted risks (rationale in squad-2026-08-18/security.md): client-posted /i funnel events can spoof signed/settled (webhook / broker_status / inbox drop) — same trust class as first-party telemetry; intent ids as capabilities; per-IP-fenced funding scan; HL Guardian arm needs no getCode belt (approveAgent is an ECDSA-only artifact; PATCH recover-verify refuses ERC-1271 sigs before the venue).

B. THE WEEK item 3 — honest arrival denominator

  • is_internal boolean not null default false (+ index) added on Neon yeetful to intent_links, wallet_working_sets, jobs, chats, dca_schedules, broker_intents, ask_failures (additive; done). lib/internal-run.ts = one reader of the Q3 signal.
  • Stamped at every write site (intent-links, mosaics, working-set sticky-on, jobs API, chats, createJob from chat/HL auto-fund/mosaic, DCA schedules + the period jobs they mint, broker open/handoff/tile/send/execute via extra.requestInfo.headers, inbox sends, ask-failure log + wallet-refusal beacon); routes echo internal: true.
  • lib/gtm-arc.ts excludes stamped rows on every table it reads (messages via chats, job_steps via jobs, events via their link, dca_schedules); cohorts milestones + digest + hero strip ("Links live"/"Opens" — static route, lands at deploy) + /api/admin/ask-failures (hidden by default, ?internal=1 + label) match.
  • test:api sends the header on every BASE request (telemetry keeps per-probe stamps; x-yf-organic-probe opts out). Pins: stamped mint never arrives / organic write does (then re-stamped so the suite leaves no stranger); SQL-text pins; end-of-suite per-wallet "no throwaway (signed-in OR wallet-in-body chat) left as an arrival" via admin ?only= on /api/admin/cohorts; harness identity has no /agents record; stamped refusal hidden by default.
  • scripts/backfill-internal-arrivals.ts — dry-run default, --apply owner-gated.

Owner-gated data steps (do not drop)

cd website && DATABASE_URL=<from .env.local> npx tsx scripts/backfill-internal-arrivals.ts           # dry run (counts + samples)
cd website && DATABASE_URL=<from .env.local> npx tsx scripts/backfill-internal-arrivals.ts --apply   # writes

Dry run 2026-08-18 ~10:30Z: intent_links 3,410/3,518 (40 organic left) · wallet_working_sets 550/586 · jobs 235/308 (dev-env + fixture wallets) · dca_schedules 6/13 · broker_intents 258/324 · ask_failures 0/28 · chats 8/434 · embed_turns 1,959 (the #637 predicate). Wallet-level rules refuse TEST_WALLETS and any wallet that signed real money.

Clear the /l/yeet drill brand row (destructive → owner):

UPDATE creator_handles SET brand_domain=NULL, brand_name=NULL, brand_logo=NULL, brand_accent=NULL, brand_bg=NULL, brand_updated_at=NULL WHERE handle='yeet' AND brand_domain='robinhood.com';

(snapshot row wears snapshot.org — also denied at render now; same UPDATE with handle='snapshot' if wanted.)

Gates

tsc + build + test:api 1478 / 1 known-red (router-select) vs next start -p 3803.

🤖 Generated with Claude Code

Security (round 1 of the 2026-08-18 squad, Security lane):
- broker_execute now PROVES the wallet: a required wallet_signature
  (personal_sign over deskExecuteConsentMessage(intent_id, wallet)) is
  recovered server-side before any job row exists. Before this, any
  unauthenticated agent could open an intent naming a stranger's wallet
  + any agent_key string and plant a needs-you job in that wallet's
  Jobs rail ("swap … then send … to 0xATTACKER"). Consent is bound to
  intent id + wallet, single-use (intent must be open). Drill + docs
  updated; harness signs it and pins the three refusals.
- broker_send sender_label can no longer wear Pantessa-stamped marks:
  leading "@" (claimed handles) and 0x… short-address shapes are
  stripped, so "from @nategeier" can only ever mean the real handle.

Honest denominator (THE WEEK item 3 + coordinator/GTM ask):
- is_internal added (additive DDL run on Neon `yeetful`) to
  intent_links, wallet_working_sets, jobs, chats; lib/internal-run.ts is
  the one reader of the Q3 signal (x-yf-internal-run header /
  internalRun body). Stamped at every write site: /api/intent-links,
  /api/mosaics, /api/working-set (sticky-on), /api/jobs, /api/chats,
  createJob (chat jobs, HL auto-fund, mosaic), the broker desk (handoff,
  tile, send, execute — read off extra.requestInfo.headers), inbox sends.
  Each route echoes `internal: true` so drills can assert.
- lib/gtm-arc.ts addrsUnion + arcQuery exclude stamped rows on every
  table they read (chats/messages via the join, job_steps via jobs,
  intent_link_events via their link); cohorts milestone CTEs match;
  digest counts organic links only.
- test:api: global x-yf-internal-run on every BASE request (telemetry
  keeps per-probe stamps; x-yf-organic-probe opts a probe out); pins:
  stamped mint/write never arrives, organic write does (then re-stamped
  sticky-on so the suite leaves no stranger), SQL-text pins, signal
  reader pin, and an end-of-suite per-wallet "no throwaway left as an
  arrival" pin (admin ?only= filter on /api/admin/cohorts).
- scripts/backfill-internal-arrivals.ts: dry-run default, --apply
  owner-gated; flags legacy rows by the audit's heuristics (fixture-only
  throwaway creators, bare {} working sets, dev-env jobs, fixture
  wallets, receipts chats) + the #637 embed_turns predicate.

Gates: tsc + build + test:api 1470/1 known-red vs :3803.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview Aug 18, 2026 10:30am

Request Review

…mps, rule-7 brand denylist, honest hero, idempotent HL delegation

- is_internal (additive DDL run) on dca_schedules, broker_intents,
  ask_failures; stamped at their write sites (runDcaTurn + the period
  jobs it mints inherit; broker open/tile/send; ask-failure log +
  wallet-refusal beacon). gtm-arc/cohorts exclude dca_schedules;
  getAgentRecord excludes internal intents (a harness identity has NO
  public /agents record); /api/admin/ask-failures hides stamped rows by
  default (+ ?internal=1 toggle + label on /dashboard/failures).
- lib/brand-denylist.ts (rule 7): POST /api/intent-links/brand refuses
  third-party financial brand hosts by name before any fetch (+ the
  post-redirect host); brandFromRow() is the ONE render-time reader on
  /l page + OG, /i page + OG — a denied stored row renders house
  (/l/yeet stops wearing Robinhood at deploy, data untouched).
- LinksHero "Links live"/"Opens" go through is_internal (route is
  static; lands at deploy). Source-contract pin.
- POST /api/hl/delegation is idempotent per wallet+chain within 20 min
  (same row + byte-identical typed data, reused:true); superseded
  pending rows are deleted, never accumulate.
- Harness: per-wallet no-stranger pin also collects wallet-in-body
  chat turns; backfill script grows D1/D2/B1/A1 rules.

Gates: tsc + build + test:api 1478/1 known-red vs :3803.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nategeier added a commit that referenced this pull request Aug 18, 2026
…s, live poll pill, internal rows hidden by default

The ten-strangers drill has Nate watching /dashboard/failures and the
per-link funnel while a recruit signs; GTM verified ?funded=1 was not read,
nothing auto-refreshed, and useIntentLinks loaded once.

- /dashboard/failures reads ?funded=1 / ?kind=wallet-refused / ?internal=1
  on mount and writes them back on every toggle (replaceState) — a watch
  link opens on the right slice. New "wallet refused" + "internal" toggles.
- components/LivePill.tsx: useLivePoll (visible-only interval, pauses when
  the tab is hidden, refires on return) + the "live · updated Xs ago" pill.
  Failures page polls quietly every 25s; useIntentLinks re-reads every 30s
  and exposes updatedAt — pill on the rail Links tab + /dashboard/links.
- /api/admin/ask-failures: ?kind= filter, ?internal=1 opt-in; rows carry
  internal:boolean read from ask_failures.is_internal via a raw query that
  tolerates the column's absence (#650 adds it) — never a 500; stamped rows
  hidden by default, counted as internalHidden, tagged "internal" when shown.
- test:api pins: ?kind slice + internal tag + ?internal=1 zeroes the hidden
  count; page source pin for the URL params + pill.

Live-proven on :3801 as the burner (real SIWE via mock EIP-6963 + local
signer): toggles write the URL, router.push with the query lights the
toggles on mount, pill ticks on failures + /dashboard/links, a stamped
drill row wears the tag only under ?internal=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esidual @/0x (QA bypasses), docs entity-space fix

- QA adversarial pass on #650: "@@ @nategeier" survived a single-pass
  strip; fullwidth @ (U+FF20) was stored verbatim. cleanSenderLabel now
  NFKC-normalizes, strips @-/0x-shapes until stable, then REFUSES any
  label still carrying an at-sign of any variant or a 0x-hex run (the
  two Pantessa-stamped marks). Pinned pure + on the wire (both bypass
  strings land as the bare word; a mid-label @ is refused, never stored).
- app/docs/desk: `<code>wallet_signature</code>{' '}—` (SWC entity-space
  class; composes with UI/UX's fence, same edit as QA's f2957a9).

Gates: tsc + build + test:api 1479/1 known-red vs :3803.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@nategeier

Copy link
Copy Markdown
Member Author

Round-2 follow-up (QA adversarial pass): cleanSenderLabel now NFKC-normalizes → strip-until-stable → REFUSES any residual at-sign (any Unicode variant) or 0x-hex run; both bypass strings (@@ @nategeier, fullwidth @nategeier) pinned pure + on the wire (land as bare nategeier; a mid-label @ is refused, never stored). Also the /docs/desk <code>wallet_signature</code>{' '}— entity-space fix (same as QA's f2957a9). Gates re-run: tsc + build + test:api 1479/1 known-red vs :3803 (4a222c0).

nategeier added a commit that referenced this pull request Aug 18, 2026
…p on the ask-failures feed

Both lanes added ?internal=1 to /api/admin/ask-failures + /dashboard/failures:
#650 via the new typed is_internal column (hidden by default in the WHERE),
#653 via a raw-SQL id read tolerant of the missing column + ?kind= slicing.
Merged: #653's structure (kind filter, page toggles/tag/poll) on #650's typed
column; raw reader retired; internalHidden counted server-side; response
carries kind + internal + per-row internal. test-api: both appended check
blocks kept. Duplicate 'internal' field in the page type dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nategeier added a commit that referenced this pull request Aug 18, 2026
…andFromRow (rule-7 render fence) AND #651's @handle byline (visuals re-cut re-inlined the brand read, dropping the denylist)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lets (first_seen ≥ run start); cohorts honors ?days= 1..90

QA's integration run replays REAL wallets read-only through chat bodies;
the pin treated every body wallet as throwaway and ?days=1 fell to 14d,
so an organic 08-12 arrival read as a leak. Now: pre-run first_seen =
replayed real wallet (skipped, never a leak); ?days= honored 1..90
(pinned: days=1 → windowDays 1, 999 → 14).

Gates: tsc + build + test:api 1480/1 known-red vs :3803.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nategeier added a commit that referenced this pull request Aug 18, 2026
…s, live poll pill, internal rows hidden by default

The ten-strangers drill has Nate watching /dashboard/failures and the
per-link funnel while a recruit signs; GTM verified ?funded=1 was not read,
nothing auto-refreshed, and useIntentLinks loaded once.

- /dashboard/failures reads ?funded=1 / ?kind=wallet-refused / ?internal=1
  on mount and writes them back on every toggle (replaceState) — a watch
  link opens on the right slice. New "wallet refused" + "internal" toggles.
- components/LivePill.tsx: useLivePoll (visible-only interval, pauses when
  the tab is hidden, refires on return) + the "live · updated Xs ago" pill.
  Failures page polls quietly every 25s; useIntentLinks re-reads every 30s
  and exposes updatedAt — pill on the rail Links tab + /dashboard/links.
- /api/admin/ask-failures: ?kind= filter, ?internal=1 opt-in; rows carry
  internal:boolean read from ask_failures.is_internal via a raw query that
  tolerates the column's absence (#650 adds it) — never a 500; stamped rows
  hidden by default, counted as internalHidden, tagged "internal" when shown.
- test:api pins: ?kind slice + internal tag + ?internal=1 zeroes the hidden
  count; page source pin for the URL params + pill.

Live-proven on :3801 as the burner (real SIWE via mock EIP-6963 + local
signer): toggles write the URL, router.push with the query lights the
toggles on mount, pill ticks on failures + /dashboard/links, a stamped
drill row wears the tag only under ?internal=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nategeier added a commit that referenced this pull request Aug 18, 2026
… refusal rows, "Confirm in your wallet…", /i exit ramps gated, eyebrow = the OG card

Merge hygiene (QA r3): rebased onto feat/squad-security 4a222c0 and resolved
the ask-failures overlap exactly as the integration branch did (8a91da0):
#653's structure (?kind= slice, page toggles/tag/poll) on #650's typed
is_internal column; the raw-SQL id reader is retired.

- lib/wallet-refusal.ts walletErrorWords: the row's reply carries the
  wallet's ACTUAL words — viem wraps RPC failures so e.message leads with
  "An internal error was received."; the diagnosable line lives in
  .details / .data.message / .cause — first specific line wins. Used at
  every beacon site (SendTxButton, SignOrderButton, SignHlActionButton).
- N1: the in-flight label is "Confirm in your wallet…" on every sign/arm
  button (was "Sign in wallet…" — read as a login).
- N2: the /i header exit ramps (MAKE A LINK / OPEN THE APP) render only on
  receipt or flow-nudge — never above the card mid-flow.
- N3: linkEyebrow (lib/intent-links) mirrors the OG card: From @handle ·
  your wallet signs / Call by <agent> · … / Intent link · from <sender> · …
  (branded splash keeps · powered by Pantessa); the /i page passes the
  creator's claimed handle. No more "CALL · BY @handle".
- Pins: walletErrorWords on viem/node/plain shapes; linkEyebrow pure +
  rendered creator/house eyebrows; N1/N2 source pins. 1513/1 known-red vs
  :3801 on the rebased branch (Security's pins included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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