-
Notifications
You must be signed in to change notification settings - Fork 42
feat(kiloclaw) "one click" install of a ClawByte into KiloClaw instance #3651
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
St0rmz1
wants to merge
23
commits into
main
Choose a base branch
from
add-byte-install-route
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,331
−34
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
50300ff
wip: clawbyte install flow — signed-payload verification + kilo-chat …
St0rmz1 9908224
Merge remote-tracking branch 'origin/main' into add-byte-install-route
St0rmz1 523dc79
fix(install): address local review findings
St0rmz1 35c2c23
wip: cloud → kilo-chat HTTP client for internal post-message-as-user
St0rmz1 946301a
fix(install): address local-review findings on the WIP install flow
St0rmz1 b1ea132
wip: installFromSource tRPC mutation + dispatch logic
St0rmz1 7ba4ada
fix(install): runtime sandbox id + shared params schema
St0rmz1 800b862
refactor(install): one-click install — server-component dispatch only
St0rmz1 9839895
wip(install): preview + explicit-POST install flow, SSRF hardening
St0rmz1 8ed73a2
fix(install): address local review — sign-only payload + schema sourc…
St0rmz1 5e455ec
Merge remote-tracking branch 'origin/main' into add-byte-install-route
St0rmz1 795f71b
refactor(install): drop cross-funnel intent persistence
St0rmz1 cd0515f
add tests
St0rmz1 d1a478c
address possible message validation drift
St0rmz1 fa5c81f
feat(install): confirmation-card UX + dedicated
St0rmz1 30d0f99
fix(install): bind dispatch to the reviewed
St0rmz1 da361d3
fix lint
St0rmz1 ad9c19b
Merge remote-tracking branch 'origin/main' into add-byte-install-route
St0rmz1 bff9cab
fix kilobot findings
St0rmz1 0f29848
fix(kilo-chat): re-sync plugin shared schemas
St0rmz1 7c77b0d
fixes
St0rmz1 c7f44bf
getKiloChatBaseUrl() now parses the resolved URL and, if the origin…
St0rmz1 e04e826
fix cache undermining the signature binding
St0rmz1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
apps/web/src/app/(app)/claw/install/[source]/[slug]/InstallClient.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| 'use client'; | ||
|
|
||
| import { useState } from 'react'; | ||
| import { useRouter } from 'next/navigation'; | ||
| import { useMutation } from '@tanstack/react-query'; | ||
| import { Loader2 } from 'lucide-react'; | ||
| import { toast } from 'sonner'; | ||
| import { TRPCClientError } from '@trpc/client'; | ||
| import { useTRPC } from '@/lib/trpc/utils'; | ||
| import { Button } from '@/components/ui/button'; | ||
| import { Badge } from '@/components/ui/badge'; | ||
| import { | ||
| Card, | ||
| CardHeader, | ||
| CardTitle, | ||
| CardDescription, | ||
| CardContent, | ||
| CardFooter, | ||
| } from '@/components/ui/card'; | ||
| import KiloClawbsterIcon from '@/components/icons/KiloClawbsterIcon'; | ||
| import type { InstallPayload } from '@/lib/kiloclaw/install'; | ||
| import type { InstallSource } from '@/lib/kiloclaw/install-sources'; | ||
|
|
||
| type InstallClientProps = { | ||
| source: InstallSource; | ||
| sourceLabel: string; | ||
| payload: InstallPayload; | ||
| }; | ||
|
|
||
| /** | ||
| * Install confirmation. The signed payload was fetched and verified | ||
| * server-side (and paid-access gated) in `page.tsx`; this is a permission-style | ||
| * confirm screen ("you're installing X from kilo.ai" plus what it does and the | ||
| * description) that the user explicitly confirms or cancels. | ||
| * | ||
| * Dispatch happens ONLY on an explicit Confirm click, which fires the | ||
| * `installFromSource` POST mutation, never on GET render. A cross-site POST | ||
| * can't carry the SameSite session cookie, so this closes the CSRF / | ||
| * lure-a-click class that a GET-dispatch route would re-open. | ||
| */ | ||
| export function InstallClient({ source, sourceLabel, payload }: InstallClientProps) { | ||
| const router = useRouter(); | ||
| const trpc = useTRPC(); | ||
| // `navigating` keeps the buttons disabled through the post-success redirect | ||
| // so a double-click can't fire a second dispatch while the route changes. | ||
| const [navigating, setNavigating] = useState(false); | ||
| const install = useMutation(trpc.kiloclaw.installFromSource.mutationOptions()); | ||
|
|
||
| async function onInstall() { | ||
| try { | ||
| const result = await install.mutateAsync({ | ||
| source, | ||
| slug: payload.slug, | ||
| // Bind the dispatch to the exact payload shown here; the server rejects | ||
| // if the byte changed since this page rendered. | ||
| signature: payload.signature, | ||
| }); | ||
| setNavigating(true); | ||
| if (result.ok) { | ||
| // Open the conversation the dispatch created, so the user lands | ||
| // directly in the installed chat, not the blank conversation index. | ||
| router.push(`/claw/chat/${result.conversationId}`); | ||
| return; | ||
| } | ||
| // No active instance yet, so send them to set one up. We intentionally do | ||
| // NOT persist the install intent across the (long, multi-step) onboarding | ||
| // flow; the user finishes setup, then installs again from the byte page. | ||
| router.push('/claw/new'); | ||
| } catch (err) { | ||
| let message = 'Could not install this byte. Please try again.'; | ||
| if (err instanceof TRPCClientError) { | ||
| if (err.data?.code === 'NOT_FOUND') { | ||
| message = 'This install link is no longer available.'; | ||
| } else if (err.data?.code === 'CONFLICT') { | ||
| message = 'This byte changed since you opened this page. Please reload and try again.'; | ||
| } | ||
| } | ||
| toast.error(message); | ||
| } | ||
| } | ||
|
|
||
| const busy = install.isPending || navigating; | ||
|
|
||
| return ( | ||
| <div className="mx-auto flex min-h-[60vh] w-full max-w-lg items-center px-6 py-12"> | ||
| <Card className="w-full text-left"> | ||
| <CardHeader> | ||
| <Badge variant="secondary" className="w-fit gap-1.5"> | ||
| <KiloClawbsterIcon className="h-4 w-auto" /> | ||
| {sourceLabel} | ||
| </Badge> | ||
| <CardTitle className="mt-2 text-xl break-words">{payload.title}</CardTitle> | ||
| <CardDescription className="leading-relaxed"> | ||
| You’re installing a {sourceLabel} from kilo.ai. Clicking Confirm Install starts a new | ||
| KiloClaw conversation and runs its prompt on your behalf. If you don’t want to install | ||
| this, then click Cancel. | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <p className="text-muted-foreground text-sm font-medium"> | ||
| This {sourceLabel} installs a skill to: | ||
| </p> | ||
| <p className="text-foreground mt-2 text-sm leading-relaxed break-words"> | ||
| {payload.description} | ||
| </p> | ||
| </CardContent> | ||
| <CardFooter className="justify-end gap-3"> | ||
| <Button | ||
| type="button" | ||
| variant="ghost" | ||
| disabled={busy} | ||
| onClick={() => router.push('/claw/chat')} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button type="button" onClick={onInstall} disabled={busy}> | ||
| {busy ? <Loader2 className="h-4 w-4 animate-spin" /> : null} | ||
| {busy ? 'Installing…' : 'Confirm Install'} | ||
| </Button> | ||
| </CardFooter> | ||
| </Card> | ||
| </div> | ||
| ); | ||
| } | ||
61 changes: 61 additions & 0 deletions
61
apps/web/src/app/(app)/claw/install/[source]/[slug]/page.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { notFound, redirect } from 'next/navigation'; | ||
| import { TRPCError } from '@trpc/server'; | ||
| import { getUserFromAuthOrRedirect } from '@/lib/user/server'; | ||
| import { requireKiloClawAccess } from '@/lib/kiloclaw/access-gate'; | ||
| import { fetchInstallPayload } from '@/lib/kiloclaw/install'; | ||
| import { INSTALL_SOURCES, isInstallSource } from '@/lib/kiloclaw/install-sources'; | ||
| import { InstallClient } from './InstallClient'; | ||
|
|
||
| type InstallPageProps = { | ||
| params: Promise<{ source: string; slug: string }>; | ||
| }; | ||
|
|
||
| /** | ||
| * One-click install preview for a signed source payload (ClawByte today, | ||
| * more sources later). Rendered as a Server Component — loading this page | ||
| * does NO install work. The actual chat dispatch happens only on an explicit | ||
| * Install click in `InstallClient`, which fires the `installFromSource` POST | ||
| * mutation. That split is load-bearing: a GET must never dispatch, or a | ||
| * third-party page could pop a prompt into a user's chat just by getting | ||
| * them to load the URL (CSRF / lure-a-click). | ||
| * | ||
| * Gating, in order: | ||
| * 1. Auth — the parent claw layout (`getUserFromAuthOrRedirect`) bounces | ||
| * unauth users to sign-in; `callbackPath` preserves this pathname so they | ||
| * return here after signing in. We call it again to get the user id. | ||
| * 2. Active paid access — fetching + verifying the signed byte is paid-user | ||
| * compute (outbound HTTP + Ed25519 verify). A logged-in user without an | ||
| * active subscription/trial must NOT be able to trigger it, so we gate | ||
| * before the fetch and route no-access users into the subscribe/provision | ||
| * funnel (`/claw/new`) instead of pulling the byte. | ||
| * 3. Payload fetch + verify — only after the access gate passes. | ||
| * | ||
| * Unknown source / unsigned byte / failed verification / slug mismatch → | ||
| * `notFound()` (404). All cases logged in detail by `fetchInstallPayload`. | ||
| */ | ||
| export default async function InstallPage({ params }: InstallPageProps) { | ||
| const { source, slug } = await params; | ||
| if (!isInstallSource(source)) notFound(); | ||
|
|
||
| const user = await getUserFromAuthOrRedirect(); | ||
|
|
||
| try { | ||
| await requireKiloClawAccess(user.id); | ||
| } catch (err) { | ||
| // No active subscription/trial → don't pull the byte. Send them to the | ||
| // subscribe/provision flow (which presents the marketing/sign-up page). | ||
| // We intentionally don't persist install intent across that flow; the user | ||
| // installs again from the byte page once they're set up. | ||
| if (err instanceof TRPCError && err.code === 'FORBIDDEN') { | ||
| redirect('/claw/new'); | ||
| } | ||
| throw err; | ||
| } | ||
|
|
||
| const payload = await fetchInstallPayload(source, slug); | ||
| if (!payload) notFound(); | ||
|
|
||
| return ( | ||
| <InstallClient source={source} sourceLabel={INSTALL_SOURCES[source].label} payload={payload} /> | ||
| ); | ||
| } |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested/nit: This badge icon is decorative, but the SVG is exposed to assistive tech without a label. Mark this use
aria-hidden="true"andfocusable="false"by passing those props throughKiloClawbsterIcon, or hide the SVG at this call site.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
KiloClawbsterIcon's svg now setsaria-hidden="true"andfocusable="false", so the decorative badge icon is hidden from assistive tech.