From 899caeef617027445e1edc88b8350ba77b19c5ec Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 30 Jan 2026 05:09:37 +0000 Subject: [PATCH 1/9] CLI: Update SDK to v0.29.0 and add 1280x800@60 viewport support - Updated kernel-go-sdk from v0.28.0 to v0.29.0 - Added 1280x800@60 to available viewport configurations to match SDK update - Updated viewport test to reflect the new viewport option SDK release notes: - Add support for 1280x800@60 viewport - Add convenient param.SetJSON helper --- cmd/browsers.go | 1 + cmd/browsers_test.go | 3 ++- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 7555d91..782579f 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -124,6 +124,7 @@ func getAvailableViewports() []string { "1920x1080@25", "1920x1200@25", "1440x900@25", + "1280x800@60", "1024x768@60", "1200x800@60", } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 447b6bd..e5c3a3e 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -1147,11 +1147,12 @@ func TestParseViewport_InvalidFormats(t *testing.T) { func TestGetAvailableViewports_ReturnsExpectedOptions(t *testing.T) { viewports := getAvailableViewports() - assert.Len(t, viewports, 6) + assert.Len(t, viewports, 7) assert.Contains(t, viewports, "2560x1440@10") assert.Contains(t, viewports, "1920x1080@25") assert.Contains(t, viewports, "1920x1200@25") assert.Contains(t, viewports, "1440x900@25") + assert.Contains(t, viewports, "1280x800@60") assert.Contains(t, viewports, "1200x800@60") assert.Contains(t, viewports, "1024x768@60") } diff --git a/go.mod b/go.mod index 09d225a..8e0b3ae 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.28.0 + github.com/kernel/kernel-go-sdk v0.29.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index f3de7bd..dea7a9e 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.28.0 h1:cvaCWP25UIB5w6oOdQ5J+rVboNGq3VaWYhtmshlPrhg= -github.com/kernel/kernel-go-sdk v0.28.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.29.0 h1:YExAB/fvwTV05pwYCf+BhvSWXRYgETAJH4pH7T8IdzE= +github.com/kernel/kernel-go-sdk v0.29.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 440c557d3b62050cd946d7a65cef7d7d45b9e67e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Feb 2026 17:08:52 +0000 Subject: [PATCH 2/9] CLI: Update SDK to v0.30.0 and add new flags - Update kernel-go-sdk from v0.29.0 to v0.30.0 - Add --status flag for browser list (active, deleted, all) - Add --async-timeout flag for invoke command SDK bump triggered by: kernel/kernel-go-sdk@6ca29d21e5610db982caf74297cf481996793170 --- cmd/browsers.go | 25 +++++++++++++++++++++---- cmd/invoke.go | 5 +++++ go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 3e5f350..c140cf4 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -223,6 +223,7 @@ type BrowsersCmd struct { type BrowsersListInput struct { Output string IncludeDeleted bool + Status string Limit int Offset int } @@ -233,7 +234,19 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { } params := kernel.BrowserListParams{} - if in.IncludeDeleted { + // Use new Status parameter if provided, otherwise fall back to deprecated IncludeDeleted + if in.Status != "" { + switch in.Status { + case "active": + params.Status = kernel.BrowserListParamsStatusActive + case "deleted": + params.Status = kernel.BrowserListParamsStatusDeleted + case "all": + params.Status = kernel.BrowserListParamsStatusAll + default: + return fmt.Errorf("invalid --status value: %s (must be 'active', 'deleted', or 'all')", in.Status) + } + } else if in.IncludeDeleted { params.IncludeDeleted = kernel.Opt(true) } if in.Limit > 0 { @@ -264,7 +277,8 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { // Prepare table data headers := []string{"Browser ID", "Created At", "Persistent ID", "Profile", "CDP WS URL", "Live View URL"} - if in.IncludeDeleted { + showDeletedAt := in.IncludeDeleted || in.Status == "deleted" || in.Status == "all" + if showDeletedAt { headers = append(headers, "Deleted At") } tableData := pterm.TableData{headers} @@ -291,7 +305,7 @@ func (b BrowsersCmd) List(ctx context.Context, in BrowsersListInput) error { truncateURL(browser.BrowserLiveViewURL, 50), } - if in.IncludeDeleted { + if showDeletedAt { deletedAt := "-" if !browser.DeletedAt.IsZero() { deletedAt = util.FormatLocal(browser.DeletedAt) @@ -2054,7 +2068,8 @@ Note: Profiles can only be loaded into sessions that don't already have a profil func init() { // list flags browsersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - browsersListCmd.Flags().Bool("include-deleted", false, "Include soft-deleted browser sessions in the results") + browsersListCmd.Flags().Bool("include-deleted", false, "DEPRECATED: Use --status instead. Include soft-deleted browser sessions in the results") + browsersListCmd.Flags().String("status", "", "Filter by status: 'active' (default), 'deleted', or 'all'") browsersListCmd.Flags().Int("limit", 0, "Maximum number of results to return (default 20, max 100)") browsersListCmd.Flags().Int("offset", 0, "Number of results to skip (for pagination)") @@ -2323,11 +2338,13 @@ func runBrowsersList(cmd *cobra.Command, args []string) error { b := BrowsersCmd{browsers: &svc} out, _ := cmd.Flags().GetString("output") includeDeleted, _ := cmd.Flags().GetBool("include-deleted") + status, _ := cmd.Flags().GetString("status") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") return b.List(cmd.Context(), BrowsersListInput{ Output: out, IncludeDeleted: includeDeleted, + Status: status, Limit: limit, Offset: offset, }) diff --git a/cmd/invoke.go b/cmd/invoke.go index 4b80b67..d8e22e2 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -40,6 +40,7 @@ func init() { invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)") invokeCmd.Flags().StringP("payload-file", "f", "", "Path to a JSON file containing the payload (use '-' for stdin)") invokeCmd.Flags().BoolP("sync", "s", false, "Invoke synchronously (default false). A synchronous invocation will open a long-lived HTTP POST to the Kernel API to wait for the invocation to complete. This will time out after 60 seconds, so only use this option if you expect your invocation to complete in less than 60 seconds. The default is to invoke asynchronously, in which case the CLI will open an SSE connection to the Kernel API after submitting the invocation and wait for the invocation to complete.") + invokeCmd.Flags().Int64("async-timeout", 0, "Timeout in seconds for async invocations (min 10, max 3600). Only applies when async mode is used.") invokeCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL streaming output") invokeCmd.MarkFlagsMutuallyExclusive("payload", "payload-file") @@ -70,12 +71,16 @@ func runInvoke(cmd *cobra.Command, args []string) error { return fmt.Errorf("version cannot be an empty string") } isSync, _ := cmd.Flags().GetBool("sync") + asyncTimeout, _ := cmd.Flags().GetInt64("async-timeout") params := kernel.InvocationNewParams{ AppName: appName, ActionName: actionName, Version: version, Async: kernel.Opt(!isSync), } + if asyncTimeout > 0 { + params.AsyncTimeoutSeconds = kernel.Opt(asyncTimeout) + } payloadStr, hasPayload, err := getPayload(cmd) if err != nil { diff --git a/go.mod b/go.mod index 140eab8..15d5dbd 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.29.0 + github.com/kernel/kernel-go-sdk v0.30.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 65a17b9..1082c5b 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.29.0 h1:YExAB/fvwTV05pwYCf+BhvSWXRYgETAJH4pH7T8IdzE= -github.com/kernel/kernel-go-sdk v0.29.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.30.0 h1:FN9G84mbqqTETSBRHRTuG4rBoUVu3xRhDIaWG3AyYNI= +github.com/kernel/kernel-go-sdk v0.30.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 91545de614a27efb9ce27228bd82d7bc8ce2d8af Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Feb 2026 18:34:46 +0000 Subject: [PATCH 3/9] fix: remove duplicate 1280x800@60 viewport entry The viewport '1280x800@60' was added twice: - Once in commit 899caee (SDK v0.29.0 update) - Again in commit 4d9565b (feat: add 1280x800 viewport support #97) This resulted in 8 items in getAvailableViewports() while the test expected 7 items, causing test failures. Also removes the duplicate assertion in the test file. Co-authored-by: mason --- cmd/browsers.go | 1 - cmd/browsers_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index c140cf4..e96eb12 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -127,7 +127,6 @@ func getAvailableViewports() []string { "1280x800@60", "1024x768@60", "1200x800@60", - "1280x800@60", } } diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index f5f13e1..696e29a 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -1153,7 +1153,6 @@ func TestGetAvailableViewports_ReturnsExpectedOptions(t *testing.T) { assert.Contains(t, viewports, "1440x900@25") assert.Contains(t, viewports, "1280x800@60") assert.Contains(t, viewports, "1200x800@60") - assert.Contains(t, viewports, "1280x800@60") assert.Contains(t, viewports, "1024x768@60") } From 26580149a654dd557ee0ca5a917e2862324ba175 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Feb 2026 18:38:39 +0000 Subject: [PATCH 4/9] feat: add 'invoke browsers' command to list browsers for an invocation Adds CLI support for the SDK's InvocationService.ListBrowsers() method which returns all active browser sessions created within a specific invocation. Usage: kernel invoke browsers [--output json] Co-authored-by: mason --- cmd/invoke.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/cmd/invoke.go b/cmd/invoke.go index d8e22e2..3b5091c 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -35,6 +35,14 @@ var invocationHistoryCmd = &cobra.Command{ RunE: runInvocationHistory, } +var invocationBrowsersCmd = &cobra.Command{ + Use: "browsers ", + Short: "List browser sessions for an invocation", + Long: "List all active browser sessions created within a specific invocation.", + Args: cobra.ExactArgs(1), + RunE: runInvocationBrowsers, +} + func init() { invokeCmd.Flags().StringP("version", "v", "latest", "Specify a version of the app to invoke (optional, defaults to 'latest')") invokeCmd.Flags().StringP("payload", "p", "", "JSON payload for the invocation (optional)") @@ -49,6 +57,9 @@ func init() { invocationHistoryCmd.Flags().String("version", "", "Filter by invocation version") invocationHistoryCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") invokeCmd.AddCommand(invocationHistoryCmd) + + invocationBrowsersCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + invokeCmd.AddCommand(invocationBrowsersCmd) } func runInvoke(cmd *cobra.Command, args []string) error { @@ -433,3 +444,56 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { } return nil } + +func runInvocationBrowsers(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + invocationID := args[0] + output, _ := cmd.Flags().GetString("output") + + if output != "" && output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + resp, err := client.Invocations.ListBrowsers(cmd.Context(), invocationID) + if err != nil { + pterm.Error.Printf("Failed to list browsers for invocation: %v\n", err) + return nil + } + + if output == "json" { + if len(resp.Browsers) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(resp.Browsers) + } + + if len(resp.Browsers) == 0 { + pterm.Info.Printf("No active browsers found for invocation %s\n", invocationID) + return nil + } + + table := pterm.TableData{{"Session ID", "Created At", "Headless", "Stealth", "Timeout", "CDP WS URL", "Live View URL"}} + + for _, browser := range resp.Browsers { + created := util.FormatLocal(browser.CreatedAt) + liveView := browser.BrowserLiveViewURL + if liveView == "" { + liveView = "-" + } + + table = append(table, []string{ + browser.SessionID, + created, + fmt.Sprintf("%v", browser.Headless), + fmt.Sprintf("%v", browser.Stealth), + fmt.Sprintf("%d", browser.TimeoutSeconds), + truncateURL(browser.CdpWsURL, 40), + truncateURL(liveView, 40), + }) + } + + pterm.Info.Printf("Browsers for invocation %s:\n", invocationID) + pterm.DefaultTable.WithHasHeader().WithData(table).Render() + return nil +} From 89d90ca49a0c06fa0740223f9f11940ba4d32fe0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 7 Feb 2026 18:39:26 +0000 Subject: [PATCH 5/9] feat: bump SDK to v0.32.0 and add credential-providers list-items command - Update github.com/kernel/kernel-go-sdk from v0.30.0 to v0.32.0 - Add `kernel credential-providers list-items ` command to list all credential items from an external provider - Fix breaking change: use Credential.Name instead of removed CredentialName field in AuthAgent display output Co-authored-by: Cursor --- cmd/agents.go | 14 +++++-- cmd/credential_providers.go | 75 +++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/cmd/agents.go b/cmd/agents.go index 718306f..73095b3 100644 --- a/cmd/agents.go +++ b/cmd/agents.go @@ -189,8 +189,11 @@ func (c AgentAuthCmd) Create(ctx context.Context, in AgentAuthCreateInput) error {"Status", string(agent.Status)}, {"Can Reauth", fmt.Sprintf("%t", agent.CanReauth)}, } - if agent.CredentialName != "" { - tableData = append(tableData, []string{"Credential Name", agent.CredentialName}) + if agent.Credential.Name != "" { + tableData = append(tableData, []string{"Credential Name", agent.Credential.Name}) + } + if agent.Credential.Provider != "" { + tableData = append(tableData, []string{"Credential Provider", agent.Credential.Provider}) } PrintTableNoPad(tableData, true) @@ -223,8 +226,11 @@ func (c AgentAuthCmd) Get(ctx context.Context, in AgentAuthGetInput) error { if agent.CredentialID != "" { tableData = append(tableData, []string{"Credential ID", agent.CredentialID}) } - if agent.CredentialName != "" { - tableData = append(tableData, []string{"Credential Name", agent.CredentialName}) + if agent.Credential.Name != "" { + tableData = append(tableData, []string{"Credential Name", agent.Credential.Name}) + } + if agent.Credential.Provider != "" { + tableData = append(tableData, []string{"Credential Provider", agent.Credential.Provider}) } if agent.PostLoginURL != "" { tableData = append(tableData, []string{"Post-Login URL", agent.PostLoginURL}) diff --git a/cmd/credential_providers.go b/cmd/credential_providers.go index 1da25f0..0ed7133 100644 --- a/cmd/credential_providers.go +++ b/cmd/credential_providers.go @@ -20,6 +20,7 @@ type CredentialProvidersService interface { List(ctx context.Context, opts ...option.RequestOption) (res *[]kernel.CredentialProvider, err error) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) Test(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProviderTestResult, err error) + ListItems(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.CredentialProviderListItemsResponse, err error) } // CredentialProvidersCmd handles credential provider operations independent of cobra. @@ -62,6 +63,11 @@ type CredentialProvidersTestInput struct { Output string } +type CredentialProvidersListItemsInput struct { + ID string + Output string +} + func (c CredentialProvidersCmd) List(ctx context.Context, in CredentialProvidersListInput) error { if in.Output != "" && in.Output != "json" { return fmt.Errorf("unsupported --output value: use 'json'") @@ -281,6 +287,51 @@ func (c CredentialProvidersCmd) Test(ctx context.Context, in CredentialProviders return nil } +func (c CredentialProvidersCmd) ListItems(ctx context.Context, in CredentialProvidersListItemsInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Output != "json" { + pterm.Info.Printf("Listing items for credential provider '%s'...\n", in.ID) + } + + result, err := c.providers.ListItems(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if len(result.Items) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(result.Items) + } + + if len(result.Items) == 0 { + pterm.Info.Println("No items found") + return nil + } + + tableData := pterm.TableData{{"Path", "Title", "Vault", "URLs"}} + for _, item := range result.Items { + urls := "" + if len(item.URLs) > 0 { + urls = strings.Join(item.URLs, ", ") + } + tableData = append(tableData, []string{ + item.Path, + item.Title, + item.VaultName, + urls, + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + // --- Cobra wiring --- var credentialProvidersCmd = &cobra.Command{ @@ -345,6 +396,14 @@ var credentialProvidersTestCmd = &cobra.Command{ RunE: runCredentialProvidersTest, } +var credentialProvidersListItemsCmd = &cobra.Command{ + Use: "list-items ", + Short: "List items from a credential provider", + Long: `List all credential items available from the specified external credential provider.`, + Args: cobra.ExactArgs(1), + RunE: runCredentialProvidersListItems, +} + func init() { credentialProvidersCmd.AddCommand(credentialProvidersListCmd) credentialProvidersCmd.AddCommand(credentialProvidersGetCmd) @@ -352,6 +411,7 @@ func init() { credentialProvidersCmd.AddCommand(credentialProvidersUpdateCmd) credentialProvidersCmd.AddCommand(credentialProvidersDeleteCmd) credentialProvidersCmd.AddCommand(credentialProvidersTestCmd) + credentialProvidersCmd.AddCommand(credentialProvidersListItemsCmd) // List flags credentialProvidersListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") @@ -379,6 +439,9 @@ func init() { // Test flags credentialProvidersTestCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // ListItems flags + credentialProvidersListItemsCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") } func runCredentialProvidersList(cmd *cobra.Command, args []string) error { @@ -464,3 +527,15 @@ func runCredentialProvidersTest(cmd *cobra.Command, args []string) error { Output: output, }) } + +func runCredentialProvidersListItems(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.CredentialProviders + c := CredentialProvidersCmd{providers: &svc} + return c.ListItems(cmd.Context(), CredentialProvidersListItemsInput{ + ID: args[0], + Output: output, + }) +} diff --git a/go.mod b/go.mod index 15d5dbd..d65b9f4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.30.0 + github.com/kernel/kernel-go-sdk v0.32.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 1082c5b..9653f0d 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.30.0 h1:FN9G84mbqqTETSBRHRTuG4rBoUVu3xRhDIaWG3AyYNI= -github.com/kernel/kernel-go-sdk v0.30.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.32.0 h1:xdypUWiHvZlivIZ4eoBUE2jxZr2h9ZGl9IdWLW6P3fc= +github.com/kernel/kernel-go-sdk v0.32.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From d1c35a49467856ce121ba4f39abfdf01a05f537c Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:47:20 +0000 Subject: [PATCH 6/9] feat: add auth connections commands for managed auth API Add CLI commands for the new Auth.Connections SDK service: - kernel auth connections create: Create managed auth for profile/domain - kernel auth connections get: Get managed auth by ID - kernel auth connections list: List managed auths - kernel auth connections delete: Delete managed auth - kernel auth connections login: Start login flow - kernel auth connections submit: Submit field values to login flow - kernel auth connections follow: Follow login flow events via SSE Also bump SDK to latest version (c90e1da19efb). Co-authored-by: Cursor --- cmd/auth_connections.go | 711 ++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 3 files changed, 714 insertions(+), 3 deletions(-) create mode 100644 cmd/auth_connections.go diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go new file mode 100644 index 0000000..affa011 --- /dev/null +++ b/cmd/auth_connections.go @@ -0,0 +1,711 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/kernel/cli/pkg/util" + "github.com/kernel/kernel-go-sdk" + "github.com/kernel/kernel-go-sdk/option" + "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/kernel/kernel-go-sdk/packages/ssestream" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +// AuthConnectionService defines the subset of the Kernel SDK auth connection client that we use. +type AuthConnectionService interface { + New(ctx context.Context, body kernel.AuthConnectionNewParams, opts ...option.RequestOption) (res *kernel.ManagedAuth, err error) + Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.ManagedAuth, err error) + List(ctx context.Context, query kernel.AuthConnectionListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.ManagedAuth], err error) + Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) + Login(ctx context.Context, id string, body kernel.AuthConnectionLoginParams, opts ...option.RequestOption) (res *kernel.LoginResponse, err error) + Submit(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (res *kernel.SubmitFieldsResponse, err error) + FollowStreaming(ctx context.Context, id string, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.AuthConnectionFollowResponseUnion]) +} + +// AuthConnectionCmd handles auth connection operations independent of cobra. +type AuthConnectionCmd struct { + svc AuthConnectionService +} + +type AuthConnectionCreateInput struct { + Domain string + ProfileName string + LoginURL string + AllowedDomains []string + CredentialName string + CredentialProvider string + CredentialPath string + CredentialAuto bool + ProxyID string + HealthCheckInterval int + Output string +} + +type AuthConnectionGetInput struct { + ID string + Output string +} + +type AuthConnectionListInput struct { + Domain string + ProfileName string + Limit int + Offset int + Output string +} + +type AuthConnectionDeleteInput struct { + ID string + SkipConfirm bool +} + +type AuthConnectionLoginInput struct { + ID string + SaveCredentialAs string + Output string +} + +type AuthConnectionSubmitInput struct { + ID string + FieldValues map[string]string + MfaOptionID string + SSOButtonSelector string + Output string +} + +type AuthConnectionFollowInput struct { + ID string + Output string +} + +func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + if in.Domain == "" { + return fmt.Errorf("--domain is required") + } + if in.ProfileName == "" { + return fmt.Errorf("--profile-name is required") + } + + params := kernel.AuthConnectionNewParams{ + ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{ + Domain: in.Domain, + ProfileName: in.ProfileName, + }, + } + if in.LoginURL != "" { + params.ManagedAuthCreateRequest.LoginURL = kernel.Opt(in.LoginURL) + } + if len(in.AllowedDomains) > 0 { + params.ManagedAuthCreateRequest.AllowedDomains = in.AllowedDomains + } + if in.HealthCheckInterval > 0 { + params.ManagedAuthCreateRequest.HealthCheckInterval = kernel.Opt(int64(in.HealthCheckInterval)) + } + + // Handle credential reference + if in.CredentialName != "" { + params.ManagedAuthCreateRequest.Credential = kernel.ManagedAuthCreateRequestCredentialParam{ + Name: kernel.Opt(in.CredentialName), + } + } else if in.CredentialProvider != "" { + params.ManagedAuthCreateRequest.Credential = kernel.ManagedAuthCreateRequestCredentialParam{ + Provider: kernel.Opt(in.CredentialProvider), + } + if in.CredentialPath != "" { + params.ManagedAuthCreateRequest.Credential.Path = kernel.Opt(in.CredentialPath) + } + if in.CredentialAuto { + params.ManagedAuthCreateRequest.Credential.Auto = kernel.Opt(true) + } + } + + if in.ProxyID != "" { + params.ManagedAuthCreateRequest.Proxy = kernel.ManagedAuthCreateRequestProxyParam{ + ProxyID: kernel.Opt(in.ProxyID), + } + } + + if in.Output != "json" { + pterm.Info.Printf("Creating managed auth for %s...\n", in.Domain) + } + + auth, err := c.svc.New(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(auth) + } + + pterm.Success.Printf("Created managed auth: %s\n", auth.ID) + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", auth.ID}, + {"Domain", auth.Domain}, + {"Profile Name", auth.ProfileName}, + {"Status", string(auth.Status)}, + {"Can Reauth", fmt.Sprintf("%t", auth.CanReauth)}, + } + if auth.CanReauthReason != "" { + tableData = append(tableData, []string{"Can Reauth Reason", auth.CanReauthReason}) + } + if auth.Credential.Name != "" { + tableData = append(tableData, []string{"Credential Name", auth.Credential.Name}) + } + if auth.Credential.Provider != "" { + tableData = append(tableData, []string{"Credential Provider", auth.Credential.Provider}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + auth, err := c.svc.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(auth) + } + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", auth.ID}, + {"Domain", auth.Domain}, + {"Profile Name", auth.ProfileName}, + {"Status", string(auth.Status)}, + {"Can Reauth", fmt.Sprintf("%t", auth.CanReauth)}, + } + if auth.CanReauthReason != "" { + tableData = append(tableData, []string{"Can Reauth Reason", auth.CanReauthReason}) + } + if auth.Credential.Name != "" { + tableData = append(tableData, []string{"Credential Name", auth.Credential.Name}) + } + if auth.Credential.Provider != "" { + tableData = append(tableData, []string{"Credential Provider", auth.Credential.Provider}) + } + if auth.FlowStatus != "" { + tableData = append(tableData, []string{"Flow Status", string(auth.FlowStatus)}) + } + if auth.FlowStep != "" { + tableData = append(tableData, []string{"Flow Step", string(auth.FlowStep)}) + } + if auth.HostedURL != "" { + tableData = append(tableData, []string{"Hosted URL", auth.HostedURL}) + } + if auth.LiveViewURL != "" { + tableData = append(tableData, []string{"Live View URL", auth.LiveViewURL}) + } + if auth.ErrorMessage != "" { + tableData = append(tableData, []string{"Error Message", auth.ErrorMessage}) + } + if !auth.LastAuthAt.IsZero() { + tableData = append(tableData, []string{"Last Auth At", util.FormatLocal(auth.LastAuthAt)}) + } + if len(auth.AllowedDomains) > 0 { + tableData = append(tableData, []string{"Allowed Domains", strings.Join(auth.AllowedDomains, ", ")}) + } + if auth.HealthCheckInterval > 0 { + tableData = append(tableData, []string{"Health Check Interval", fmt.Sprintf("%d seconds", auth.HealthCheckInterval)}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.AuthConnectionListParams{} + if in.Domain != "" { + params.Domain = kernel.Opt(in.Domain) + } + if in.ProfileName != "" { + params.ProfileName = kernel.Opt(in.ProfileName) + } + if in.Limit > 0 { + params.Limit = kernel.Opt(int64(in.Limit)) + } + if in.Offset > 0 { + params.Offset = kernel.Opt(int64(in.Offset)) + } + + page, err := c.svc.List(ctx, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + var auths []kernel.ManagedAuth + if page != nil { + auths = page.Items + } + + if in.Output == "json" { + if len(auths) == 0 { + fmt.Println("[]") + return nil + } + return util.PrintPrettyJSONSlice(auths) + } + + if len(auths) == 0 { + pterm.Info.Println("No managed auths found") + return nil + } + + tableData := pterm.TableData{{"ID", "Domain", "Profile Name", "Status", "Can Reauth"}} + for _, auth := range auths { + tableData = append(tableData, []string{ + auth.ID, + auth.Domain, + auth.ProfileName, + string(auth.Status), + fmt.Sprintf("%t", auth.CanReauth), + }) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AuthConnectionCmd) Delete(ctx context.Context, in AuthConnectionDeleteInput) error { + if !in.SkipConfirm { + msg := fmt.Sprintf("Are you sure you want to delete managed auth '%s'?", in.ID) + pterm.DefaultInteractiveConfirm.DefaultText = msg + ok, _ := pterm.DefaultInteractiveConfirm.Show() + if !ok { + pterm.Info.Println("Deletion cancelled") + return nil + } + } + + if err := c.svc.Delete(ctx, in.ID); err != nil { + if util.IsNotFound(err) { + pterm.Info.Printf("Managed auth '%s' not found\n", in.ID) + return nil + } + return util.CleanedUpSdkError{Err: err} + } + pterm.Success.Printf("Deleted managed auth: %s\n", in.ID) + return nil +} + +func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + params := kernel.AuthConnectionLoginParams{} + if in.SaveCredentialAs != "" { + params.LoginRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) + } + + if in.Output != "json" { + pterm.Info.Println("Starting login flow...") + } + + resp, err := c.svc.Login(ctx, in.ID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + pterm.Success.Printf("Login flow started: %s\n", resp.FlowType) + + tableData := pterm.TableData{ + {"Property", "Value"}, + {"ID", resp.ID}, + {"Flow Type", string(resp.FlowType)}, + {"Hosted URL", resp.HostedURL}, + {"Flow Expires At", util.FormatLocal(resp.FlowExpiresAt)}, + } + if resp.LiveViewURL != "" { + tableData = append(tableData, []string{"Live View URL", resp.LiveViewURL}) + } + + PrintTableNoPad(tableData, true) + return nil +} + +func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + // Validate that we have some input to submit + hasFields := len(in.FieldValues) > 0 + hasMfaOption := in.MfaOptionID != "" + hasSSOButton := in.SSOButtonSelector != "" + + if !hasFields && !hasMfaOption && !hasSSOButton { + return fmt.Errorf("must provide at least one of: --field, --mfa-option-id, or --sso-button-selector") + } + + params := kernel.AuthConnectionSubmitParams{ + SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ + Fields: in.FieldValues, + }, + } + if hasMfaOption { + params.SubmitFieldsRequest.MfaOptionID = kernel.Opt(in.MfaOptionID) + } + if hasSSOButton { + params.SubmitFieldsRequest.SSOButtonSelector = kernel.Opt(in.SSOButtonSelector) + } + + if in.Output != "json" { + pterm.Info.Println("Submitting to managed auth...") + } + + resp, err := c.svc.Submit(ctx, in.ID, params) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + return util.PrintPrettyJSON(resp) + } + + if resp.Accepted { + pterm.Success.Println("Submission accepted") + } else { + pterm.Warning.Println("Submission not accepted") + } + return nil +} + +func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowInput) error { + if in.Output != "" && in.Output != "json" { + return fmt.Errorf("unsupported --output value: use 'json'") + } + + stream := c.svc.FollowStreaming(ctx, in.ID) + defer stream.Close() + + if in.Output != "json" { + pterm.Info.Println("Following managed auth events (Ctrl+C to stop)...") + } + + for stream.Next() { + event := stream.Current() + + if in.Output == "json" { + if err := util.PrintPrettyJSON(event); err != nil { + return err + } + continue + } + + // Human-readable output + switch event.Event { + case "managed_auth_state": + state := event.AsManagedAuthState() + pterm.Info.Printf("[%s] Status: %s, Step: %s\n", + state.Timestamp.Local().Format(time.RFC3339), + state.FlowStatus, + state.FlowStep) + if len(state.DiscoveredFields) > 0 { + var fieldNames []string + for _, f := range state.DiscoveredFields { + fieldNames = append(fieldNames, f.Name) + } + pterm.Info.Printf(" Discovered fields: %s\n", strings.Join(fieldNames, ", ")) + } + if state.ErrorMessage != "" { + pterm.Error.Printf(" Error: %s\n", state.ErrorMessage) + } + if state.WebsiteError != "" { + pterm.Warning.Printf(" Website error: %s\n", state.WebsiteError) + } + case "error": + errEvent := event.AsError() + pterm.Error.Printf("Error: %s\n", errEvent.Error.Message) + case "sse_heartbeat": + // Silently ignore heartbeats for human-readable output + } + } + + if err := stream.Err(); err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output != "json" { + pterm.Success.Println("Stream ended") + } + return nil +} + +// --- Cobra wiring --- + +var authConnectionsCmd = &cobra.Command{ + Use: "connections", + Short: "Manage auth connections (managed auth)", + Long: "Commands for managing authentication connections that keep profiles logged into domains", +} + +var authConnectionsCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a managed auth connection", + Long: "Create managed authentication for a profile and domain combination", + Args: cobra.NoArgs, + RunE: runAuthConnectionsCreate, +} + +var authConnectionsGetCmd = &cobra.Command{ + Use: "get ", + Short: "Get a managed auth by ID", + Args: cobra.ExactArgs(1), + RunE: runAuthConnectionsGet, +} + +var authConnectionsListCmd = &cobra.Command{ + Use: "list", + Short: "List managed auths", + Args: cobra.NoArgs, + RunE: runAuthConnectionsList, +} + +var authConnectionsDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a managed auth", + Args: cobra.ExactArgs(1), + RunE: runAuthConnectionsDelete, +} + +var authConnectionsLoginCmd = &cobra.Command{ + Use: "login ", + Short: "Start a login flow", + Long: "Start a login flow for the managed auth, returns a hosted URL for authentication", + Args: cobra.ExactArgs(1), + RunE: runAuthConnectionsLogin, +} + +var authConnectionsSubmitCmd = &cobra.Command{ + Use: "submit ", + Short: "Submit field values to a login flow", + Long: `Submit field values for the login form. Poll the managed auth to track progress. + +Examples: + # Submit field values + kernel auth connections submit --field username=myuser --field password=mypass + + # Select an MFA option + kernel auth connections submit --mfa-option-id + + # Click an SSO button + kernel auth connections submit --sso-button-selector "//button[@id='google-sso']"`, + Args: cobra.ExactArgs(1), + RunE: runAuthConnectionsSubmit, +} + +var authConnectionsFollowCmd = &cobra.Command{ + Use: "follow ", + Short: "Follow login flow events", + Long: "Establish an SSE stream to receive real-time login flow state updates", + Args: cobra.ExactArgs(1), + RunE: runAuthConnectionsFollow, +} + +func init() { + // Create flags + authConnectionsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + authConnectionsCreateCmd.Flags().String("domain", "", "Target domain for authentication (required)") + authConnectionsCreateCmd.Flags().String("profile-name", "", "Name of the profile to manage (required)") + authConnectionsCreateCmd.Flags().String("login-url", "", "Optional login page URL to skip discovery") + authConnectionsCreateCmd.Flags().StringSlice("allowed-domain", []string{}, "Additional allowed domains (repeatable)") + authConnectionsCreateCmd.Flags().String("credential-name", "", "Kernel credential name to use") + authConnectionsCreateCmd.Flags().String("credential-provider", "", "External credential provider name") + authConnectionsCreateCmd.Flags().String("credential-path", "", "Provider-specific path (e.g., VaultName/ItemName)") + authConnectionsCreateCmd.Flags().Bool("credential-auto", false, "Lookup by domain from the specified provider") + authConnectionsCreateCmd.Flags().String("proxy-id", "", "Optional proxy ID to use") + authConnectionsCreateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks (300-86400)") + _ = authConnectionsCreateCmd.MarkFlagRequired("domain") + _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") + + // Get flags + authConnectionsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // List flags + authConnectionsListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + authConnectionsListCmd.Flags().String("domain", "", "Filter by domain") + authConnectionsListCmd.Flags().String("profile-name", "", "Filter by profile name") + authConnectionsListCmd.Flags().Int("limit", 0, "Maximum number of results to return") + authConnectionsListCmd.Flags().Int("offset", 0, "Number of results to skip") + + // Delete flags + authConnectionsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + + // Login flags + authConnectionsLoginCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + authConnectionsLoginCmd.Flags().String("save-credential-as", "", "Save credentials under this name on success") + + // Submit flags + authConnectionsSubmitCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + authConnectionsSubmitCmd.Flags().StringArray("field", []string{}, "Field name=value pair (repeatable)") + authConnectionsSubmitCmd.Flags().String("mfa-option-id", "", "MFA option ID if user selected an MFA method") + authConnectionsSubmitCmd.Flags().String("sso-button-selector", "", "XPath selector if user chose an SSO button") + + // Follow flags + authConnectionsFollowCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") + + // Wire up commands + authConnectionsCmd.AddCommand(authConnectionsCreateCmd) + authConnectionsCmd.AddCommand(authConnectionsGetCmd) + authConnectionsCmd.AddCommand(authConnectionsListCmd) + authConnectionsCmd.AddCommand(authConnectionsDeleteCmd) + authConnectionsCmd.AddCommand(authConnectionsLoginCmd) + authConnectionsCmd.AddCommand(authConnectionsSubmitCmd) + authConnectionsCmd.AddCommand(authConnectionsFollowCmd) + + authCmd.AddCommand(authConnectionsCmd) +} + +func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + profileName, _ := cmd.Flags().GetString("profile-name") + loginURL, _ := cmd.Flags().GetString("login-url") + allowedDomains, _ := cmd.Flags().GetStringSlice("allowed-domain") + credentialName, _ := cmd.Flags().GetString("credential-name") + credentialProvider, _ := cmd.Flags().GetString("credential-provider") + credentialPath, _ := cmd.Flags().GetString("credential-path") + credentialAuto, _ := cmd.Flags().GetBool("credential-auto") + proxyID, _ := cmd.Flags().GetString("proxy-id") + healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Create(cmd.Context(), AuthConnectionCreateInput{ + Domain: domain, + ProfileName: profileName, + LoginURL: loginURL, + AllowedDomains: allowedDomains, + CredentialName: credentialName, + CredentialProvider: credentialProvider, + CredentialPath: credentialPath, + CredentialAuto: credentialAuto, + ProxyID: proxyID, + HealthCheckInterval: healthCheckInterval, + Output: output, + }) +} + +func runAuthConnectionsGet(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Get(cmd.Context(), AuthConnectionGetInput{ + ID: args[0], + Output: output, + }) +} + +func runAuthConnectionsList(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + domain, _ := cmd.Flags().GetString("domain") + profileName, _ := cmd.Flags().GetString("profile-name") + limit, _ := cmd.Flags().GetInt("limit") + offset, _ := cmd.Flags().GetInt("offset") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.List(cmd.Context(), AuthConnectionListInput{ + Domain: domain, + ProfileName: profileName, + Limit: limit, + Offset: offset, + Output: output, + }) +} + +func runAuthConnectionsDelete(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + skip, _ := cmd.Flags().GetBool("yes") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Delete(cmd.Context(), AuthConnectionDeleteInput{ + ID: args[0], + SkipConfirm: skip, + }) +} + +func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Login(cmd.Context(), AuthConnectionLoginInput{ + ID: args[0], + SaveCredentialAs: saveCredentialAs, + Output: output, + }) +} + +func runAuthConnectionsSubmit(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + fieldPairs, _ := cmd.Flags().GetStringArray("field") + mfaOptionID, _ := cmd.Flags().GetString("mfa-option-id") + ssoButtonSelector, _ := cmd.Flags().GetString("sso-button-selector") + + // Parse field pairs into map + fieldValues := make(map[string]string) + for _, pair := range fieldPairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid field format: %s (expected key=value)", pair) + } + fieldValues[parts[0]] = parts[1] + } + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Submit(cmd.Context(), AuthConnectionSubmitInput{ + ID: args[0], + FieldValues: fieldValues, + MfaOptionID: mfaOptionID, + SSOButtonSelector: ssoButtonSelector, + Output: output, + }) +} + +func runAuthConnectionsFollow(cmd *cobra.Command, args []string) error { + client := getKernelClient(cmd) + output, _ := cmd.Flags().GetString("output") + + svc := client.Auth.Connections + c := AuthConnectionCmd{svc: &svc} + return c.Follow(cmd.Context(), AuthConnectionFollowInput{ + ID: args[0], + Output: output, + }) +} diff --git a/go.mod b/go.mod index d65b9f4..4a547e5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.32.0 + github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 9653f0d..799bf0e 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.32.0 h1:xdypUWiHvZlivIZ4eoBUE2jxZr2h9ZGl9IdWLW6P3fc= -github.com/kernel/kernel-go-sdk v0.32.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb h1:yu5PSECVvcZ7Erb5lvwbc8wLa4ncrZjT+EF6R9g4zaU= +github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From d2d5f507c94770e7810afe1a311430f5e8b11cad Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:11:45 +0000 Subject: [PATCH 7/9] feat: update SDK to v0.33.0 and fix breaking changes - Update kernel-go-sdk to v0.33.0 (commit 4719594652b863858cb7492ca78adbb850a10552) - Remove agents.go as the AgentAuth API has been removed from SDK - Fix auth_connections.go to use correct SDK field names: - Use ManagedAuthCreateRequestProxyParam.ID instead of ProxyID - Update AuthConnectionLoginInput to use Proxy params instead of SaveCredentialAs - Add proxy-id and proxy-name flags to auth connections login command Co-authored-by: Cursor --- cmd/agents.go | 1366 --------------------------------------- cmd/auth_connections.go | 32 +- cmd/root.go | 1 - go.mod | 2 +- go.sum | 4 +- 5 files changed, 24 insertions(+), 1381 deletions(-) delete mode 100644 cmd/agents.go diff --git a/cmd/agents.go b/cmd/agents.go deleted file mode 100644 index 73095b3..0000000 --- a/cmd/agents.go +++ /dev/null @@ -1,1366 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/kernel/cli/pkg/util" - "github.com/kernel/kernel-go-sdk" - "github.com/kernel/kernel-go-sdk/option" - "github.com/kernel/kernel-go-sdk/packages/pagination" - "github.com/pkg/browser" - "github.com/pquerna/otp/totp" - "github.com/pterm/pterm" - "github.com/spf13/cobra" -) - -// AgentAuthService defines the subset of the Kernel SDK agent auth client that we use. -type AgentAuthService interface { - New(ctx context.Context, body kernel.AgentAuthNewParams, opts ...option.RequestOption) (res *kernel.AuthAgent, err error) - Get(ctx context.Context, id string, opts ...option.RequestOption) (res *kernel.AuthAgent, err error) - List(ctx context.Context, query kernel.AgentAuthListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.AuthAgent], err error) - Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) -} - -// AgentAuthInvocationsService defines the subset of the Kernel SDK agent auth invocations client that we use. -type AgentAuthInvocationsService interface { - New(ctx context.Context, body kernel.AgentAuthInvocationNewParams, opts ...option.RequestOption) (res *kernel.AuthAgentInvocationCreateResponse, err error) - Get(ctx context.Context, invocationID string, opts ...option.RequestOption) (res *kernel.AgentAuthInvocationResponse, err error) - Exchange(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationExchangeParams, opts ...option.RequestOption) (res *kernel.AgentAuthInvocationExchangeResponse, err error) - Submit(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationSubmitParams, opts ...option.RequestOption) (res *kernel.AgentAuthSubmitResponse, err error) -} - -// AgentAuthCmd handles agent auth operations independent of cobra. -type AgentAuthCmd struct { - auth AgentAuthService - invocations AgentAuthInvocationsService -} - -type AgentAuthCreateInput struct { - Domain string - ProfileName string - CredentialName string - LoginURL string - AllowedDomains []string - ProxyID string - Output string -} - -type AgentAuthGetInput struct { - ID string - Output string -} - -type AgentAuthListInput struct { - Domain string - ProfileName string - Limit int - Offset int - Output string -} - -type AgentAuthDeleteInput struct { - ID string - SkipConfirm bool -} - -type AgentAuthInvocationCreateInput struct { - AuthAgentID string - SaveCredentialAs string - Output string -} - -type AgentAuthInvocationGetInput struct { - InvocationID string - Output string -} - -type AgentAuthInvocationExchangeInput struct { - InvocationID string - Code string - Output string -} - -type AgentAuthInvocationSubmitInput struct { - InvocationID string - FieldValues map[string]string - SSOButton string - SelectedMfaType string - Output string -} - -// AgentAuthRunInput contains all parameters for the automated auth run flow. -type AgentAuthRunInput struct { - Domain string - ProfileName string - Values map[string]string - CredentialName string - SaveCredentialAs string - TotpSecret string - ProxyID string - LoginURL string - AllowedDomains []string - Timeout time.Duration - OpenLiveView bool - Output string -} - -// AgentAuthRunResult is the result of a successful auth run. -type AgentAuthRunResult struct { - ProfileName string `json:"profile_name"` - ProfileID string `json:"profile_id"` - Domain string `json:"domain"` - AuthAgentID string `json:"auth_agent_id"` -} - -// AgentAuthRunEvent represents a status update during the auth run (for JSON output). -type AgentAuthRunEvent struct { - Type string `json:"type"` // status, error, success, waiting - Step string `json:"step,omitempty"` - Status string `json:"status,omitempty"` - Message string `json:"message,omitempty"` - LiveViewURL string `json:"live_view_url,omitempty"` -} - -// AgentAuthRunCmd handles the automated auth run flow. -type AgentAuthRunCmd struct { - auth AgentAuthService - invocations AgentAuthInvocationsService - profiles ProfilesService - credentials CredentialsService -} - -func (c AgentAuthCmd) Create(ctx context.Context, in AgentAuthCreateInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - if in.Domain == "" { - return fmt.Errorf("--domain is required") - } - if in.ProfileName == "" { - return fmt.Errorf("--profile-name is required") - } - - params := kernel.AgentAuthNewParams{ - AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ - Domain: in.Domain, - ProfileName: in.ProfileName, - }, - } - if in.CredentialName != "" { - params.AuthAgentCreateRequest.CredentialName = kernel.Opt(in.CredentialName) - } - if in.LoginURL != "" { - params.AuthAgentCreateRequest.LoginURL = kernel.Opt(in.LoginURL) - } - if len(in.AllowedDomains) > 0 { - params.AuthAgentCreateRequest.AllowedDomains = in.AllowedDomains - } - if in.ProxyID != "" { - params.AuthAgentCreateRequest.Proxy = kernel.AuthAgentCreateRequestProxyParam{ - ProxyID: kernel.Opt(in.ProxyID), - } - } - - if in.Output != "json" { - pterm.Info.Printf("Creating auth agent for %s...\n", in.Domain) - } - - agent, err := c.auth.New(ctx, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(agent) - } - - pterm.Success.Printf("Created auth agent: %s\n", agent.ID) - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", agent.ID}, - {"Domain", agent.Domain}, - {"Profile Name", agent.ProfileName}, - {"Status", string(agent.Status)}, - {"Can Reauth", fmt.Sprintf("%t", agent.CanReauth)}, - } - if agent.Credential.Name != "" { - tableData = append(tableData, []string{"Credential Name", agent.Credential.Name}) - } - if agent.Credential.Provider != "" { - tableData = append(tableData, []string{"Credential Provider", agent.Credential.Provider}) - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) Get(ctx context.Context, in AgentAuthGetInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - agent, err := c.auth.Get(ctx, in.ID) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(agent) - } - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"ID", agent.ID}, - {"Domain", agent.Domain}, - {"Profile Name", agent.ProfileName}, - {"Status", string(agent.Status)}, - {"Can Reauth", fmt.Sprintf("%t", agent.CanReauth)}, - {"Has Selectors", fmt.Sprintf("%t", agent.HasSelectors)}, - } - if agent.CredentialID != "" { - tableData = append(tableData, []string{"Credential ID", agent.CredentialID}) - } - if agent.Credential.Name != "" { - tableData = append(tableData, []string{"Credential Name", agent.Credential.Name}) - } - if agent.Credential.Provider != "" { - tableData = append(tableData, []string{"Credential Provider", agent.Credential.Provider}) - } - if agent.PostLoginURL != "" { - tableData = append(tableData, []string{"Post-Login URL", agent.PostLoginURL}) - } - if !agent.LastAuthCheckAt.IsZero() { - tableData = append(tableData, []string{"Last Auth Check", util.FormatLocal(agent.LastAuthCheckAt)}) - } - if len(agent.AllowedDomains) > 0 { - tableData = append(tableData, []string{"Allowed Domains", strings.Join(agent.AllowedDomains, ", ")}) - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) List(ctx context.Context, in AgentAuthListInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - params := kernel.AgentAuthListParams{} - if in.Domain != "" { - params.Domain = kernel.Opt(in.Domain) - } - if in.ProfileName != "" { - params.ProfileName = kernel.Opt(in.ProfileName) - } - if in.Limit > 0 { - params.Limit = kernel.Opt(int64(in.Limit)) - } - if in.Offset > 0 { - params.Offset = kernel.Opt(int64(in.Offset)) - } - - page, err := c.auth.List(ctx, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - var agents []kernel.AuthAgent - if page != nil { - agents = page.Items - } - - if in.Output == "json" { - if len(agents) == 0 { - fmt.Println("[]") - return nil - } - return util.PrintPrettyJSONSlice(agents) - } - - if len(agents) == 0 { - pterm.Info.Println("No auth agents found") - return nil - } - - tableData := pterm.TableData{{"ID", "Domain", "Profile Name", "Status", "Can Reauth"}} - for _, agent := range agents { - tableData = append(tableData, []string{ - agent.ID, - agent.Domain, - agent.ProfileName, - string(agent.Status), - fmt.Sprintf("%t", agent.CanReauth), - }) - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) Delete(ctx context.Context, in AgentAuthDeleteInput) error { - if !in.SkipConfirm { - msg := fmt.Sprintf("Are you sure you want to delete auth agent '%s'?", in.ID) - pterm.DefaultInteractiveConfirm.DefaultText = msg - ok, _ := pterm.DefaultInteractiveConfirm.Show() - if !ok { - pterm.Info.Println("Deletion cancelled") - return nil - } - } - - if err := c.auth.Delete(ctx, in.ID); err != nil { - if util.IsNotFound(err) { - pterm.Info.Printf("Auth agent '%s' not found\n", in.ID) - return nil - } - return util.CleanedUpSdkError{Err: err} - } - pterm.Success.Printf("Deleted auth agent: %s\n", in.ID) - return nil -} - -func (c AgentAuthCmd) InvocationCreate(ctx context.Context, in AgentAuthInvocationCreateInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - if in.AuthAgentID == "" { - return fmt.Errorf("--auth-agent-id is required") - } - - params := kernel.AgentAuthInvocationNewParams{ - AuthAgentInvocationCreateRequest: kernel.AuthAgentInvocationCreateRequestParam{ - AuthAgentID: in.AuthAgentID, - }, - } - if in.SaveCredentialAs != "" { - params.AuthAgentInvocationCreateRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) - } - - if in.Output != "json" { - pterm.Info.Println("Creating auth invocation...") - } - - resp, err := c.invocations.New(ctx, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(resp) - } - - pterm.Success.Printf("Created invocation: %s\n", resp.InvocationID) - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"Invocation ID", resp.InvocationID}, - {"Type", string(resp.Type)}, - {"Handoff Code", resp.HandoffCode}, - {"Hosted URL", resp.HostedURL}, - {"Expires At", util.FormatLocal(resp.ExpiresAt)}, - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) InvocationGet(ctx context.Context, in AgentAuthInvocationGetInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - resp, err := c.invocations.Get(ctx, in.InvocationID) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(resp) - } - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"App Name", resp.AppName}, - {"Domain", resp.Domain}, - {"Type", string(resp.Type)}, - {"Status", string(resp.Status)}, - {"Step", string(resp.Step)}, - {"Expires At", util.FormatLocal(resp.ExpiresAt)}, - } - if resp.LiveViewURL != "" { - tableData = append(tableData, []string{"Live View URL", resp.LiveViewURL}) - } - if resp.ErrorMessage != "" { - tableData = append(tableData, []string{"Error Message", resp.ErrorMessage}) - } - if resp.ExternalActionMessage != "" { - tableData = append(tableData, []string{"External Action", resp.ExternalActionMessage}) - } - if len(resp.PendingFields) > 0 { - var fields []string - for _, f := range resp.PendingFields { - fields = append(fields, f.Name) - } - tableData = append(tableData, []string{"Pending Fields", strings.Join(fields, ", ")}) - } - if len(resp.SubmittedFields) > 0 { - tableData = append(tableData, []string{"Submitted Fields", strings.Join(resp.SubmittedFields, ", ")}) - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) InvocationExchange(ctx context.Context, in AgentAuthInvocationExchangeInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - if in.Code == "" { - return fmt.Errorf("--code is required") - } - - params := kernel.AgentAuthInvocationExchangeParams{ - Code: in.Code, - } - - resp, err := c.invocations.Exchange(ctx, in.InvocationID, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(resp) - } - - pterm.Success.Printf("Exchanged code for JWT\n") - - tableData := pterm.TableData{ - {"Property", "Value"}, - {"Invocation ID", resp.InvocationID}, - {"JWT", resp.Jwt}, - } - - PrintTableNoPad(tableData, true) - return nil -} - -func (c AgentAuthCmd) InvocationSubmit(ctx context.Context, in AgentAuthInvocationSubmitInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - // Validate that exactly one of the submit types is provided - hasFields := len(in.FieldValues) > 0 - hasSSO := in.SSOButton != "" - hasMFA := in.SelectedMfaType != "" - - count := 0 - if hasFields { - count++ - } - if hasSSO { - count++ - } - if hasMFA { - count++ - } - - if count == 0 { - return fmt.Errorf("must provide one of: --field (field values), --sso-button, or --mfa-type") - } - if count > 1 { - return fmt.Errorf("can only provide one of: --field (field values), --sso-button, or --mfa-type") - } - - var params kernel.AgentAuthInvocationSubmitParams - if hasFields { - params.OfFieldValues = &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ - FieldValues: in.FieldValues, - } - } else if hasSSO { - params.OfSSOButton = &kernel.AgentAuthInvocationSubmitParamsBodySSOButton{ - SSOButton: in.SSOButton, - } - } else if hasMFA { - params.OfSelectedMfaType = &kernel.AgentAuthInvocationSubmitParamsBodySelectedMfaType{ - SelectedMfaType: in.SelectedMfaType, - } - } - - if in.Output != "json" { - pterm.Info.Println("Submitting to invocation...") - } - - resp, err := c.invocations.Submit(ctx, in.InvocationID, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - return util.PrintPrettyJSON(resp) - } - - if resp.Accepted { - pterm.Success.Println("Submission accepted") - } else { - pterm.Warning.Println("Submission not accepted") - } - return nil -} - -const ( - totpPeriod = 30 // TOTP codes are valid for 30-second windows - minSecondsRemaining = 5 // Minimum seconds remaining before we wait for next window -) - -// generateTOTPCode generates a TOTP code from a base32 secret. -// Waits for a fresh window if needed to ensure enough time to submit the code. -// If quiet is true, suppresses human-readable console output (for JSON mode). -func generateTOTPCode(secret string, quiet bool) (string, error) { - // Check if we have enough time in the current window - now := time.Now().Unix() - secondsIntoWindow := now % totpPeriod - remaining := totpPeriod - secondsIntoWindow - - if remaining < minSecondsRemaining { - waitTime := remaining + 1 // Wait until just after the new window starts - if !quiet { - pterm.Info.Printf("TOTP window has only %ds remaining, waiting %ds for fresh window...\n", remaining, waitTime) - } - time.Sleep(time.Duration(waitTime) * time.Second) - } - - // Clean the secret (remove spaces that may be added for readability) - cleanSecret := strings.ReplaceAll(strings.ToUpper(secret), " ", "") - - code, err := totp.GenerateCode(cleanSecret, time.Now()) - if err != nil { - return "", fmt.Errorf("failed to generate TOTP code: %w", err) - } - return code, nil -} - -// Run executes the full automated auth flow: create profile, credential, auth agent, and run invocation to completion. -func (c AgentAuthRunCmd) Run(ctx context.Context, in AgentAuthRunInput) error { - if in.Output != "" && in.Output != "json" { - return fmt.Errorf("unsupported --output value: use 'json'") - } - - if in.Domain == "" { - return fmt.Errorf("--domain is required") - } - if in.ProfileName == "" { - return fmt.Errorf("--profile is required") - } - - // Validate that we have credentials to work with - if in.CredentialName == "" && len(in.Values) == 0 { - return fmt.Errorf("must provide either --credential or --value flags with credentials") - } - - jsonOutput := in.Output == "json" - emitEvent := func(event AgentAuthRunEvent) { - if jsonOutput { - data, _ := json.Marshal(event) - fmt.Println(string(data)) - } - } - - // Step 1: Find or create the profile - if !jsonOutput { - pterm.Info.Printf("Looking for profile '%s'...\n", in.ProfileName) - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Looking for profile"}) - - var profileID string - profile, err := c.profiles.Get(ctx, in.ProfileName) - if err != nil { - if !util.IsNotFound(err) { - return util.CleanedUpSdkError{Err: err} - } - // Profile not found, create it - if !jsonOutput { - pterm.Info.Printf("Creating profile '%s'...\n", in.ProfileName) - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating profile"}) - - newProfile, err := c.profiles.New(ctx, kernel.ProfileNewParams{ - Name: kernel.Opt(in.ProfileName), - }) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - profileID = newProfile.ID - if !jsonOutput { - pterm.Success.Printf("Created profile: %s\n", newProfile.ID) - } - } else { - profileID = profile.ID - if !jsonOutput { - pterm.Success.Printf("Found existing profile: %s\n", profile.ID) - } - } - - // Step 2: Handle credentials - var credentialName string - if in.CredentialName != "" { - // Using existing credential - credentialName = in.CredentialName - if !jsonOutput { - pterm.Info.Printf("Using existing credential '%s'\n", credentialName) - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Using existing credential"}) - } else if in.SaveCredentialAs != "" { - // Create new credential with provided values - credentialName = in.SaveCredentialAs - if !jsonOutput { - pterm.Info.Printf("Creating credential '%s'...\n", credentialName) - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating credential"}) - - params := kernel.CredentialNewParams{ - CreateCredentialRequest: kernel.CreateCredentialRequestParam{ - Name: credentialName, - Domain: in.Domain, - Values: in.Values, - }, - } - if in.TotpSecret != "" { - params.CreateCredentialRequest.TotpSecret = kernel.Opt(in.TotpSecret) - } - - _, err := c.credentials.New(ctx, params) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - if !jsonOutput { - pterm.Success.Printf("Created credential: %s\n", credentialName) - } - } - - // Step 3: Create auth agent - if !jsonOutput { - pterm.Info.Printf("Creating auth agent for %s...\n", in.Domain) - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Creating auth agent"}) - - agentParams := kernel.AgentAuthNewParams{ - AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ - Domain: in.Domain, - ProfileName: in.ProfileName, - }, - } - if credentialName != "" { - agentParams.AuthAgentCreateRequest.CredentialName = kernel.Opt(credentialName) - } - if in.LoginURL != "" { - agentParams.AuthAgentCreateRequest.LoginURL = kernel.Opt(in.LoginURL) - } - if len(in.AllowedDomains) > 0 { - agentParams.AuthAgentCreateRequest.AllowedDomains = in.AllowedDomains - } - if in.ProxyID != "" { - agentParams.AuthAgentCreateRequest.Proxy = kernel.AuthAgentCreateRequestProxyParam{ - ProxyID: kernel.Opt(in.ProxyID), - } - } - - agent, err := c.auth.New(ctx, agentParams) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - if !jsonOutput { - pterm.Success.Printf("Created auth agent: %s\n", agent.ID) - } - - // Step 4: Create invocation - if !jsonOutput { - pterm.Info.Println("Starting authentication flow...") - } - emitEvent(AgentAuthRunEvent{Type: "status", Message: "Starting authentication"}) - - invocationParams := kernel.AgentAuthInvocationNewParams{ - AuthAgentInvocationCreateRequest: kernel.AuthAgentInvocationCreateRequestParam{ - AuthAgentID: agent.ID, - }, - } - if in.SaveCredentialAs != "" && credentialName == "" { - // Save credential during invocation if we have values but didn't create upfront - invocationParams.AuthAgentInvocationCreateRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) - } - - invocation, err := c.invocations.New(ctx, invocationParams) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - // Step 5: Polling loop - deadline := time.Now().Add(in.Timeout) - pollInterval := 2 * time.Second - var lastStep string - liveViewShown := false - fieldsSubmitted := make(map[string]bool) - - if !jsonOutput { - pterm.Info.Println("Waiting for authentication to complete...") - } - - for { - if time.Now().After(deadline) { - emitEvent(AgentAuthRunEvent{Type: "error", Message: "Timeout waiting for authentication"}) - return fmt.Errorf("timeout waiting for authentication to complete") - } - - resp, err := c.invocations.Get(ctx, invocation.InvocationID) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - // Emit status update if step changed - if string(resp.Step) != lastStep { - lastStep = string(resp.Step) - emitEvent(AgentAuthRunEvent{ - Type: "status", - Step: lastStep, - Status: string(resp.Status), - LiveViewURL: resp.LiveViewURL, - }) - if !jsonOutput { - pterm.Info.Printf("Step: %s (Status: %s)\n", resp.Step, resp.Status) - } - } - - // Check terminal states - switch resp.Status { - case kernel.AgentAuthInvocationResponseStatusSuccess: - if !jsonOutput { - pterm.Success.Println("Authentication successful!") - pterm.Success.Printf("Profile '%s' is now authenticated for %s\n", in.ProfileName, in.Domain) - } - result := AgentAuthRunResult{ - ProfileName: in.ProfileName, - ProfileID: profileID, - Domain: in.Domain, - AuthAgentID: agent.ID, - } - if jsonOutput { - emitEvent(AgentAuthRunEvent{Type: "success", Message: "Authentication successful"}) - data, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil - } - return nil - - case kernel.AgentAuthInvocationResponseStatusFailed: - errMsg := "Authentication failed" - if resp.ErrorMessage != "" { - errMsg = resp.ErrorMessage - } - emitEvent(AgentAuthRunEvent{Type: "error", Message: errMsg}) - return fmt.Errorf("authentication failed: %s", errMsg) - - case kernel.AgentAuthInvocationResponseStatusExpired: - emitEvent(AgentAuthRunEvent{Type: "error", Message: "Authentication session expired"}) - return fmt.Errorf("authentication session expired") - - case kernel.AgentAuthInvocationResponseStatusCanceled: - emitEvent(AgentAuthRunEvent{Type: "error", Message: "Authentication was canceled"}) - return fmt.Errorf("authentication was canceled") - } - - // Handle awaiting_input step - if resp.Step == kernel.AgentAuthInvocationResponseStepAwaitingInput { - // Check for pending fields - if len(resp.PendingFields) > 0 { - // Build field values to submit - submitValues := make(map[string]string) - missingFields := []string{} - - for _, field := range resp.PendingFields { - fieldName := field.Name - // Check if we already submitted this field - if fieldsSubmitted[fieldName] { - continue - } - - // Try to find a matching value - if val, ok := in.Values[fieldName]; ok { - submitValues[fieldName] = val - } else { - // Check common field name aliases - matched := false - aliases := map[string][]string{ - "identifier": {"username", "email", "login"}, - "username": {"identifier", "email", "login"}, - "email": {"identifier", "username", "login"}, - "password": {"pass", "passwd"}, - } - if alts, ok := aliases[fieldName]; ok { - for _, alt := range alts { - if val, ok := in.Values[alt]; ok { - submitValues[fieldName] = val - matched = true - break - } - } - } - - // Check if this looks like a TOTP/verification code field - if !matched && in.TotpSecret != "" { - fieldLower := strings.ToLower(fieldName) - totpPatterns := []string{"totp", "code", "verification", "otp", "2fa", "mfa", "authenticator", "token"} - for _, pattern := range totpPatterns { - if strings.Contains(fieldLower, pattern) { - code, err := generateTOTPCode(in.TotpSecret, jsonOutput) - if err == nil { - submitValues[fieldName] = code - matched = true - if !jsonOutput { - pterm.Info.Printf("Generated TOTP code for field: %s\n", fieldName) - } - } - break - } - } - } - - if !matched { - missingFields = append(missingFields, fieldName) - } - } - } - - // Submit if we have values - if len(submitValues) > 0 { - if !jsonOutput { - var fieldNames []string - for k := range submitValues { - fieldNames = append(fieldNames, k) - } - pterm.Info.Printf("Submitting fields: %s\n", strings.Join(fieldNames, ", ")) - } - - submitParams := kernel.AgentAuthInvocationSubmitParams{ - OfFieldValues: &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ - FieldValues: submitValues, - }, - } - _, err := c.invocations.Submit(ctx, invocation.InvocationID, submitParams) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - // Mark fields as submitted - for k := range submitValues { - fieldsSubmitted[k] = true - } - } - - // Show live view if we have missing fields - if len(missingFields) > 0 && !liveViewShown && resp.LiveViewURL != "" { - liveViewShown = true - emitEvent(AgentAuthRunEvent{ - Type: "waiting", - Message: fmt.Sprintf("Need human input for: %s", strings.Join(missingFields, ", ")), - LiveViewURL: resp.LiveViewURL, - }) - if !jsonOutput { - pterm.Warning.Printf("Missing values for fields: %s\n", strings.Join(missingFields, ", ")) - pterm.Info.Printf("Live view: %s\n", resp.LiveViewURL) - } - if in.OpenLiveView { - _ = browser.OpenURL(resp.LiveViewURL) - } - } - } - - // Check for MFA options - if len(resp.MfaOptions) > 0 { - // Check if TOTP is available and we have a secret - hasTOTP := false - for _, opt := range resp.MfaOptions { - if opt.Type == "totp" { - hasTOTP = true - break - } - } - - if hasTOTP && in.TotpSecret != "" { - // Generate and submit TOTP code - code, err := generateTOTPCode(in.TotpSecret, jsonOutput) - if err != nil { - return err - } - - if !jsonOutput { - pterm.Info.Println("Submitting TOTP code...") - } - - submitParams := kernel.AgentAuthInvocationSubmitParams{ - OfFieldValues: &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ - FieldValues: map[string]string{"totp": code}, - }, - } - _, err = c.invocations.Submit(ctx, invocation.InvocationID, submitParams) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - } else if !liveViewShown && resp.LiveViewURL != "" { - // Need human for MFA - liveViewShown = true - var optTypes []string - for _, opt := range resp.MfaOptions { - optTypes = append(optTypes, opt.Type) - } - emitEvent(AgentAuthRunEvent{ - Type: "waiting", - Message: fmt.Sprintf("MFA required: %s", strings.Join(optTypes, ", ")), - LiveViewURL: resp.LiveViewURL, - }) - if !jsonOutput { - pterm.Warning.Printf("MFA required. Options: %s\n", strings.Join(optTypes, ", ")) - pterm.Info.Printf("Complete MFA at: %s\n", resp.LiveViewURL) - } - if in.OpenLiveView { - _ = browser.OpenURL(resp.LiveViewURL) - } - } - } - } - - // Handle awaiting_external_action step - if resp.Step == kernel.AgentAuthInvocationResponseStepAwaitingExternalAction && !liveViewShown { - liveViewShown = true - msg := "External action required" - if resp.ExternalActionMessage != "" { - msg = resp.ExternalActionMessage - } - emitEvent(AgentAuthRunEvent{ - Type: "waiting", - Message: msg, - LiveViewURL: resp.LiveViewURL, - }) - if !jsonOutput { - pterm.Warning.Printf("%s\n", msg) - if resp.LiveViewURL != "" { - pterm.Info.Printf("Live view: %s\n", resp.LiveViewURL) - } - } - if in.OpenLiveView && resp.LiveViewURL != "" { - _ = browser.OpenURL(resp.LiveViewURL) - } - } - - // Wait before next poll - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(pollInterval): - } - } -} - -// --- Cobra wiring --- - -var agentsCmd = &cobra.Command{ - Use: "agents", - Short: "Manage agents", - Long: "Commands for managing Kernel agents (auth, etc.)", -} - -var agentsAuthCmd = &cobra.Command{ - Use: "auth", - Short: "Manage auth agents", - Long: "Commands for managing authentication agents that handle login flows", -} - -var agentsAuthCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create an auth agent", - Long: "Create or find an auth agent for a specific domain and profile combination", - Args: cobra.NoArgs, - RunE: runAgentsAuthCreate, -} - -var agentsAuthGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get an auth agent by ID", - Args: cobra.ExactArgs(1), - RunE: runAgentsAuthGet, -} - -var agentsAuthListCmd = &cobra.Command{ - Use: "list", - Short: "List auth agents", - Args: cobra.NoArgs, - RunE: runAgentsAuthList, -} - -var agentsAuthDeleteCmd = &cobra.Command{ - Use: "delete ", - Short: "Delete an auth agent", - Args: cobra.ExactArgs(1), - RunE: runAgentsAuthDelete, -} - -var agentsAuthInvocationsCmd = &cobra.Command{ - Use: "invocations", - Short: "Manage auth invocations", - Long: "Commands for managing authentication invocations (login flows)", -} - -var agentsAuthInvocationsCreateCmd = &cobra.Command{ - Use: "create", - Short: "Create an auth invocation", - Long: "Start a new authentication flow for an auth agent", - Args: cobra.NoArgs, - RunE: runAgentsAuthInvocationsCreate, -} - -var agentsAuthInvocationsGetCmd = &cobra.Command{ - Use: "get ", - Short: "Get an auth invocation", - Args: cobra.ExactArgs(1), - RunE: runAgentsAuthInvocationsGet, -} - -var agentsAuthInvocationsExchangeCmd = &cobra.Command{ - Use: "exchange ", - Short: "Exchange a handoff code for a JWT", - Args: cobra.ExactArgs(1), - RunE: runAgentsAuthInvocationsExchange, -} - -var agentsAuthInvocationsSubmitCmd = &cobra.Command{ - Use: "submit ", - Short: "Submit field values to an invocation", - Long: `Submit field values, SSO button click, or MFA selection to an auth invocation. - -Examples: - # Submit field values - kernel agents auth invocations submit --field username=myuser --field password=mypass - - # Click an SSO button - kernel agents auth invocations submit --sso-button "//button[@id='google-sso']" - - # Select an MFA method - kernel agents auth invocations submit --mfa-type sms`, - Args: cobra.ExactArgs(1), - RunE: runAgentsAuthInvocationsSubmit, -} - -var agentsAuthRunCmd = &cobra.Command{ - Use: "run", - Short: "Run a complete auth flow", - Long: `Run a complete authentication flow for a domain, automatically handling credential submission and polling. - -This command orchestrates the entire agent auth process: -1. Creates or finds a profile with the given name -2. Creates a credential if --save-credential-as is specified -3. Creates an auth agent linking domain, profile, and credential -4. Starts an invocation and polls until completion -5. Auto-submits credentials when prompted -6. Auto-submits TOTP codes if --totp-secret is provided -7. Shows live view URL when human intervention is needed - -Examples: - # Basic auth with inline credentials - kernel agents auth run --domain github.com --profile my-github \ - --value username=myuser --value password=mypass - - # With TOTP for automatic 2FA - kernel agents auth run --domain github.com --profile my-github \ - --value username=myuser --value password=mypass \ - --totp-secret JBSWY3DPEHPK3PXP - - # Save credentials for future re-auth - kernel agents auth run --domain github.com --profile my-github \ - --value username=myuser --value password=mypass \ - --save-credential-as github-creds - - # Re-use existing saved credential - kernel agents auth run --domain github.com --profile my-github \ - --credential github-creds - - # Auto-open browser for human intervention - kernel agents auth run --domain github.com --profile my-github \ - --credential github-creds --open`, - Args: cobra.NoArgs, - RunE: runAgentsAuthRun, -} - -func init() { - // Auth create flags - agentsAuthCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - agentsAuthCreateCmd.Flags().String("domain", "", "Target domain for authentication (required)") - agentsAuthCreateCmd.Flags().String("profile-name", "", "Name of the profile to use (required)") - agentsAuthCreateCmd.Flags().String("credential-name", "", "Optional credential name to link for auto-fill") - agentsAuthCreateCmd.Flags().String("login-url", "", "Optional login page URL") - agentsAuthCreateCmd.Flags().StringSlice("allowed-domain", []string{}, "Additional allowed domains (repeatable)") - agentsAuthCreateCmd.Flags().String("proxy-id", "", "Optional proxy ID to use") - _ = agentsAuthCreateCmd.MarkFlagRequired("domain") - _ = agentsAuthCreateCmd.MarkFlagRequired("profile-name") - - // Auth get flags - agentsAuthGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - - // Auth list flags - agentsAuthListCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - agentsAuthListCmd.Flags().String("domain", "", "Filter by domain") - agentsAuthListCmd.Flags().String("profile-name", "", "Filter by profile name") - agentsAuthListCmd.Flags().Int("limit", 0, "Maximum number of results to return") - agentsAuthListCmd.Flags().Int("offset", 0, "Number of results to skip") - - // Auth delete flags - agentsAuthDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") - - // Invocations create flags - agentsAuthInvocationsCreateCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - agentsAuthInvocationsCreateCmd.Flags().String("auth-agent-id", "", "ID of the auth agent (required)") - agentsAuthInvocationsCreateCmd.Flags().String("save-credential-as", "", "Save credentials under this name on success") - _ = agentsAuthInvocationsCreateCmd.MarkFlagRequired("auth-agent-id") - - // Invocations get flags - agentsAuthInvocationsGetCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - - // Invocations exchange flags - agentsAuthInvocationsExchangeCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - agentsAuthInvocationsExchangeCmd.Flags().String("code", "", "Handoff code from the start endpoint (required)") - _ = agentsAuthInvocationsExchangeCmd.MarkFlagRequired("code") - - // Invocations submit flags - agentsAuthInvocationsSubmitCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - agentsAuthInvocationsSubmitCmd.Flags().StringArray("field", []string{}, "Field name=value pair (repeatable)") - agentsAuthInvocationsSubmitCmd.Flags().String("sso-button", "", "Selector of SSO button to click") - agentsAuthInvocationsSubmitCmd.Flags().String("mfa-type", "", "MFA type to select (sms, call, email, totp, push, security_key)") - - // Auth run flags - agentsAuthRunCmd.Flags().StringP("output", "o", "", "Output format: json for JSONL events") - agentsAuthRunCmd.Flags().String("domain", "", "Target domain for authentication (required)") - agentsAuthRunCmd.Flags().String("profile", "", "Profile name to use/create (required)") - agentsAuthRunCmd.Flags().StringArray("value", []string{}, "Field name=value pair (e.g., --value username=foo --value password=bar)") - agentsAuthRunCmd.Flags().String("credential", "", "Existing credential name to use") - agentsAuthRunCmd.Flags().String("save-credential-as", "", "Save provided credentials under this name") - agentsAuthRunCmd.Flags().String("totp-secret", "", "Base32 TOTP secret for automatic 2FA") - agentsAuthRunCmd.Flags().String("proxy-id", "", "Proxy ID to use") - agentsAuthRunCmd.Flags().String("login-url", "", "Custom login page URL") - agentsAuthRunCmd.Flags().StringSlice("allowed-domain", []string{}, "Additional allowed domains") - agentsAuthRunCmd.Flags().Duration("timeout", 5*time.Minute, "Maximum time to wait for auth completion") - agentsAuthRunCmd.Flags().Bool("open", false, "Open live view URL in browser when human intervention needed") - _ = agentsAuthRunCmd.MarkFlagRequired("domain") - _ = agentsAuthRunCmd.MarkFlagRequired("profile") - - // Wire up commands - agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsCreateCmd) - agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsGetCmd) - agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsExchangeCmd) - agentsAuthInvocationsCmd.AddCommand(agentsAuthInvocationsSubmitCmd) - - agentsAuthCmd.AddCommand(agentsAuthCreateCmd) - agentsAuthCmd.AddCommand(agentsAuthGetCmd) - agentsAuthCmd.AddCommand(agentsAuthListCmd) - agentsAuthCmd.AddCommand(agentsAuthDeleteCmd) - agentsAuthCmd.AddCommand(agentsAuthInvocationsCmd) - agentsAuthCmd.AddCommand(agentsAuthRunCmd) - - agentsCmd.AddCommand(agentsAuthCmd) -} - -func runAgentsAuthCreate(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - domain, _ := cmd.Flags().GetString("domain") - profileName, _ := cmd.Flags().GetString("profile-name") - credentialName, _ := cmd.Flags().GetString("credential-name") - loginURL, _ := cmd.Flags().GetString("login-url") - allowedDomains, _ := cmd.Flags().GetStringSlice("allowed-domain") - proxyID, _ := cmd.Flags().GetString("proxy-id") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.Create(cmd.Context(), AgentAuthCreateInput{ - Domain: domain, - ProfileName: profileName, - CredentialName: credentialName, - LoginURL: loginURL, - AllowedDomains: allowedDomains, - ProxyID: proxyID, - Output: output, - }) -} - -func runAgentsAuthGet(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.Get(cmd.Context(), AgentAuthGetInput{ - ID: args[0], - Output: output, - }) -} - -func runAgentsAuthList(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - domain, _ := cmd.Flags().GetString("domain") - profileName, _ := cmd.Flags().GetString("profile-name") - limit, _ := cmd.Flags().GetInt("limit") - offset, _ := cmd.Flags().GetInt("offset") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.List(cmd.Context(), AgentAuthListInput{ - Domain: domain, - ProfileName: profileName, - Limit: limit, - Offset: offset, - Output: output, - }) -} - -func runAgentsAuthDelete(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - skip, _ := cmd.Flags().GetBool("yes") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.Delete(cmd.Context(), AgentAuthDeleteInput{ - ID: args[0], - SkipConfirm: skip, - }) -} - -func runAgentsAuthInvocationsCreate(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - authAgentID, _ := cmd.Flags().GetString("auth-agent-id") - saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.InvocationCreate(cmd.Context(), AgentAuthInvocationCreateInput{ - AuthAgentID: authAgentID, - SaveCredentialAs: saveCredentialAs, - Output: output, - }) -} - -func runAgentsAuthInvocationsGet(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.InvocationGet(cmd.Context(), AgentAuthInvocationGetInput{ - InvocationID: args[0], - Output: output, - }) -} - -func runAgentsAuthInvocationsExchange(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - code, _ := cmd.Flags().GetString("code") - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.InvocationExchange(cmd.Context(), AgentAuthInvocationExchangeInput{ - InvocationID: args[0], - Code: code, - Output: output, - }) -} - -func runAgentsAuthInvocationsSubmit(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - output, _ := cmd.Flags().GetString("output") - fieldPairs, _ := cmd.Flags().GetStringArray("field") - ssoButton, _ := cmd.Flags().GetString("sso-button") - mfaType, _ := cmd.Flags().GetString("mfa-type") - - // Parse field pairs into map - fieldValues := make(map[string]string) - for _, pair := range fieldPairs { - parts := strings.SplitN(pair, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid field format: %s (expected key=value)", pair) - } - fieldValues[parts[0]] = parts[1] - } - - svc := client.Agents.Auth - c := AgentAuthCmd{auth: &svc, invocations: &svc.Invocations} - return c.InvocationSubmit(cmd.Context(), AgentAuthInvocationSubmitInput{ - InvocationID: args[0], - FieldValues: fieldValues, - SSOButton: ssoButton, - SelectedMfaType: mfaType, - Output: output, - }) -} - -func runAgentsAuthRun(cmd *cobra.Command, args []string) error { - client := getKernelClient(cmd) - - output, _ := cmd.Flags().GetString("output") - domain, _ := cmd.Flags().GetString("domain") - profileName, _ := cmd.Flags().GetString("profile") - valuePairs, _ := cmd.Flags().GetStringArray("value") - credentialName, _ := cmd.Flags().GetString("credential") - saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") - totpSecret, _ := cmd.Flags().GetString("totp-secret") - proxyID, _ := cmd.Flags().GetString("proxy-id") - loginURL, _ := cmd.Flags().GetString("login-url") - allowedDomains, _ := cmd.Flags().GetStringSlice("allowed-domain") - timeout, _ := cmd.Flags().GetDuration("timeout") - openLiveView, _ := cmd.Flags().GetBool("open") - - // Parse value pairs into map - values := make(map[string]string) - for _, pair := range valuePairs { - parts := strings.SplitN(pair, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid value format: %s (expected key=value)", pair) - } - values[parts[0]] = parts[1] - } - - authSvc := client.Agents.Auth - profilesSvc := client.Profiles - credentialsSvc := client.Credentials - - c := AgentAuthRunCmd{ - auth: &authSvc, - invocations: &authSvc.Invocations, - profiles: &profilesSvc, - credentials: &credentialsSvc, - } - - return c.Run(cmd.Context(), AgentAuthRunInput{ - Domain: domain, - ProfileName: profileName, - Values: values, - CredentialName: credentialName, - SaveCredentialAs: saveCredentialAs, - TotpSecret: totpSecret, - ProxyID: proxyID, - LoginURL: loginURL, - AllowedDomains: allowedDomains, - Timeout: timeout, - OpenLiveView: openLiveView, - Output: output, - }) -} diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index affa011..ab5ee2a 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -64,9 +64,10 @@ type AuthConnectionDeleteInput struct { } type AuthConnectionLoginInput struct { - ID string - SaveCredentialAs string - Output string + ID string + ProxyID string + ProxyName string + Output string } type AuthConnectionSubmitInput struct { @@ -129,7 +130,7 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn if in.ProxyID != "" { params.ManagedAuthCreateRequest.Proxy = kernel.ManagedAuthCreateRequestProxyParam{ - ProxyID: kernel.Opt(in.ProxyID), + ID: kernel.Opt(in.ProxyID), } } @@ -315,8 +316,14 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu } params := kernel.AuthConnectionLoginParams{} - if in.SaveCredentialAs != "" { - params.LoginRequest.SaveCredentialAs = kernel.Opt(in.SaveCredentialAs) + if in.ProxyID != "" || in.ProxyName != "" { + params.Proxy = kernel.AuthConnectionLoginParamsProxy{} + if in.ProxyID != "" { + params.Proxy.ID = kernel.Opt(in.ProxyID) + } + if in.ProxyName != "" { + params.Proxy.Name = kernel.Opt(in.ProxyName) + } } if in.Output != "json" { @@ -559,7 +566,8 @@ func init() { // Login flags authConnectionsLoginCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") - authConnectionsLoginCmd.Flags().String("save-credential-as", "", "Save credentials under this name on success") + authConnectionsLoginCmd.Flags().String("proxy-id", "", "Proxy ID to use for this login") + authConnectionsLoginCmd.Flags().String("proxy-name", "", "Proxy name to use for this login") // Submit flags authConnectionsSubmitCmd.Flags().StringP("output", "o", "", "Output format: json for raw API response") @@ -659,14 +667,16 @@ func runAuthConnectionsDelete(cmd *cobra.Command, args []string) error { func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) output, _ := cmd.Flags().GetString("output") - saveCredentialAs, _ := cmd.Flags().GetString("save-credential-as") + proxyID, _ := cmd.Flags().GetString("proxy-id") + proxyName, _ := cmd.Flags().GetString("proxy-name") svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ - ID: args[0], - SaveCredentialAs: saveCredentialAs, - Output: output, + ID: args[0], + ProxyID: proxyID, + ProxyName: proxyName, + Output: output, }) } diff --git a/cmd/root.go b/cmd/root.go index de4b5bb..1bbee88 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -141,7 +141,6 @@ func init() { rootCmd.AddCommand(extensionsCmd) rootCmd.AddCommand(credentialsCmd) rootCmd.AddCommand(credentialProvidersCmd) - rootCmd.AddCommand(agentsCmd) rootCmd.AddCommand(createCmd) rootCmd.AddCommand(mcp.MCPCmd) rootCmd.AddCommand(upgradeCmd) diff --git a/go.mod b/go.mod index 4a547e5..436fdb3 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb + github.com/kernel/kernel-go-sdk v0.33.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pquerna/otp v1.5.0 github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 799bf0e..d254c19 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb h1:yu5PSECVvcZ7Erb5lvwbc8wLa4ncrZjT+EF6R9g4zaU= -github.com/kernel/kernel-go-sdk v0.32.1-0.20260210174239-c90e1da19efb/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.33.0 h1:kfk2bwrw3mbR4IW3JMnOj6Tecxor44YjM8YV153xDTY= +github.com/kernel/kernel-go-sdk v0.33.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= From 74bffb0b98b87ce011a164974eb3125a30ec4ffc Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Wed, 11 Feb 2026 11:56:17 -0500 Subject: [PATCH 8/9] fix: add nil checks and proper error returns for API failures - Return error instead of nil in runInvocationBrowsers on API failure - Add nil check on resp before accessing resp.Browsers - Add nil check on SSE stream before defer Close in Follow - Fix indentation in invoke SSE event handling Co-authored-by: Cursor --- cmd/auth_connections.go | 3 +++ cmd/invoke.go | 32 ++++++++++++++++++-------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index ab5ee2a..b82218b 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -409,6 +409,9 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn } stream := c.svc.FollowStreaming(ctx, in.ID) + if stream == nil { + return fmt.Errorf("failed to establish SSE stream") + } defer stream.Close() if in.Output != "json" { diff --git a/cmd/invoke.go b/cmd/invoke.go index 3b5091c..20e0066 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -195,21 +195,21 @@ func runInvoke(cmd *cobra.Command, args []string) error { if err == nil { fmt.Println(string(bs)) } - // Check for terminal states - if ev.Event == "invocation_state" { - stateEv := ev.AsInvocationState() - status := stateEv.Invocation.Status - if status == string(kernel.InvocationGetResponseStatusSucceeded) { - return nil + // Check for terminal states + if ev.Event == "invocation_state" { + stateEv := ev.AsInvocationState() + status := stateEv.Invocation.Status + if status == string(kernel.InvocationGetResponseStatusSucceeded) { + return nil + } + if status == string(kernel.InvocationGetResponseStatusFailed) { + return fmt.Errorf("invocation failed") + } } - if status == string(kernel.InvocationGetResponseStatusFailed) { - return fmt.Errorf("invocation failed") + if ev.Event == "error" { + errEv := ev.AsError() + return fmt.Errorf("%s: %s", errEv.Error.Code, errEv.Error.Message) } - } - if ev.Event == "error" { - errEv := ev.AsError() - return fmt.Errorf("%s: %s", errEv.Error.Code, errEv.Error.Message) - } continue } @@ -456,7 +456,11 @@ func runInvocationBrowsers(cmd *cobra.Command, args []string) error { resp, err := client.Invocations.ListBrowsers(cmd.Context(), invocationID) if err != nil { - pterm.Error.Printf("Failed to list browsers for invocation: %v\n", err) + return util.CleanedUpSdkError{Err: err} + } + + if resp == nil { + pterm.Info.Printf("No active browsers found for invocation %s\n", invocationID) return nil } From f014527f1604ed5e1ee3213e5ab30a41b437984a Mon Sep 17 00:00:00 2001 From: Mason Williams Date: Wed, 11 Feb 2026 12:16:16 -0500 Subject: [PATCH 9/9] fix: auth connections client init and add missing create flags - Fix auth exempt check so `auth connections` subcommands get an authenticated client (only `kernel auth` itself is exempt) - Add --proxy-name to create (parity with login command) - Add --no-save-credentials to create (defaults to saving) Co-authored-by: Cursor --- cmd/auth_connections.go | 25 +++++++++++++++++++++---- cmd/root.go | 5 ++++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index b82218b..5ff1e31 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -41,6 +41,9 @@ type AuthConnectionCreateInput struct { CredentialPath string CredentialAuto bool ProxyID string + ProxyName string + SaveCredentials bool + NoSaveCredentials bool HealthCheckInterval int Output string } @@ -128,12 +131,20 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn } } - if in.ProxyID != "" { - params.ManagedAuthCreateRequest.Proxy = kernel.ManagedAuthCreateRequestProxyParam{ - ID: kernel.Opt(in.ProxyID), + if in.ProxyID != "" || in.ProxyName != "" { + params.ManagedAuthCreateRequest.Proxy = kernel.ManagedAuthCreateRequestProxyParam{} + if in.ProxyID != "" { + params.ManagedAuthCreateRequest.Proxy.ID = kernel.Opt(in.ProxyID) + } + if in.ProxyName != "" { + params.ManagedAuthCreateRequest.Proxy.Name = kernel.Opt(in.ProxyName) } } + if in.NoSaveCredentials { + params.ManagedAuthCreateRequest.SaveCredentials = kernel.Opt(false) + } + if in.Output != "json" { pterm.Info.Printf("Creating managed auth for %s...\n", in.Domain) } @@ -549,7 +560,9 @@ func init() { authConnectionsCreateCmd.Flags().String("credential-provider", "", "External credential provider name") authConnectionsCreateCmd.Flags().String("credential-path", "", "Provider-specific path (e.g., VaultName/ItemName)") authConnectionsCreateCmd.Flags().Bool("credential-auto", false, "Lookup by domain from the specified provider") - authConnectionsCreateCmd.Flags().String("proxy-id", "", "Optional proxy ID to use") + authConnectionsCreateCmd.Flags().String("proxy-id", "", "Proxy ID to use") + authConnectionsCreateCmd.Flags().String("proxy-name", "", "Proxy name to use") + authConnectionsCreateCmd.Flags().Bool("no-save-credentials", false, "Disable saving credentials after successful login") authConnectionsCreateCmd.Flags().Int("health-check-interval", 0, "Interval in seconds between health checks (300-86400)") _ = authConnectionsCreateCmd.MarkFlagRequired("domain") _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") @@ -605,6 +618,8 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { credentialPath, _ := cmd.Flags().GetString("credential-path") credentialAuto, _ := cmd.Flags().GetBool("credential-auto") proxyID, _ := cmd.Flags().GetString("proxy-id") + proxyName, _ := cmd.Flags().GetString("proxy-name") + noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials") healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") svc := client.Auth.Connections @@ -619,6 +634,8 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { CredentialPath: credentialPath, CredentialAuto: credentialAuto, ProxyID: proxyID, + ProxyName: proxyName, + NoSaveCredentials: noSaveCredentials, HealthCheckInterval: healthCheckInterval, Output: output, }) diff --git a/cmd/root.go b/cmd/root.go index 1bbee88..5542dcb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -90,8 +90,11 @@ func isAuthExempt(cmd *cobra.Command) bool { // Check if the top-level command is in the exempt list switch topLevel.Name() { - case "login", "logout", "auth", "help", "completion", "create", "mcp", "upgrade": + case "login", "logout", "help", "completion", "create", "mcp", "upgrade": return true + case "auth": + // Only exempt the auth command itself (status display), not its subcommands + return cmd == topLevel } return false