Skip to content

feat(web): the homepage shows B4's guardrails and route shapes - #851

Merged
blove merged 15 commits into
mainfrom
blove/homepage-tracer-shapes-impl
Sep 25, 2026
Merged

blove merged 15 commits into
mainfrom
blove/homepage-tracer-shapes-impl

Conversation

@blove

@blove blove commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

PR 2 of 3 from docs/superpowers/specs/2026-09-25-homepage-files-tour-design.md. It adds two sections between the folder tour (#845) and the closing call to action. The plan is docs/superpowers/plans/2026-09-25-homepage-files-tour-pr2.md.

Guardrails (#guardrails): "Four checks decide what a call can do."

You pick one of five calls a support agent might make. The page then shows what each of B4's four checks (tool scope, permission, sandbox, delegation) does with it, under one real app config.

  • readFile passes every check and runs inside the sandbox.
  • refund pauses for approval. Allow once runs it and saves nothing, so the next refund asks again. Deny returns [B4_E3001] Permission denied by user: tool refund to the model.
  • runBash("curl … | sh") has no allow rule, so it pauses. Allow once runs it in the Docker sandbox with no network, because the config sets network: { mode: "deny" }. Deny stops it.
  • deleteUser is withheld by tool scope, so the model never sees it.
  • task to the translator subagent is stopped by a delegation rule, with the real [B4_E3002] message.

Every result on the page is produced by B4's own code in the tests:

  • resolveToolScope decides tool scope.
  • For approvals, the permissions store and wrapToolWithApproval run inside a real LangGraph graph that pauses and resumes.
  • The sandbox builds its own docker run arguments, and the tests record them with a fake Docker client.
  • resolveGuardedSubagent decides delegation.

A mutation check backs this up. Adding a curl allow rule makes the pause, allow-once and deny tests fail, even with the pinned config text taken out.

Route shapes (#route-shapes): "Pick the shape per route."

The scaffold's hello route is written four ways: agent, workflow, a LangGraph graph and a LangChain chain. B4's discoverRoutes classifies each fixture, and the graph, chain and workflow versions each run and greet "Ada".

Spec corrections (details in the plan)

  • The four-call spec never reached the delegation check. A fifth call, task, was added, as Brian decided.
  • runBash asks for approval instead of being allow-listed. Brian decided this so the page doesn't read as advice to allow-list curl.
  • Config placement:
    • sandbox belongs in b4.config.ts, not in the route.
    • A deny delegation rule hides the subagent from the model entirely, so the fixture uses a constrain rule instead.
  • The default-network sentence was false. The spec said the sandbox's default blocks 169.254.169.254, but the Docker provider doesn't enforce that denylist. The copy now says only what's tested. The same claim in sandbox.mdx is flagged as a separate follow-up.
  • The spec called for "a radio group with aria-pressed", which mixes two ARIA patterns. Both switchers use native radios in a <fieldset> instead.

UI/UX (ui-ux-pro-max)

  • Clicks set the final state at once. The board, aria attributes and announcement all update on the click; GSAP only animates, and the next action stops it. There's one announcement per action.
  • Nothing shifts. Every variant stays in one grid cell; the hidden ones are inert.
  • Accessible basics:
    • targets are 44px
    • gate states are text plus a glyph, not colour alone
    • highlighted code keeps at least 4.5:1 contrast (tested)
    • reduced motion runs no animation
  • Focus stays in the tracer when a click hides the focused control. The WebKit run caught a case the Chromium-only checks missed: WebKit moves focus on mousedown.

New devDependencies (apps/web)

@b4run/cli, @b4run/permissions, @b4run/sandbox, @langchain/core@1.2.12, @langchain/langgraph@1.4.17. They add 15 lines to the lockfile and no new packages.

Verification

  • Gates: pnpm --dir apps/web test passes (60 files: 920 passed, 1 skipped). lint, typecheck, node scripts/check-docs.mjs and pnpm check:build-cache all pass.
  • Playwright + axe at 375, 768, 1024 and 1440px, with motion on and reduced (all 8 runs):
    • no horizontal scroll, smallest target 44px, and 0px layout shift across every call, answer and shape
    • the keyboard alone works through both answers on refund and runBash
    • rapid clicks show the right final state, with exactly one announcement per action
    • axe reports 0 violations
  • The WebKit focus scenarios pass.
  • lastmod: only the / entry changes.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
b4-run Ready Ready Preview Sep 25, 2026 9:30pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

blove and others added 15 commits September 25, 2026 14:27
… shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s decisions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…by route discovery

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion, sandbox and delegation code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under load, the act() flush after a pick can take longer than the 200ms
fade, so checking for a still-running tween failed intermittently. The
test now spies on gsap.fromTo; the kill-and-clear checks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able traces

The motion test checks each trace was started over its own board (read from
the timelines the island created) rather than that a 180ms tween is still
running after act(), which a slow flush under load can outlast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bash caption claimed the default network policy blocks 169.254.169.254;
Docker's allow mode is a plain bridge network and enforces no denylist. A new
test resolves an app without a network line through the CLI's own
resolveSandboxManager and asserts the keeper container starts with
--network bridge and no 169.254 anywhere, and the caption now says just that.

refund now runs through wrapToolWithApproval, as the runtime serves it:
Allow once returns the tool's { refunded: 500 } and saves nothing; Deny
returns exactly "[B4_E3001] Permission denied by user: tool refund", which
the board quotes. readFile runs through the same pausable graph on the
fixture's sandbox: no interrupt, every docker exec in the keeper container.

The keeper docker run is selected by --name, and the recording sandboxes take
scope and image from the options the fixture config passed to dockerSandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Safari and Firefox on macOS don't focus a radio when its label is clicked.
With focus on Allow once, Ask again or the caption's docs link, picking a
call that doesn't pause made that control inert and dropped focus to
<body>. When focus is inside the tracer but outside the radio group, the
pick now moves it to the newly checked radio. The focus effect runs as a
layout effect, so focus moves before paint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The motion test checked only that killed traces left no inline opacity. It
now checks transform as well, so a stopTrace that forgot to clear the
y offset fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r on a label click

WebKit moves focus on mousedown to the nearest focusable ancestor of the
clicked label (the page's <main tabindex="-1">) or to <body> before the
radio changes, so picking read after refund left focus on <main>. Treat
focus on an ancestor of the tracer, or none, as stranded too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 375px the four options wrap to two rows; .option + .option shifted the
row-two cell 1px left and the rows' borders doubled. Every cell now
overlaps its right and bottom neighbours by 1px, with the padding giving
the last overlap back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blove
blove force-pushed the blove/homepage-tracer-shapes-impl branch from 61bb2a2 to 59f6bb7 Compare September 25, 2026 21:29
@blove
blove deployed to vercel-preview September 25, 2026 21:29 — with GitHub Actions Active
@blove
blove merged commit c075436 into main Sep 25, 2026
30 of 31 checks passed
@blove
blove deleted the blove/homepage-tracer-shapes-impl branch September 25, 2026 21:49

This branch was successfully deployed

2 active deployments
Preview — 59f6bb75 Deployed Sep 25, 2026 by vercel[bot]
vercel-preview — 59f6bb75 Deployed Sep 25, 2026 by blove via vercel-native #2419
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