Repository navigation
Redesign the landing page to match the new mockup - #31
Conversation
Rebuild the landing page around the new design: a four-questions band that links into the page, a problem split with a figure, the primitives as a list beside a scroll-dealt card deck, the security areas as hash-addressable tabs, numbered Agent Builder steps, an icon grid, and a defense-in-depth figure at the close. The nav gains an animated mark and a theme menu that shares next-themes state with the footer toggle. The concept figures are hand-authored illustrations ported from the design study into geo/figures.js, behind a typed GeoFigure boundary that server-renders the SVG and animates only visible, unpaused figures on one shared timer, honoring prefers-reduced-motion. The OWASP table adopts the mockup's wording: "In progress" for partial coverage, a "Not yet" column, reworded gaps, and a per-row coverage bar. The coverage labels now live in owasp.ts beside the rows they describe. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
samkim
left a comment
There was a problem hiding this comment.
Diagram and copy accuracy review
I fact-checked the 8 diagrams this PR renders against the code. The 6 other figures in figures.js (peraction, injection, leakage, delegation, revoke, pods) aren't rendered, and I didn't review them. This PR ports the mockup as designed; these comments track the corrections for follow-up commits. Each finding cites code evidence, and I re-checked the high-severity ones by hand.
Inaccurate (fix before launch)
- Defense-in-depth: the layer order is wrong, slot isn't a separate layer, plan and slot are opt-in, and the sandbox does hold the credential (
figures.js:615,Landing.tsx:1437; the same phrase is inREADME.md:371anddocs/security-model.md:627) - Broker: MCP credentials are resolved and held by the runner, and sidecar credentials are frozen at pod creation; "handle" is the wrong term (
figures.js:243,Landing.tsx:163/168; doc drift ingiving-credentials.mdx:46) - Lens:
service:is relation-less and can never be granted anything (figures.js:289)
Misleading
- Lens:
class ∩ user:maya. The class is a ceiling, not an intersected subject. - Gate:
gh apishown as allowed without its GET-only constraint - Define: "in git" and "own … memory"
- Channels:
cronisn't a channel kind (bentois) - Memory:
company:acmeisn't a shipped type, and sessions aren't limited to one slot - Union: the figure and the copy beside it describe different problems (minor)
Pre-existing card copy (unchanged in this diff, so it can't take inline comments)
Landing.tsx:143, "kept in git": same issue as the define figure.Landing.tsx:153, "never through a shell": technically false. Credentialed calls go through a fixedsh -cloader (pkg/tools/exec/remote/env.go:136-140,:291). The model's args arrive as positional"$@"and are never interpolated, so the property holds. Suggested: "argv only, never shell interpolation".Landing.tsx:163, "the agent never holds them": see the broker comments.
Verified accurate: denied calls never reach the tool (loop_dispatch.go:755-756); each session gets its own pods and they're reaped when done (agentsession/controller.go:3921-3946); GitHub and Slack each start a session; reading memory is a SpiceDB check; and the six gate tool names match reviewbot's toolspecs.
🤖 Generated with Claude Code
| holds (the accent). Each cycle one more layer fails, so every layer gets to | ||
| be the one that holds. */ | ||
| { | ||
| const names = ["plan", "slot", "tool spec", "call check", "sandbox"]; |
There was a problem hiding this comment.
[Inaccurate] Defense-in-depth figure: layer order and two layers don't match the code
- Order. The pre-call order is fixed in
pkg/authz/hooks/order.go:12-58: McpTrust (10) → Revocation (12) → ToolGuard (15) → ContentGuard (18) → PlanGate (19) → ToolCallAuthz (20) → Scope (30) → Execute. For a CLI or sandbox tool, the tool spec is validated after that, in the ToolCall controller (pkg/controllers/toolcall/controller.go:230), and then the command runs in the sandbox (:441). The real path is plan → call check → tool spec → sandbox. - The slot isn't a separate layer. It's a precondition on the same SpiceDB check as the call check (
pkg/agent/runner/pipeline_wiring.go:433-437,:460-464). - Plan gating and slots are opt-in. Plan gating defaults to
disabled(pkg/apis/v1alpha1/agentclass_types.go:1486,hooks_dataplane.go:51-54).Slotsisomitempty(agentclass_types.go:1370-1373). The figure draws both as always present. - The bottom layer's credential claim is false. See the comment on the closing copy.
Suggested fix: draw four layers, plan (opt-in) → call check (slot-bound) → tool spec → sandbox, with plan dashed. Alternatively, keep five and relabel "slot" + "call check" as one "call check · slot" layer. Update the aria label in Landing.tsx to match.
There was a problem hiding this comment.
Addressed in 938c9e5. The figure is conceptual, so it keeps five layers as representative guards, reordered to plan → slot → call check → tool spec → sandbox. The aria label now matches.
| <p className="lp-lede"> | ||
| To misuse an agent, an attacker has to get past an approved plan, | ||
| a locked slot, a tool spec, a check on every call, and a sandbox | ||
| that never held the credential. |
There was a problem hiding this comment.
[Inaccurate] "a sandbox that never held the credential"
For CLI and sandbox tools, the broker resolves the credential into env vars, and the controller runs the command inside that tool's sandbox with them:
pkg/controllers/toolcall/controller.go:272-279:res, berr := r.Broker.Resolve(...)…agentEnv = res.EnvVarscontroller.go:433-441:Env: execEnv(agentEnv, ...)→executor.Exec(...)
What is true: the credential shows up only at exec time, only for that call, after a per-exec use_token re-check (controller.go:350-360), and in that tool's own sandbox. The model never sees it.
Suggested: "…a check on every call, and a sandbox that holds only that tool's credential, only for that call."
The same wrong phrase is in README.md:371 and docs/security-model.md:627.
There was a problem hiding this comment.
Addressed in 938c9e5. The closing copy is now framed around the model: "…a locked slot, a check on every call, a tool spec, and a sandbox. The model never sees a credential." The same sentence is fixed in README.md and docs/security-model.md.
| b += agent(ag[0], ag[1], 22) + `<circle class="dot" cx="${ag[0] + 30}" cy="${ag[1] + 30}" r="5"/>`; | ||
| b += `<rect data-k="tool" class="dot" x="${tl[0] - 10}" y="${tl[1] - 10}" width="20" height="20"/>`; | ||
| b += `<circle data-k="call" class="pf" r="4" cx="-10" cy="-10"/><circle data-k="kd" class="af" r="5" cx="-10" cy="-10"/>`; | ||
| b += T(ag[0], ag[1] - 34, "agent", "", "middle") + T(ag[0] + 40, ag[1] + 34, "handle") + T(tl[0], tl[1] - 22, "tool", "", "middle"); |
There was a problem hiding this comment.
[Inaccurate] Broker figure: "handle", and "straight to the tool" only holds for sandbox tools
There are three credential paths, and only one matches the picture:
- Sandbox/CLI tools: resolved per call by the ToolCall controller and injected into the exec (
pkg/controllers/toolcall/controller.go:272). The runner only stamps descriptors (controller.go:249-250). ✅ Matches the figure. - MCP servers: the runner resolves the credential once at session start (
internal/cmd/runner/main.go:1060), freezes it on the tool (mt.SetAuth(header, value),:1220), and sends it on each MCP request. It re-resolves only on a 401 or revocation (:1267). - SidecarToolbox: resolved by the AgentSession controller into a per-session Secret, frozen at pod creation, with no per-use re-check (
pkg/controllers/agentsession/controller.go:3538-3549).
Also, "handle" means something else in this codebase: pkg/agent/secretout returns handles for secrets a tool produces (secretout.go:1-4). For credentials going into a tool, the runner holds descriptors/references and the model holds nothing.
Suggested: relabel "handle" as "reference" (or drop it). Make "agent" "model", or make the claim explicit: "the model never sees the credential".
There was a problem hiding this comment.
Addressed in 938c9e5. "handle" becomes "reference", and the claim is about the model rather than the agent.
| hue: 2, | ||
| geo: "broker", | ||
| figLabel: | ||
| "The broker hands the credential straight to the tool at the moment of use; the agent holds only a handle", |
There was a problem hiding this comment.
[Inaccurate] Broker aria label, plus the card body three lines up (L163)
"The broker hands the credential straight to the tool at the moment of use; the agent holds only a handle." The card body says: "Credentials resolve at the moment of use, and the agent never holds them."
Both are strictly true only for sandbox tools. For MCP, the runner process resolves the credential at session start and holds the value (internal/cmd/runner/main.go:1060, :1220). See the comment on figures.js:243.
Suggested body: "Credentials are resolved by a broker outside the model and injected into the tool that needs them; the model never sees them."
Suggested label: "The broker injects the credential into the tool that uses it; the model holds only a reference."
Related doc drift: site/content/docs/giving-credentials.mdx:46 says "The runner process never sees a credential's bytes", which is false for MCP.
There was a problem hiding this comment.
Addressed in 938c9e5. Card body: "Credentials are resolved by a broker outside the model and injected into the tool that needs them; the model never sees them." Label: "The broker injects the credential into the tool that uses it; the model holds only a reference." giving-credentials.mdx now says the model never sees the bytes and notes when MCP servers and sidecars get them.
| const alone = clamp((d - 160) / 30); | ||
| q(r, "af").setAttribute("opacity", alone); | ||
| q(r, "ag").style.stroke = alone > 0.2 ? ACC : ""; | ||
| setText(q(r, "subj"), d < 158 ? "class ∩ user:maya" : "class ∩ service:nightly"); |
There was a problem hiding this comment.
[Inaccurate] Lens figure: class ∩ service:nightly
service is a deliberately relation-less type, and no tuple ever names one. pkg/authz/spicedb/schema/schema.zed:84-94: "It is DELIBERATELY relation-less … It exists so that a check against a service subject RESOLVES — returning no permission". loop_subjects.go:100-103 says the same: a service subject "could be REFUSED precisely and never ALLOWED anything".
So there's no service permission set to intersect with the class. An unattended run's authority comes from slot grants on the session (check_tool_call.go:433-439, slot fillFrom: trigger), and by default the session acts as itself (agentsession:<ns>/<name>). service: is the override (schema.zed:74-82, loop_subjects.go:88-90).
Suggested: for the unattended state, show agentsession:nightly with authority from the class's slot grants. Or keep service:nightly with a caption like "no standing of its own; only the session's slot grants". Update the aria label in Landing.tsx (L177-178), which says "as the service subject its channel declares".
There was a problem hiding this comment.
Addressed in 938c9e5. service:nightly is gone. The figure is now conceptual: unattended, the whole agent circle lights ("agent · full scope"), and a present user narrows it to the overlap. The aria label is updated to match.
| X on the way out. */ | ||
| { | ||
| const DENY = "var(--deny)"; | ||
| const calls = [["git clone", 1], ["gh pr view", 1], ["git push", 0], ["gh pr checks", 1], ["gh pr comment", 0], ["gh api", 1]]; |
There was a problem hiding this comment.
[Misleading] Gate figure: gh api → allow, next to gh pr comment → deny
The six names match reviewbot's real toolspec allowlists (examples/reviewbot/manifests/toolspecs.yaml:67, :104). But gh api is only allowed there under a strict CEL constraint: GET only, no method/field/input flags, and an endpoint regex limited to check-run and PR read endpoints (toolspecs.yaml:151). The same file warns that unconstrained gh api "will POST anything the token permits" (:106-108). As drawn, "gh api: allow" undercuts "gh pr comment: deny", because gh api can post comments.
Suggested: ["gh api GET", 1], plus a ["gh api -X POST", 0] entry, which tells the read-only story better.
There was a problem hiding this comment.
Addressed in 938c9e5. The figure now shows gh api GET allowed and adds gh api -X POST denied.
| const d = triD(x, base, sh); | ||
| b += `<path data-k="sf${k}" d="${d}" style="fill:url(#geo-g-define)" opacity="0"/><path data-k="s${k}" class="i" d="${d}"/><path class="f" d="${holeD(x, base, sh)}"/>`; | ||
| }); | ||
| b += T(250, 122, "agentclass") + T(250, 140, "in git") + Tk("lbl", 200, 326, "session", "ac", "middle"); |
There was a problem hiding this comment.
[Misleading] Define figure: "in git", and "own … memory" in the footer (L158)
- "in git": an AgentClass is a cluster CRD. Nothing reads it from git. The docs only say you can keep the YAML in git (
site/content/docs/oap-agent.mdx:182). The real distribution path is an.oapbundle (packaging-oap.mdx:22). The card body (Landing.tsx:143, "kept in git") makes the same claim. - "own pod · budget · memory": pods ✅ (
agentsession/podspec.go:320-322; really several per session). Budget ✅ (agentsession_types.go:60-63). But memory also lives at user scope (pkg/memory/userscope.go:13) and resource scope (resourcescope.go:9), beyond the session. The docs' own phrase is "its own workspace, its own budget, and its own audit trail" (agentclass-agentsession.mdx).
Suggested: "in git" becomes "versioned YAML" (or "reviewed · pinned"). The footer becomes "own pods · budget · audit trail".
There was a problem hiding this comment.
Addressed in 938c9e5. "in git" becomes "versioned" (we're avoiding a YAML connotation), and the footer becomes "own runtime · budget · audit trail". The card body and aria label now match.
|
|
||
| /* Carry: a scheduled run, a GitHub webhook, a Slack thread. Each wakes a session. */ | ||
| { | ||
| const src = [[70, 108, "cron"], [70, 200, "github"], [70, 292, "slack"]], seq = [0, 1, 0, 2], tx = 300, tb = 236, th = 104; |
There was a problem hiding this comment.
[Misleading] Channels figure: there's no cron channel kind
The channel kind enum is fake;slack;bento;local;browser;github;agent (pkg/apis/v1alpha1/channel_types.go:49). Scheduled runs are the bento kind (pkg/channels/channelkinds/bento/kind.go:1-3, "Cron-style triggers without a human user"). Calling a schedule a channel is accurate. "cron" just shouldn't read like a kind name.
GitHub and Slack waking sessions is accurate (github/kind.go:215-218, slack/kind.go:466-469).
Suggested: label it "schedule". Update the aria label in Landing.tsx (L187) to match: "A schedule, GitHub and Slack each wake a session".
There was a problem hiding this comment.
Addressed in 938c9e5. The source is labeled "schedule", and the aria label reads "A schedule, GitHub and Slack each wake a session".
| }); | ||
| b += `<path class="i" d="${triD(gate, 238, 84)}" style="fill:var(--card)"/><path data-k="hole" class="i" d="${holeD(gate, 238, 84)}"/>`; | ||
| b += agent(ag[0], ag[1], 17) + `<circle data-k="m" class="pf" r="4" cx="-10" cy="-10"/>`; | ||
| b += T(ag[0] - 36, ag[1] - 32, "company:acme", "ac", "start") + Tk("vd", gate, 270, "", "", "middle"); |
There was a problem hiding this comment.
[Misleading] Memory figure: company:acme, and "one slot"
companyisn't a type in the shipped schema. The shipped example usescrm_company(examples/hubspot-companies/manifests/mcpserver.yaml:92), and tests usecrm_company:acme(pkg/authz/guardian/schema/slots_test.go:31-32).- A session isn't committed to exactly one slot. It reads the pool of every resource it holds a slot grant on (
pkg/memory/pools/reader.go:41-48). Refusing an unheld resource is accurate (search/searcher.go:56-74,memory/approval.go:85-100), and so is "reading memory is a check" (spicedbauthorizer/authorizer.go:186-258).
Suggested: use crm_company:acme (and the same prefix for the slabs). Change the aria label (Landing.tsx L196-197) to "A session reads the memory of resources it's bound to; reading one it holds no grant on is refused".
There was a problem hiding this comment.
Addressed in 938c9e5. The agent label is now the plainer "bound to acme", and the aria label reads "A session reads memory only for the resources it's bound to; reading another is refused".
|
|
||
| /* Problem: one agent holding everyone's access, each call needing one slice. */ | ||
| { | ||
| const D = 76, R = 46, users = ["maya", "jon", "priya", "sam", "leo"]; |
There was a problem hiding this comment.
[Minor] Union figure shows a different problem than the copy beside it
The copy ("A token for one repository usually reaches every repository", which matches README.md:37-40) frames the problem as one credential broader than one task. The figure frames it as one agent holding the union of many people's access. Both are real, and the per-requester check is real (loop_subjects.go:218-243), but they're different problems shown side by side.
One caveat for any OAP-side copy: in the default agent identity mode, tools still authenticate with the agent's own credential. OAP's fix is that each call is checked against the person who asked, not that the agent acts as each user (site/content/docs/identity-modes.mdx). The subject also advances per turn, not strictly per call (loop_subjects.go:222-224).
Suggested: align one with the other. For example, change the copy to "A shared agent usually holds everyone's access at once, while each request comes from one person."
(The name "sam" is fine. Existing docs use it as a fictional requester, e.g. shared-permissions.mdx:29.)
There was a problem hiding this comment.
Addressed in 938c9e5. The figure now follows the copy: the circles are repositories ("token reaches ∪ repos", "this task needs repo:api").
There was a problem hiding this comment.
Follow-up: ac78026 reworked this figure again. The label is now "token access: 5 repos"; all five access lines draw in, the one the task needs (api) lights, and the other four turn red as "unneeded access: 4 repos".
Address the accuracy review on the landing page redesign. The figures stay
conceptual, but no longer claim things OAP doesn't do:
- Depth: reorder layers to plan, slot, call check, tool spec, sandbox; the
closing copy says the model never sees a credential.
- Broker: "handle" becomes "reference"; copy is framed around the model.
- Lens: labels follow left-to-right order, the person and agent sets read
apart, and unattended the agent's full scope lights.
- Gate: gh api GET allowed, gh api -X POST denied.
- Define, channels, memory, union: plainer labels ("versioned", "own
runtime · budget · audit trail", "schedule", "bound to acme") and the
union figure now shows repositories to match its copy.
- Copy: sandboxes are per bundle, not per tool; bus credentials,
pluggable parts and micro-VM scope are stated as they are.
README.md, docs/security-model.md and giving-credentials.mdx carried the
same credential and sandbox claims and are corrected to match.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
938c9e5 addresses the accuracy review. Each inline thread has a reply. The items from the review summary are done too:
A second pass over all rendered figures and copy found four more issues, all fixed:
The site checks (lint, typecheck, test, build) and 🤖 Generated with Claude Code |
- Problem figure: lines show what the token can access. All five repos draw in, the one the task needs lights, and the other four turn red as unneeded access. Labels sit clear of their circles. - Card bodies for Broker, Decide, Carry and Remember now carry the core idea of each primitive's definition: an agent acts as itself or for a person, the agent asks and never answers, every message and click is checked, and one store holds memory and a signed record. - "Scroll to deal" becomes "Scroll to view". - The OWASP lede points to each item's write-up instead of a disclaimer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| ### Isolated sandboxes, credentials injected per tool | ||
|
|
||
| **Each tool runs in its own sandbox and holds only its own credentials.** | ||
| **Tools run in sandboxes apart from the agent, and a credential is injected only |
There was a problem hiding this comment.
Tools run in sandboxes kept independent from the agent, and a credential is injected only for the specific tool that needs it - different tools do not see credentials for other tools
There was a problem hiding this comment.
Adopted in f8fe21c. The bold lead is your line, and the body now says a tool can be a tool bundle: operations talking to the same service share one sandbox and its credentials.
Take the reviewer's lead sentence: tools run in sandboxes kept independent from the agent, a credential is injected only for the tool that needs it, and different tools do not see each other's credentials. The body notes that a tool can be a tool bundle, whose operations talk to one service and share its sandbox and credentials. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Motivation
The landing page is moving to a new design, delivered as a static mockup (
oap-landing.html). This PR ports that mockup onto the live Next.js page as designed. Copy and diagrams are carried over faithfully, including claims that a code fact-check found inaccurate. Those findings are posted as review comments on this PR, so the port and the corrections can be reviewed separately.Summary of changes
#problem,#area-1/2/6). The problem section now has text beside a diagram. The primitives are a list beside a card deck that deals as you scroll. Security areas are tabs addressable by URL hash (#area-N), and every panel renders without JS. Agent Builder shows numbered steps beside the clip. "Everything else" is an icon grid. The closing section has a defense-in-depth diagram.next-themesstate with the footer toggle.site/app/(landing)/geo/figures.jsports the design study's geometric figures unchanged, with a header noting they're hand-authored illustrations.GeoFigure.tsxis the typed boundary: it server-renders the SVG, and animates only visible, unpaused figures on one shared timer, honoringprefers-reduced-motion.owasp.ts.content/docs/owasp-top10.mdxis unchanged.OapMark.tsxandThemeToggle.tsxexport their paths and icons for reuse.Alternatives considered
Provenance
Ship gate
Results: not run. The change is confined to
site/, which is outside the Go suites. Site gate fromsite/:Visual check: the production build was compared against the mockup in Chrome. At 1440px dark, every section's height matched exactly. At 390px light, all sections matched except the questions band, which is 16px taller: the mockup alternates 12px/16px padding there because of a CSS specificity accident, and this PR normalizes it. Other checks: no horizontal scroll at 390px, no console or hydration errors,
/#area-2selects tab 2, and the deck's card depths match the mockup mid-scroll.Regeneration
+kubebuilder:rbacmarker or a CRD-shaping field → n/aconfig/**→ n/amage fmt:alluntilmage fmt:checkis cleanCoverage
Before requesting review
figures.jsis a verbatim port of the mockup's script. Diff it against the mockup to check it rather than reading the geometry.🤖 Generated with Claude Code