22package main
33
44import (
5+ "bufio"
56 "context"
67 "encoding/json"
78 "fmt"
@@ -43,7 +44,7 @@ SkillPacks, and feature gates in your Kubernetes cluster.`,
4344 PersistentPreRunE : func (cmd * cobra.Command , args []string ) error {
4445 // Skip K8s client init for commands that don't need it.
4546 switch cmd .Name () {
46- case "version" , "install" , "uninstall" :
47+ case "version" , "install" , "uninstall" , "onboard" :
4748 return nil
4849 }
4950 return initClient ()
@@ -56,6 +57,7 @@ SkillPacks, and feature gates in your Kubernetes cluster.`,
5657 rootCmd .AddCommand (
5758 newInstallCmd (),
5859 newUninstallCmd (),
60+ newOnboardCmd (),
5961 newInstancesCmd (),
6062 newRunsCmd (),
6163 newPoliciesCmd (),
@@ -404,6 +406,327 @@ const (
404406 manifestAsset = "k8sclaw-manifests.tar.gz"
405407)
406408
409+ // ── Onboard ──────────────────────────────────────────────────────────────────
410+
411+ func newOnboardCmd () * cobra.Command {
412+ return & cobra.Command {
413+ Use : "onboard" ,
414+ Short : "Interactive setup wizard for K8sClaw" ,
415+ Long : `Walks you through creating your first ClawInstance, connecting a
416+ channel (Telegram, Slack, Discord, or WhatsApp), setting up your AI provider
417+ credentials, and optionally applying a default ClawPolicy.` ,
418+ RunE : func (cmd * cobra.Command , args []string ) error {
419+ return runOnboard ()
420+ },
421+ }
422+ }
423+
424+ func runOnboard () error {
425+ reader := bufio .NewReader (os .Stdin )
426+
427+ printBanner ()
428+
429+ // ── Step 1: Check K8sClaw is installed ───────────────────────────────
430+ fmt .Println ("\n 📋 Step 1/5 — Checking cluster..." )
431+ if err := initClient (); err != nil {
432+ fmt .Println ("\n ❌ Cannot connect to your cluster." )
433+ fmt .Println (" Make sure kubectl is configured and run: k8sclaw install" )
434+ return err
435+ }
436+
437+ // Quick health check: can we list CRDs?
438+ ctx := context .Background ()
439+ var instances k8sclawv1alpha1.ClawInstanceList
440+ if err := k8sClient .List (ctx , & instances , client .InNamespace (namespace )); err != nil {
441+ fmt .Println ("\n ❌ K8sClaw CRDs not found. Run 'k8sclaw install' first." )
442+ return fmt .Errorf ("CRDs not installed: %w" , err )
443+ }
444+ fmt .Println (" ✅ K8sClaw is installed and CRDs are available." )
445+
446+ // ── Step 2: Instance name ────────────────────────────────────────────
447+ fmt .Println ("\n 📋 Step 2/5 — Create your ClawInstance" )
448+ fmt .Println (" An instance represents you (or a tenant) in the system." )
449+ instanceName := prompt (reader , " Instance name" , "my-agent" )
450+
451+ // ── Step 3: AI provider ──────────────────────────────────────────────
452+ fmt .Println ("\n 📋 Step 3/5 — AI Provider" )
453+ fmt .Println (" Which model provider do you want to use?" )
454+ fmt .Println (" 1) OpenAI" )
455+ fmt .Println (" 2) Anthropic" )
456+ fmt .Println (" 3) Other / bring-your-own" )
457+ providerChoice := prompt (reader , " Choice [1-3]" , "1" )
458+
459+ var providerName , secretEnvKey , modelName string
460+ switch providerChoice {
461+ case "2" :
462+ providerName = "anthropic"
463+ secretEnvKey = "ANTHROPIC_API_KEY"
464+ modelName = prompt (reader , " Model name" , "claude-sonnet-4-20250514" )
465+ case "3" :
466+ providerName = prompt (reader , " Provider name" , "custom" )
467+ secretEnvKey = prompt (reader , " API key env var name" , "API_KEY" )
468+ modelName = prompt (reader , " Model name" , "" )
469+ default :
470+ providerName = "openai"
471+ secretEnvKey = "OPENAI_API_KEY"
472+ modelName = prompt (reader , " Model name" , "gpt-4o" )
473+ }
474+
475+ apiKey := promptSecret (reader , fmt .Sprintf (" %s" , secretEnvKey ))
476+ if apiKey == "" {
477+ fmt .Println (" ⚠ No API key provided — you can add it later:" )
478+ fmt .Printf (" kubectl create secret generic %s-%s-key --from-literal=%s=<key>\n " ,
479+ instanceName , providerName , secretEnvKey )
480+ }
481+
482+ providerSecretName := fmt .Sprintf ("%s-%s-key" , instanceName , providerName )
483+
484+ // ── Step 4: Channel ──────────────────────────────────────────────────
485+ fmt .Println ("\n 📋 Step 4/5 — Connect a Channel (optional)" )
486+ fmt .Println (" Channels let your agent receive messages from external platforms." )
487+ fmt .Println (" 1) Telegram — easiest, just talk to @BotFather" )
488+ fmt .Println (" 2) Slack" )
489+ fmt .Println (" 3) Discord" )
490+ fmt .Println (" 4) WhatsApp" )
491+ fmt .Println (" 5) Skip — I'll add a channel later" )
492+ channelChoice := prompt (reader , " Choice [1-5]" , "5" )
493+
494+ var channelType , channelTokenKey , channelToken string
495+ switch channelChoice {
496+ case "1" :
497+ channelType = "telegram"
498+ channelTokenKey = "TELEGRAM_BOT_TOKEN"
499+ fmt .Println ("\n 💡 Get a bot token from https://t.me/BotFather" )
500+ channelToken = promptSecret (reader , " Bot Token" )
501+ case "2" :
502+ channelType = "slack"
503+ channelTokenKey = "SLACK_BOT_TOKEN"
504+ fmt .Println ("\n 💡 Create a Slack app at https://api.slack.com/apps" )
505+ channelToken = promptSecret (reader , " Bot OAuth Token" )
506+ case "3" :
507+ channelType = "discord"
508+ channelTokenKey = "DISCORD_BOT_TOKEN"
509+ fmt .Println ("\n 💡 Create a Discord app at https://discord.com/developers/applications" )
510+ channelToken = promptSecret (reader , " Bot Token" )
511+ case "4" :
512+ channelType = "whatsapp"
513+ channelTokenKey = "WHATSAPP_ACCESS_TOKEN"
514+ fmt .Println ("\n 💡 Set up the WhatsApp Cloud API at https://developers.facebook.com" )
515+ channelToken = promptSecret (reader , " Access Token" )
516+ default :
517+ channelType = ""
518+ }
519+
520+ channelSecretName := fmt .Sprintf ("%s-%s-secret" , instanceName , channelType )
521+
522+ // ── Step 5: Apply default policy? ────────────────────────────────────
523+ fmt .Println ("\n 📋 Step 5/5 — Default Policy" )
524+ fmt .Println (" A ClawPolicy controls what tools agents can use, sandboxing, etc." )
525+ applyPolicy := promptYN (reader , " Apply the default policy?" , true )
526+
527+ // ── Summary ──────────────────────────────────────────────────────────
528+ fmt .Println ("\n ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
529+ fmt .Println (" Summary" )
530+ fmt .Println ("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
531+ fmt .Printf (" Instance: %s (namespace: %s)\n " , instanceName , namespace )
532+ fmt .Printf (" Provider: %s (model: %s)\n " , providerName , modelName )
533+ if channelType != "" {
534+ fmt .Printf (" Channel: %s\n " , channelType )
535+ } else {
536+ fmt .Println (" Channel: (none)" )
537+ }
538+ fmt .Printf (" Policy: %v\n " , applyPolicy )
539+ fmt .Println ("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" )
540+
541+ if ! promptYN (reader , "\n Proceed?" , true ) {
542+ fmt .Println (" Aborted." )
543+ return nil
544+ }
545+
546+ // ── Apply resources ──────────────────────────────────────────────────
547+ fmt .Println ()
548+
549+ // 1. Create AI provider secret.
550+ if apiKey != "" {
551+ fmt .Printf (" Creating secret %s...\n " , providerSecretName )
552+ // Delete first to allow re-runs.
553+ _ = kubectl ("delete" , "secret" , providerSecretName , "-n" , namespace , "--ignore-not-found" )
554+ if err := kubectl ("create" , "secret" , "generic" , providerSecretName ,
555+ "-n" , namespace ,
556+ fmt .Sprintf ("--from-literal=%s=%s" , secretEnvKey , apiKey )); err != nil {
557+ return fmt .Errorf ("create provider secret: %w" , err )
558+ }
559+ }
560+
561+ // 2. Create channel secret.
562+ if channelType != "" && channelToken != "" {
563+ fmt .Printf (" Creating secret %s...\n " , channelSecretName )
564+ _ = kubectl ("delete" , "secret" , channelSecretName , "-n" , namespace , "--ignore-not-found" )
565+ if err := kubectl ("create" , "secret" , "generic" , channelSecretName ,
566+ "-n" , namespace ,
567+ fmt .Sprintf ("--from-literal=%s=%s" , channelTokenKey , channelToken )); err != nil {
568+ return fmt .Errorf ("create channel secret: %w" , err )
569+ }
570+ }
571+
572+ // 3. Apply default policy.
573+ policyName := "default-policy"
574+ if applyPolicy {
575+ fmt .Println (" Applying default ClawPolicy..." )
576+ policyYAML := buildDefaultPolicyYAML (policyName , namespace )
577+ if err := kubectlApplyStdin (policyYAML ); err != nil {
578+ return fmt .Errorf ("apply policy: %w" , err )
579+ }
580+ }
581+
582+ // 4. Create ClawInstance.
583+ fmt .Printf (" Creating ClawInstance %s...\n " , instanceName )
584+ instanceYAML := buildClawInstanceYAML (instanceName , namespace , modelName ,
585+ providerName , providerSecretName , channelType , channelSecretName ,
586+ policyName , applyPolicy )
587+ if err := kubectlApplyStdin (instanceYAML ); err != nil {
588+ return fmt .Errorf ("apply instance: %w" , err )
589+ }
590+
591+ // ── Done ─────────────────────────────────────────────────────────────
592+ fmt .Println ("\n ✅ Onboarding complete!" )
593+ fmt .Println ()
594+ fmt .Println (" Next steps:" )
595+ fmt .Println (" ─────────────────────────────────────────────────" )
596+ fmt .Printf (" • Check your instance: k8sclaw instances get %s\n " , instanceName )
597+ if channelType == "telegram" {
598+ fmt .Println (" • Send a message to your Telegram bot — it's live!" )
599+ }
600+ fmt .Printf (" • Run an agent: kubectl apply -f config/samples/agentrun_sample.yaml\n " )
601+ fmt .Printf (" • View runs: k8sclaw runs list\n " )
602+ fmt .Printf (" • Feature gates: k8sclaw features list --policy %s\n " , policyName )
603+ fmt .Println ()
604+ return nil
605+ }
606+
607+ func printBanner () {
608+ fmt .Println ()
609+ fmt .Println (" ╔═══════════════════════════════════════════╗" )
610+ fmt .Println (" ║ K8sClaw · Onboarding Wizard ║" )
611+ fmt .Println (" ╚═══════════════════════════════════════════╝" )
612+ }
613+
614+ // prompt shows a prompt with an optional default and returns the user's input.
615+ func prompt (reader * bufio.Reader , label , defaultVal string ) string {
616+ if defaultVal != "" {
617+ fmt .Printf ("%s [%s]: " , label , defaultVal )
618+ } else {
619+ fmt .Printf ("%s: " , label )
620+ }
621+ line , _ := reader .ReadString ('\n' )
622+ line = strings .TrimSpace (line )
623+ if line == "" {
624+ return defaultVal
625+ }
626+ return line
627+ }
628+
629+ // promptSecret reads input without showing a default.
630+ func promptSecret (reader * bufio.Reader , label string ) string {
631+ fmt .Printf ("%s: " , label )
632+ line , _ := reader .ReadString ('\n' )
633+ return strings .TrimSpace (line )
634+ }
635+
636+ // promptYN asks a yes/no question.
637+ func promptYN (reader * bufio.Reader , label string , defaultYes bool ) bool {
638+ hint := "Y/n"
639+ if ! defaultYes {
640+ hint = "y/N"
641+ }
642+ fmt .Printf ("%s [%s]: " , label , hint )
643+ line , _ := reader .ReadString ('\n' )
644+ line = strings .TrimSpace (strings .ToLower (line ))
645+ if line == "" {
646+ return defaultYes
647+ }
648+ return line == "y" || line == "yes"
649+ }
650+
651+ func buildDefaultPolicyYAML (name , ns string ) string {
652+ return fmt .Sprintf (`apiVersion: k8sclaw.io/v1alpha1
653+ kind: ClawPolicy
654+ metadata:
655+ name: %s
656+ namespace: %s
657+ spec:
658+ toolGating:
659+ defaultAction: allow
660+ rules:
661+ - tool: exec_command
662+ action: ask
663+ - tool: write_file
664+ action: allow
665+ conditions:
666+ maxCallsPerRun: 100
667+ - tool: network_request
668+ action: deny
669+ execGating:
670+ allowShell: true
671+ allowedCommands: [git, npm, go, python, make]
672+ deniedCommands: ["rm -rf /", curl, wget]
673+ maxExecTime: "30s"
674+ subagentPolicy:
675+ maxDepth: 3
676+ maxConcurrent: 5
677+ inheritTools: true
678+ sandboxPolicy:
679+ required: false
680+ defaultImage: ghcr.io/alexsjones/k8sclaw/sandbox:latest
681+ maxCPU: "4"
682+ maxMemory: 8Gi
683+ featureGates:
684+ browser-automation: false
685+ code-execution: true
686+ file-access: true
687+ ` , name , ns )
688+ }
689+
690+ func buildClawInstanceYAML (name , ns , model , provider , providerSecret ,
691+ channelType , channelSecret , policyName string , hasPolicy bool ) string {
692+
693+ var channelsBlock string
694+ if channelType != "" {
695+ channelsBlock = fmt .Sprintf (` channels:
696+ - type: %s
697+ configRef:
698+ secret: %s
699+ ` , channelType , channelSecret )
700+ }
701+
702+ var policyBlock string
703+ if hasPolicy {
704+ policyBlock = fmt .Sprintf (" policyRef: %s\n " , policyName )
705+ }
706+
707+ return fmt .Sprintf (`apiVersion: k8sclaw.io/v1alpha1
708+ kind: ClawInstance
709+ metadata:
710+ name: %s
711+ namespace: %s
712+ spec:
713+ %s agents:
714+ default:
715+ model: %s
716+ authRefs:
717+ - provider: %s
718+ secret: %s
719+ %s` , name , ns , channelsBlock , model , provider , providerSecret , policyBlock )
720+ }
721+
722+ func kubectlApplyStdin (yaml string ) error {
723+ cmd := exec .Command ("kubectl" , "apply" , "-f" , "-" )
724+ cmd .Stdin = strings .NewReader (yaml )
725+ cmd .Stdout = os .Stdout
726+ cmd .Stderr = os .Stderr
727+ return cmd .Run ()
728+ }
729+
407730func newInstallCmd () * cobra.Command {
408731 var manifestVersion string
409732 cmd := & cobra.Command {
0 commit comments