From 9cf24318fab8b6c38e54c143242ae353093babeb Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:47:32 +0000 Subject: [PATCH 1/6] CLI: Update SDK to 0a28735 and add org entitlements command Bump github.com/kernel/kernel-go-sdk to v0.91.1-0.20260817203807-0a287359dcc5 (0a28735). Coverage gap found by enumerating all 140 methods in the SDK's api.md against the CLI command tree: the new Organization.Entitlements resource had no CLI surface. Everything else was already covered. New command: - `kernel org entitlements get` for client.Organization.Entitlements.Get (GET /org/entitlements). Renders Plan, Features, and Limits sections; supports --output json. Null constraint values mean unlimited in this API, and the SDK models them as non-pointer int64, so rendering keys off respjson field validity rather than the zero value. Tested against the real API: - kernel org entitlements get (table output, ENTERPRISE plan) - kernel org entitlements get --output json - kernel org entitlements get --output yaml (rejected as expected) - go build ./... and go test ./... pass, including 5 new unit tests covering populated constraints, null-as-unlimited, null plan fields, invalid --output, and API errors. Co-Authored-By: Claude Opus 5 --- README.md | 2 + cmd/org.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++- cmd/org_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 5 files changed, 308 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 96eed04e..64611f30 100644 --- a/README.md +++ b/README.md @@ -729,6 +729,8 @@ Automated authentication for web services. The `run` command orchestrates the fu - `kernel org limits set` - Set the default per-project concurrency cap applied to projects without an explicit override - `--default-project-max-concurrent-sessions ` - Default maximum concurrent browsers for projects without an explicit override (`0` to remove the default) - `--output json`, `-o json` - Output raw JSON object +- `kernel org entitlements get` - Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides; unlimited constraints are shown as `unlimited` + - `--output json`, `-o json` - Output raw JSON object ## Examples diff --git a/cmd/org.go b/cmd/org.go index ffd8c02b..790087bc 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -10,6 +11,7 @@ import ( "github.com/kernel/kernel-go-sdk/packages/param" "github.com/kernel/kernel-go-sdk/packages/respjson" "github.com/pterm/pterm" + "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -19,14 +21,24 @@ type OrgLimitsService interface { Update(ctx context.Context, body kernel.OrganizationLimitUpdateParams, opts ...option.RequestOption) (res *kernel.OrgLimits, err error) } +// OrgEntitlementsService defines the subset of the Kernel SDK organization entitlements client that we use. +type OrgEntitlementsService interface { + Get(ctx context.Context, opts ...option.RequestOption) (res *kernel.OrgEntitlements, err error) +} + type OrgCmd struct { - limits OrgLimitsService + limits OrgLimitsService + entitlements OrgEntitlementsService } type OrgLimitsGetInput struct { Output string } +type OrgEntitlementsGetInput struct { + Output string +} + type OrgLimitsSetInput struct { DefaultProjectMaxConcurrentSessions Int64Flag Output string @@ -88,6 +100,118 @@ func (c OrgCmd) LimitsSet(ctx context.Context, in OrgLimitsSetInput) error { return nil } +func (c OrgCmd) EntitlementsGet(ctx context.Context, in OrgEntitlementsGetInput) error { + if err := validateJSONOutput(in.Output); err != nil { + return err + } + + entitlements, err := c.entitlements.Get(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if entitlements == nil { + fmt.Println("null") + return nil + } + return util.PrintPrettyJSON(entitlements) + } + + renderOrgEntitlements(entitlements) + return nil +} + +func renderOrgEntitlements(ent *kernel.OrgEntitlements) { + if ent == nil { + pterm.Info.Println("No organization entitlements found") + return + } + + plan := ent.Plan + planRows := pterm.TableData{ + {"Field", "Value"}, + {"Plan", plan.ID}, + // Active trials resolve to a different effective plan than the + // contractual one, so show both. + {"Effective Plan", plan.EffectiveID}, + {"Trialing", lo.Ternary(plan.IsTrialing, "yes", "no")}, + // Billing status and trial end are both nullable. + {"Billing Status", formatOrgEntitlementString(plan.Status, plan.JSON.Status)}, + {"Trial Ends At", formatOrgEntitlementTime(plan.TrialEndsAt, plan.JSON.TrialEndsAt)}, + } + pterm.DefaultSection.Println("Plan") + PrintTableNoPad(planRows, true) + + f := ent.Features + featureRows := pterm.TableData{ + {"Feature", "Enabled", "Constraints"}, + {"Browser Extensions", formatOrgEntitlementEnabled(f.BrowserExtensions.Enabled), fmt.Sprintf("max stored per org: %s", formatProjectLimitValue(f.BrowserExtensions.MaxStoredPerOrg, f.BrowserExtensions.JSON.MaxStoredPerOrg))}, + {"Browser Pools", formatOrgEntitlementEnabled(f.BrowserPools.Enabled), ""}, + {"Browser Replays", formatOrgEntitlementEnabled(f.BrowserReplays.Enabled), fmt.Sprintf("retention: %s", formatOrgEntitlementDays(f.BrowserReplays.RetentionDays, f.BrowserReplays.JSON.RetentionDays))}, + {"Credential Providers", formatOrgEntitlementEnabled(f.CredentialProviders.Enabled), ""}, + {"Credentials", formatOrgEntitlementEnabled(f.Credentials.Enabled), ""}, + {"Custom Proxies", formatOrgEntitlementEnabled(f.CustomProxies.Enabled), ""}, + {"File I/O", formatOrgEntitlementEnabled(f.FileIo.Enabled), ""}, + {"GPU", formatOrgEntitlementEnabled(f.GPU.Enabled), ""}, + {"Managed Auth", formatOrgEntitlementEnabled(f.ManagedAuth.Enabled), formatManagedAuthConstraints(f.ManagedAuth)}, + {"Managed Proxies", formatOrgEntitlementEnabled(f.ManagedProxies.Enabled), ""}, + {"Profiles", formatOrgEntitlementEnabled(f.Profiles.Enabled), ""}, + {"Proxy Bypass Hosts", formatOrgEntitlementEnabled(f.ProxyBypassHosts.Enabled), ""}, + } + pterm.DefaultSection.Println("Features") + PrintTableNoPad(featureRows, true) + + l := ent.Limits + limitRows := pterm.TableData{ + {"Limit", "Value"}, + {"Max Concurrent Browsers", formatProjectLimitValue(l.MaxConcurrentBrowsers, l.JSON.MaxConcurrentBrowsers)}, + {"Max Concurrent Invocations", formatProjectLimitValue(l.MaxConcurrentInvocations, l.JSON.MaxConcurrentInvocations)}, + {"Default Max Concurrent Invocations Per App", formatProjectLimitValue(l.DefaultMaxConcurrentInvocationsPerApp, l.JSON.DefaultMaxConcurrentInvocationsPerApp)}, + } + pterm.DefaultSection.Println("Limits") + PrintTableNoPad(limitRows, true) +} + +func formatOrgEntitlementEnabled(enabled bool) string { + return lo.Ternary(enabled, "yes", "no") +} + +// formatManagedAuthConstraints summarizes the managed auth connection cap and the +// accepted health-check interval window in a single cell. +func formatManagedAuthConstraints(ma kernel.OrgEntitlementsFeaturesManagedAuth) string { + return fmt.Sprintf( + "max connections: %s, health check interval: %ds default (%ds-%ds)", + formatProjectLimitValue(ma.MaxConnections, ma.JSON.MaxConnections), + ma.HealthCheckIntervalDefaultSeconds, + ma.HealthCheckIntervalMinSeconds, + ma.HealthCheckIntervalMaxSeconds, + ) +} + +// formatOrgEntitlementDays renders a retention window, treating a null value as +// unlimited retention rather than zero days. +func formatOrgEntitlementDays(value int64, field respjson.Field) string { + if !field.Valid() { + return "unlimited" + } + return fmt.Sprintf("%d days", value) +} + +func formatOrgEntitlementString(value string, field respjson.Field) string { + if !field.Valid() || value == "" { + return "-" + } + return value +} + +func formatOrgEntitlementTime(value time.Time, field respjson.Field) string { + if !field.Valid() || value.IsZero() { + return "-" + } + return util.FormatLocal(value) +} + func renderOrgLimits(limits *kernel.OrgLimits) { if limits == nil { pterm.Info.Println("No organization limits found") @@ -144,6 +268,22 @@ var orgLimitsCmd = &cobra.Command{ }, } +var orgEntitlementsCmd = &cobra.Command{ + Use: "entitlements", + Short: "Read organization entitlements", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +var orgEntitlementsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get organization entitlements", + Long: "Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides. Unlimited constraints are shown as \"unlimited\".", + Args: cobra.NoArgs, + RunE: runOrgEntitlementsGet, +} + var orgLimitsGetCmd = &cobra.Command{ Use: "get", Short: "Get organization limits", @@ -162,7 +302,16 @@ var orgLimitsSetCmd = &cobra.Command{ func getOrgHandler(cmd *cobra.Command) OrgCmd { client := getKernelClient(cmd) - return OrgCmd{limits: &client.Organization.Limits} + return OrgCmd{ + limits: &client.Organization.Limits, + entitlements: &client.Organization.Entitlements, + } +} + +func runOrgEntitlementsGet(cmd *cobra.Command, args []string) error { + c := getOrgHandler(cmd) + output, _ := cmd.Flags().GetString("output") + return c.EntitlementsGet(cmd.Context(), OrgEntitlementsGetInput{Output: output}) } func runOrgLimitsGet(cmd *cobra.Command, args []string) error { @@ -189,7 +338,11 @@ func init() { orgLimitsSetCmd.Flags().Int64("default-project-max-concurrent-sessions", 0, "Default maximum concurrent browsers for projects without an explicit override (0 to remove the default)") addJSONOutputFlag(orgLimitsSetCmd) + addJSONOutputFlag(orgEntitlementsGetCmd) + orgLimitsCmd.AddCommand(orgLimitsGetCmd) orgLimitsCmd.AddCommand(orgLimitsSetCmd) + orgEntitlementsCmd.AddCommand(orgEntitlementsGetCmd) orgCmd.AddCommand(orgLimitsCmd) + orgCmd.AddCommand(orgEntitlementsCmd) } diff --git a/cmd/org_test.go b/cmd/org_test.go index 946800b0..ea2110db 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" @@ -177,3 +178,150 @@ func TestOrgLimitsSet_RejectsNegative(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "must be non-negative") } + +type FakeOrgEntitlementsService struct { + GetFunc func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) +} + +func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, opts...) + } + return &kernel.OrgEntitlements{}, nil +} + +// populatedEntitlements builds an entitlements payload with every nullable field +// present, so renders exercise the non-"unlimited" branches. +func populatedEntitlements() *kernel.OrgEntitlements { + ent := &kernel.OrgEntitlements{} + + ent.Plan.ID = "START_UP" + ent.Plan.EffectiveID = "START_UP" + ent.Plan.IsTrialing = true + ent.Plan.Status = "ACTIVE" + ent.Plan.TrialEndsAt = time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC) + ent.Plan.JSON.Status = respjson.NewField(`"ACTIVE"`) + ent.Plan.JSON.TrialEndsAt = respjson.NewField(`"2030-01-02T03:04:05Z"`) + + ent.Features.BrowserExtensions.Enabled = true + ent.Features.BrowserExtensions.MaxStoredPerOrg = 25 + ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField("25") + ent.Features.BrowserPools.Enabled = true + ent.Features.BrowserReplays.Enabled = true + ent.Features.BrowserReplays.RetentionDays = 7 + ent.Features.BrowserReplays.JSON.RetentionDays = respjson.NewField("7") + ent.Features.CredentialProviders.Enabled = true + ent.Features.Credentials.Enabled = true + ent.Features.CustomProxies.Enabled = false + ent.Features.FileIo.Enabled = true + ent.Features.GPU.Enabled = false + ent.Features.ManagedAuth.Enabled = true + ent.Features.ManagedAuth.MaxConnections = 10 + ent.Features.ManagedAuth.HealthCheckIntervalDefaultSeconds = 600 + ent.Features.ManagedAuth.HealthCheckIntervalMinSeconds = 300 + ent.Features.ManagedAuth.HealthCheckIntervalMaxSeconds = 86400 + ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField("10") + ent.Features.ManagedProxies.Enabled = true + ent.Features.Profiles.Enabled = true + ent.Features.ProxyBypassHosts.Enabled = true + + ent.Limits.MaxConcurrentBrowsers = 50 + ent.Limits.MaxConcurrentInvocations = 20 + ent.Limits.DefaultMaxConcurrentInvocationsPerApp = 5 + ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField("50") + ent.Limits.JSON.MaxConcurrentInvocations = respjson.NewField("20") + ent.Limits.JSON.DefaultMaxConcurrentInvocationsPerApp = respjson.NewField("5") + + return ent +} + +func TestOrgEntitlementsGet_RendersPlanFeaturesAndLimits(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return populatedEntitlements(), nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + // Plan section + assert.Contains(t, out, "START_UP") + assert.Contains(t, out, "Effective Plan") + assert.Contains(t, out, "Trialing") + assert.Contains(t, out, "ACTIVE") + // Features section — every feature should get a row. + for _, feature := range []string{ + "Browser Extensions", "Browser Pools", "Browser Replays", "Credential Providers", + "Credentials", "Custom Proxies", "File I/O", "GPU", "Managed Auth", + "Managed Proxies", "Profiles", "Proxy Bypass Hosts", + } { + assert.Contains(t, out, feature) + } + assert.Contains(t, out, "max stored per org: 25") + assert.Contains(t, out, "retention: 7 days") + assert.Contains(t, out, "max connections: 10") + assert.Contains(t, out, "600s default (300s-86400s)") + // Limits section + assert.Contains(t, out, "Max Concurrent Browsers") + assert.Contains(t, out, "Max Concurrent Invocations") + assert.Contains(t, out, "Default Max Concurrent Invocations Per App") +} + +func TestOrgEntitlementsGet_NullConstraintsShownAsUnlimited(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + ent := populatedEntitlements() + // Null (not omitted) constraints mean unlimited. + ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField(respjson.Null) + ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField(respjson.Null) + ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField(respjson.Null) + return ent, nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + assert.Contains(t, out, "max stored per org: unlimited") + assert.Contains(t, out, "max connections: unlimited") + assert.Contains(t, out, "unlimited") +} + +func TestOrgEntitlementsGet_NullPlanFieldsShownAsDash(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + ent := populatedEntitlements() + ent.Plan.IsTrialing = false + ent.Plan.JSON.Status = respjson.NewField(respjson.Null) + ent.Plan.JSON.TrialEndsAt = respjson.NewField(respjson.Null) + return ent, nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + assert.Contains(t, out, "Billing Status") + assert.Contains(t, out, "Trial Ends At") + assert.NotContains(t, out, "ACTIVE") +} + +func TestOrgEntitlementsGet_RejectsUnknownOutput(t *testing.T) { + c := OrgCmd{entitlements: &FakeOrgEntitlementsService{}} + assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{Output: "yaml"})) +} + +func TestOrgEntitlementsGet_SurfacesAPIError(t *testing.T) { + capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return nil, errors.New("boom") + }, + } + c := OrgCmd{entitlements: fake} + assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) +} diff --git a/go.mod b/go.mod index ebe99635..85d0a029 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.91.0 + github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 7808eb39..b91fd6b3 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.91.0 h1:/bJKFJQ8ZwAyl+r8P1sUW8NQYEjDekYZJ5R8Sml5bus= -github.com/kernel/kernel-go-sdk v0.91.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5 h1:Kaq0Dhh1VW36HzqOUOpvWnB1PF3XtPekC++RYdgePNQ= +github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From dfdba4fccc49d1e67bce163b8122769024932fbc Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:07:08 +0000 Subject: [PATCH 2/6] CLI: Update Go SDK to v0.92.0 (a156820) Updates github.com/kernel/kernel-go-sdk from v0.91.1-0.20260817203807-0a287359dcc5 to v0.92.0. ## Coverage Analysis Diffing the two module sources shows the SDK API surface is byte-identical between these versions -- the only changes are release metadata (.release-please-manifest.json, CHANGELOG.md, README.md, internal/version.go). A full enumeration was still performed: - All 140 SDK methods in api.md have corresponding CLI commands. - The 4 x-cli-skip endpoints (/site-configs/lookup, /site-configs/resolve, /site-configs/analyses/{id}, /auth/connections/{id}/exchange) are absent from the SDK surface, so nothing to skip. - All params struct fields are covered by CLI flags except three, each intentional: - AuthConnectionLoginParams.BrowserTelemetry -- deprecated in favor of browser.telemetry, which the CLI already uses via ManagedAuthBrowserConfigParam. - AuditLogListParams.PageToken -- opaque cursor handled internally by ListAutoPaging; CLI exposes --limit instead. - BrowserCurlParams.TimeoutMs / ResponseEncoding -- `browsers curl` is implemented against browsers.HTTPClient rather than the SDK curl endpoint; --max-time covers the timeout and raw bytes are streamed, so response encoding is not applicable. No coverage gaps found; no new commands or flags added. ## Tested - go build ./... and go vet ./... clean - go test ./... all packages pass - Smoke tested rebuilt binary against the live API: `kernel browsers list` Triggered by: kernel/kernel-go-sdk@a1568205c576686eeafc634fff0ea72b75c28c0e Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 85d0a029..502421b9 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.91.1-0.20260817203807-0a287359dcc5 + github.com/kernel/kernel-go-sdk v0.92.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index b91fd6b3..04679b80 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.91.1-0.20260817203807-0a287359dcc5 h1:Kaq0Dhh1VW36HzqOUOpvWnB1PF3XtPekC++RYdgePNQ= -github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= +github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 8a7b36334f3cc3f878df9b17d90da78d24e5b6d2 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:13:13 +0000 Subject: [PATCH 3/6] CLI: Update Go SDK to 6e62bf5 and track managed-auth field reason Bumps kernel-go-sdk to 6e62bf5b91e5d315b90b6c9c7296e09e312fb338. That SDK release reshapes the canonical managed-auth input field: the boolean `replace_existing` is gone and a `reason` enum ("missing" | "rejected") takes its place, so `auth connections get` and the `auth connections follow` event stream now render `reason=` instead of the `replace-existing` marker. A rejected credential is still visible, now alongside the missing-value case it could not previously express. A full enumeration of api.md against the CLI's service interfaces and flags found no other coverage gaps: all 136 non-x-cli-skip SDK methods have commands, and every params field maps to an existing flag. Tested: auth connections list, auth connections get (table + json), browsers create -t 60, browsers get , browsers delete against the live API; go build ./... and go test ./cmd/... pass. Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 46 ++++++++++++++++++------------------ cmd/auth_connections_test.go | 5 +++- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 79a34567..4237e9a3 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -414,13 +414,13 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn // models the one on `get` and the one on the `follow` event stream as two // identical but distinct types, so both are converted to this before rendering. type managedAuthInputField struct { - ID string - Label string - Type string - Ref string - Hint string - Required bool - ReplaceExisting bool + ID string + Label string + Type string + Ref string + Hint string + Reason string + Required bool } // managedAuthInputChoice is the choice counterpart of managedAuthInputField. @@ -448,8 +448,8 @@ func formatManagedAuthField(f managedAuthInputField) string { if f.Required { meta = append(meta, "required") } - if f.ReplaceExisting { - meta = append(meta, "replace-existing") + if f.Reason != "" { + meta = append(meta, "reason="+f.Reason) } if f.Hint != "" { meta = append(meta, fmt.Sprintf("hint=%q", f.Hint)) @@ -542,13 +542,13 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e fields := make([]string, 0, len(auth.Fields)) for _, f := range auth.Fields { fields = append(fields, formatManagedAuthField(managedAuthInputField{ - ID: f.ID, - Label: f.Label, - Type: f.Type, - Ref: f.Ref, - Hint: f.Hint, - Required: f.Required, - ReplaceExisting: f.ReplaceExisting, + ID: f.ID, + Label: f.Label, + Type: f.Type, + Ref: f.Ref, + Hint: f.Hint, + Reason: f.Reason, + Required: f.Required, })) } tableData = append(tableData, []string{"Fields", strings.Join(fields, "; ")}) @@ -1067,13 +1067,13 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn fields := make([]string, 0, len(state.Fields)) for _, f := range state.Fields { fields = append(fields, formatManagedAuthField(managedAuthInputField{ - ID: f.ID, - Label: f.Label, - Type: f.Type, - Ref: f.Ref, - Hint: f.Hint, - Required: f.Required, - ReplaceExisting: f.ReplaceExisting, + ID: f.ID, + Label: f.Label, + Type: f.Type, + Ref: f.Ref, + Hint: f.Hint, + Reason: f.Reason, + Required: f.Required, })) } pterm.Info.Printf(" Fields: %s\n", strings.Join(fields, ", ")) diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index b1c654a9..a403464e 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -154,6 +154,7 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { Type: "code", Ref: "totp_code", Hint: "Enter the code sent to +1 ••• ••• 1234", + Reason: "rejected", Required: true, }, }, @@ -182,7 +183,9 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { out := outBuf.String() assert.Contains(t, out, `otp (One-time code)`) - assert.Contains(t, out, `code, ref=totp_code, required`) + // The reason tells the user why the field is being asked for: "rejected" + // means a stored credential was refused, so a new value has to replace it. + assert.Contains(t, out, `code, ref=totp_code, required, reason=rejected`) assert.Contains(t, out, `hint="Enter the code sent to +1 ••• ••• 1234"`) assert.Contains(t, out, `mfa_sms (Text message)`) assert.Contains(t, out, `mfa_method, sms, to=+1 ••• ••• 1234`) diff --git a/go.mod b/go.mod index 502421b9..52d4a655 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.92.0 + github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 04679b80..ef49c798 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= -github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5 h1:xnui88jn6CAp2Ys15AP7aagFGPveqrT/3LfdDsuIeY4= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From ca46838accfb10bf06678d3de3185d3dbca51f3a Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:02:46 +0000 Subject: [PATCH 4/6] CLI: Update Go SDK to 796d424 and bind canonical submits to interactions Bumps kernel-go-sdk to 796d4245c87a39acbb0d408b05f0de830c500772. That SDK release adds `interaction_id` to managed auth state and to the submit request. The API requires it for canonical submissions (field_values / selected_choice_id) and rejects it when paired with a legacy submit mode, so before this change every canonical `auth connections submit` failed with "interaction_id is required for canonical submissions". `auth connections submit` gains --interaction-id. Left off, the CLI reads the connection's current interaction ID, since the ID changes on every actionable pause and the freshly read one is the only sane default; passing it pins the submission so the API can reject it as stale. Legacy submit modes never send one, and --interaction-id with a legacy mode is rejected locally with the same rule the API enforces. `auth connections get` and `follow` now show the interaction ID next to the canonical fields and choices it scopes. Also resolves the stale merge of main into this branch, which had left two competing org entitlements implementations in cmd/org.go (the branch built `org entitlements get`; main shipped `org entitlements` in #232) so the package no longer compiled. Main's reviewed version wins. A full enumeration of api.md against the CLI found no other gaps: all 136 non-x-cli-skip SDK methods have commands, and the only new params field in this release is SubmitFieldsRequest.interaction_id. Tested against the live API: created a managed auth connection, started a login flow, and confirmed `get` (table + JSON) and `follow` render the interaction ID at AWAITING_INPUT; canonical submit with and without --interaction-id now clears the API's interaction validation (it stops at this org's submit-v2 feature gate, while the same request sent without interaction_id still returns "interaction_id is required"); legacy `--field` submit still accepted; `--interaction-id` with `--field` rejected locally; org entitlements, browsers create/get/delete pass. go build ./..., go vet ./... and go test ./... pass. Co-Authored-By: Claude Opus 5 --- README.md | 3 +- cmd/auth_connections.go | 54 ++++++++++++- cmd/auth_connections_test.go | 127 +++++++++++++++++++++++++++--- cmd/org.go | 137 -------------------------------- cmd/org_test.go | 147 ----------------------------------- go.mod | 2 +- go.sum | 4 +- 7 files changed, 174 insertions(+), 300 deletions(-) diff --git a/README.md b/README.md index b45c27af..4f7655e4 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,7 @@ Managed auth connections (`kernel auth connections`). The commands below are new - `kernel auth connections submit ` - New flags: - `--field-value ` - Canonical field-id=value pair from the connection's `fields` list (repeatable); preferred over the legacy `--field` - `--choice-id ` - Canonical choice ID from the connection's `choices` list + - `--interaction-id ` - Canonical interaction the submitted values answer. Only valid with `--field-value` or `--choice-id`; omit it and the CLI reads the connection's current interaction ID for you. Pass it to pin the submission, so the API rejects it if the flow has already moved on. `kernel auth connections get` and `follow` list those IDs alongside the metadata the API captured for them, so you can tell the options apart before submitting. Fields show their type, ref, and any hint (which names the masked destination a one-time code was sent to); choices show their type, semantic MFA method (`sms`, `totp`, `push`, …), and masked destination. @@ -731,8 +732,6 @@ Automated authentication for web services. The `run` command orchestrates the fu - `kernel org limits set` - Set the default per-project concurrency cap applied to projects without an explicit override - `--default-project-max-concurrent-sessions ` - Default maximum concurrent browsers for projects without an explicit override (`0` to remove the default) - `--output json`, `-o json` - Output raw JSON object -- `kernel org entitlements get` - Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides; unlimited constraints are shown as `unlimited` - - `--output json`, `-o json` - Output raw JSON object ## Examples diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 4237e9a3..c6aedcfa 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -130,7 +130,11 @@ type AuthConnectionSubmitInput struct { // canonical `field_values` keyed by the field IDs the API returned. CanonicalFieldValues map[string]string // SelectedChoiceID is the canonical choice ID from the API's `choices` list. - SelectedChoiceID string + SelectedChoiceID string + // InteractionID pins the submission to the canonical interaction the values + // were read from. Left empty, the CLI reads the connection's current + // interaction ID, since the API requires one for canonical submissions. + InteractionID string MfaOptionID string SignInOptionID string SSOButtonSelector string @@ -538,6 +542,11 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e // Canonical fields/choices supersede discovered_fields, mfa_options and // pending_sso_buttons. Show them first so the IDs needed by `submit // --field-value` and `submit --choice-id` are the first thing visible. + // The interaction ID scopes those submissions and only accompanies canonical + // input, so show it alongside them. + if auth.InteractionID != "" { + tableData = append(tableData, []string{"Interaction ID", auth.InteractionID}) + } if len(auth.Fields) > 0 { fields := make([]string, 0, len(auth.Fields)) for _, f := range auth.Fields { @@ -838,6 +847,28 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn return fmt.Errorf("provide exactly one of: %s", submitModeFlags) } + // The API binds canonical submissions to the interaction the values were read + // from, and rejects an interaction ID sent with a legacy submit mode. + isCanonical := hasCanonicalFields || hasChoice + if in.InteractionID != "" && !isCanonical { + return fmt.Errorf("the --interaction-id flag is only valid with --field-value or --choice-id") + } + if isCanonical && in.InteractionID == "" { + // Resolve the current interaction rather than making the user copy it out + // of `get` or `follow` first. The ID changes on every actionable pause, so + // the freshly read one is the only one worth defaulting to; passing + // --interaction-id explicitly pins the submission to an older interaction + // and lets the API reject it as stale. + conn, err := c.svc.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for interaction ID resolution: %w", err)} + } + if conn == nil || conn.InteractionID == "" { + return fmt.Errorf("connection %s has no canonical interaction awaiting input; run 'kernel auth connections get %s' to see what the flow is waiting on", in.ID, in.ID) + } + in.InteractionID = conn.InteractionID + } + // Resolve MFA option: the user may pass the label (e.g. "Get a text"), the // type (e.g. "sms"), or the display string ("Get a text (sms)"). The API // expects the type, so look up the connection's available options and map @@ -884,6 +915,9 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn if hasChoice { params.SubmitFieldsRequest.SelectedChoiceID = kernel.Opt(in.SelectedChoiceID) } + if in.InteractionID != "" { + params.SubmitFieldsRequest.InteractionID = kernel.Opt(in.InteractionID) + } if hasMfaOption { params.SubmitFieldsRequest.MfaOptionID = kernel.Opt(in.MfaOptionID) } @@ -1063,6 +1097,9 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn state.Timestamp.Local().Format(time.RFC3339), state.FlowStatus, state.FlowStep) + if state.InteractionID != "" { + pterm.Info.Printf(" Interaction ID: %s\n", state.InteractionID) + } if len(state.Fields) > 0 { fields := make([]string, 0, len(state.Fields)) for _, f := range state.Fields { @@ -1181,8 +1218,18 @@ var authConnectionsSubmitCmd = &cobra.Command{ Short: "Submit field values to a login flow", Long: `Submit field values for the login form. Poll the managed auth to track progress. +Canonical submissions (--field-value, --choice-id) are bound to the interaction +they answer. The CLI reads the connection's current interaction ID for you; pass +--interaction-id to pin the submission to a specific interaction instead. + Examples: - # Submit field values + # Submit canonical field values from the connection's fields list + kernel auth connections submit --field-value field_email=me@example.com --field-value field_password=secret + + # Answer a specific interaction (rejected if the flow has moved on) + kernel auth connections submit --choice-id mfa_sms --interaction-id mai_abc123xyz + + # Submit legacy field values kernel auth connections submit --field username=myuser --field password=mypass # Select an MFA option @@ -1291,6 +1338,7 @@ func init() { addJSONOutputFlag(authConnectionsSubmitCmd) authConnectionsSubmitCmd.Flags().StringArray("field-value", []string{}, "Canonical field-id=value pair from the connection's `fields` list (repeatable)") authConnectionsSubmitCmd.Flags().String("choice-id", "", "Canonical choice ID from the connection's `choices` list") + authConnectionsSubmitCmd.Flags().String("interaction-id", "", "Canonical interaction ID the submitted values belong to; defaults to the connection's current interaction. Only valid with --field-value or --choice-id") authConnectionsSubmitCmd.Flags().StringArray("field", []string{}, "Legacy field name=value pair (repeatable); prefer --field-value") authConnectionsSubmitCmd.Flags().String("mfa-option-id", "", "MFA option ID if user selected an MFA method") authConnectionsSubmitCmd.Flags().String("sign-in-option-id", "", "Sign-in option ID if the flow returned non-MFA choices") @@ -1516,6 +1564,7 @@ func runAuthConnectionsSubmit(cmd *cobra.Command, args []string) error { fieldPairs, _ := cmd.Flags().GetStringArray("field") canonicalFieldPairs, _ := cmd.Flags().GetStringArray("field-value") choiceID, _ := cmd.Flags().GetString("choice-id") + interactionID, _ := cmd.Flags().GetString("interaction-id") mfaOptionID, _ := cmd.Flags().GetString("mfa-option-id") signInOptionID, _ := cmd.Flags().GetString("sign-in-option-id") ssoButtonSelector, _ := cmd.Flags().GetString("sso-button-selector") @@ -1543,6 +1592,7 @@ func runAuthConnectionsSubmit(cmd *cobra.Command, args []string) error { FieldValues: fieldValues, CanonicalFieldValues: canonicalFieldValues, SelectedChoiceID: choiceID, + InteractionID: interactionID, MfaOptionID: mfaOptionID, SignInOptionID: signInOptionID, SSOButtonSelector: ssoButtonSelector, diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index a403464e..4466e483 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -147,6 +147,9 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { Status: kernel.ManagedAuthStatusNeedsAuth, FlowStatus: kernel.ManagedAuthFlowStatusInProgress, FlowStep: kernel.ManagedAuthFlowStepAwaitingInput, + // Canonical fields and choices always arrive with the interaction + // they belong to, which `submit` needs. + InteractionID: "mai_abc123xyz", Fields: []kernel.ManagedAuthField{ { ID: "otp", @@ -182,6 +185,7 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "e0x3vbw4z66kpwny3k5k46tj"})) out := outBuf.String() + assert.Contains(t, out, `mai_abc123xyz`) assert.Contains(t, out, `otp (One-time code)`) // The reason tells the user why the field is being asked for: "rejected" // means a stored credential was refused, so a new value has to replace it. @@ -823,16 +827,24 @@ func TestLogin_TelemetryOverride(t *testing.T) { assert.True(t, captured.Browser.Telemetry.Browser.Screenshot.Enabled.Value) } -func TestSubmit_CanonicalChoiceID(t *testing.T) { - capturePtermOutput(t) - var captured kernel.AuthConnectionSubmitParams - fake := &FakeAuthConnectionService{ +// canonicalSubmitFake serves the current interaction ID from `get` and captures +// what `submit` sends, which is what every canonical submission needs. +func canonicalSubmitFake(interactionID string, captured *kernel.AuthConnectionSubmitParams) *FakeAuthConnectionService { + return &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ID: id, InteractionID: interactionID}, nil + }, SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { - captured = body + *captured = body return &kernel.SubmitFieldsResponse{Accepted: true}, nil }, } - c := AuthConnectionCmd{svc: fake} +} + +func TestSubmit_CanonicalChoiceID(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ ID: "auth_1", SelectedChoiceID: "choice_sms", @@ -844,6 +856,53 @@ func TestSubmit_CanonicalChoiceID(t *testing.T) { } func TestSubmit_CanonicalFieldValues(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, + })) + assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues) + assert.Nil(t, captured.SubmitFieldsRequest.Fields) +} + +func TestSubmit_CanonicalResolvesCurrentInteractionID(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, + })) + require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid()) + assert.Equal(t, "mai_current", captured.SubmitFieldsRequest.InteractionID.Value) +} + +func TestSubmit_ExplicitInteractionIDIsNotOverwritten(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + fake := canonicalSubmitFake("mai_current", &captured) + getCalls := 0 + inner := fake.GetFunc + fake.GetFunc = func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + getCalls++ + return inner(ctx, id, opts...) + } + c := AuthConnectionCmd{svc: fake} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + SelectedChoiceID: "choice_sms", + // Pinning an older interaction is how a caller detects that the flow moved + // on, so the CLI must forward it untouched. + InteractionID: "mai_pinned", + })) + assert.Equal(t, 0, getCalls) + require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid()) + assert.Equal(t, "mai_pinned", captured.SubmitFieldsRequest.InteractionID.Value) +} + +func TestSubmit_LegacyModeOmitsInteractionID(t *testing.T) { capturePtermOutput(t) var captured kernel.AuthConnectionSubmitParams fake := &FakeAuthConnectionService{ @@ -854,11 +913,61 @@ func TestSubmit_CanonicalFieldValues(t *testing.T) { } c := AuthConnectionCmd{svc: fake} require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + FieldValues: map[string]string{"username": "me"}, + })) + // The API rejects an interaction ID paired with a legacy submit mode. + assert.False(t, captured.SubmitFieldsRequest.InteractionID.Valid()) +} + +func TestSubmit_InteractionIDRequiresCanonicalMode(t *testing.T) { + capturePtermOutput(t) + c := AuthConnectionCmd{svc: &FakeAuthConnectionService{}} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + FieldValues: map[string]string{"username": "me"}, + InteractionID: "mai_current", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "the --interaction-id flag is only valid with --field-value or --choice-id") +} + +func TestSubmit_CanonicalWithoutPendingInteractionErrors(t *testing.T) { + capturePtermOutput(t) + submitted := false + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ID: id}, nil + }, + SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submitted = true + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + }, + } + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + SelectedChoiceID: "choice_sms", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "no canonical interaction awaiting input") + assert.False(t, submitted) +} + +func TestSubmit_CanonicalGetErrorSurfaced(t *testing.T) { + capturePtermOutput(t) + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return nil, errors.New("boom") + }, + } + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ ID: "auth_1", CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, - })) - assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues) - assert.Nil(t, captured.SubmitFieldsRequest.Fields) + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "interaction ID resolution") } func TestSubmit_CanonicalAndLegacyAreMutuallyExclusive(t *testing.T) { diff --git a/cmd/org.go b/cmd/org.go index f6b58675..e8ba0d52 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -12,7 +11,6 @@ import ( "github.com/kernel/kernel-go-sdk/packages/param" "github.com/kernel/kernel-go-sdk/packages/respjson" "github.com/pterm/pterm" - "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -36,10 +34,6 @@ type OrgLimitsGetInput struct { Output string } -type OrgEntitlementsGetInput struct { - Output string -} - type OrgLimitsSetInput struct { DefaultProjectMaxConcurrentSessions Int64Flag Output string @@ -127,118 +121,6 @@ func (c OrgCmd) LimitsSet(ctx context.Context, in OrgLimitsSetInput) error { return nil } -func (c OrgCmd) EntitlementsGet(ctx context.Context, in OrgEntitlementsGetInput) error { - if err := validateJSONOutput(in.Output); err != nil { - return err - } - - entitlements, err := c.entitlements.Get(ctx) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - if entitlements == nil { - fmt.Println("null") - return nil - } - return util.PrintPrettyJSON(entitlements) - } - - renderOrgEntitlements(entitlements) - return nil -} - -func renderOrgEntitlements(ent *kernel.OrgEntitlements) { - if ent == nil { - pterm.Info.Println("No organization entitlements found") - return - } - - plan := ent.Plan - planRows := pterm.TableData{ - {"Field", "Value"}, - {"Plan", plan.ID}, - // Active trials resolve to a different effective plan than the - // contractual one, so show both. - {"Effective Plan", plan.EffectiveID}, - {"Trialing", lo.Ternary(plan.IsTrialing, "yes", "no")}, - // Billing status and trial end are both nullable. - {"Billing Status", formatOrgEntitlementString(plan.Status, plan.JSON.Status)}, - {"Trial Ends At", formatOrgEntitlementTime(plan.TrialEndsAt, plan.JSON.TrialEndsAt)}, - } - pterm.DefaultSection.Println("Plan") - PrintTableNoPad(planRows, true) - - f := ent.Features - featureRows := pterm.TableData{ - {"Feature", "Enabled", "Constraints"}, - {"Browser Extensions", formatOrgEntitlementEnabled(f.BrowserExtensions.Enabled), fmt.Sprintf("max stored per org: %s", formatProjectLimitValue(f.BrowserExtensions.MaxStoredPerOrg, f.BrowserExtensions.JSON.MaxStoredPerOrg))}, - {"Browser Pools", formatOrgEntitlementEnabled(f.BrowserPools.Enabled), ""}, - {"Browser Replays", formatOrgEntitlementEnabled(f.BrowserReplays.Enabled), fmt.Sprintf("retention: %s", formatOrgEntitlementDays(f.BrowserReplays.RetentionDays, f.BrowserReplays.JSON.RetentionDays))}, - {"Credential Providers", formatOrgEntitlementEnabled(f.CredentialProviders.Enabled), ""}, - {"Credentials", formatOrgEntitlementEnabled(f.Credentials.Enabled), ""}, - {"Custom Proxies", formatOrgEntitlementEnabled(f.CustomProxies.Enabled), ""}, - {"File I/O", formatOrgEntitlementEnabled(f.FileIo.Enabled), ""}, - {"GPU", formatOrgEntitlementEnabled(f.GPU.Enabled), ""}, - {"Managed Auth", formatOrgEntitlementEnabled(f.ManagedAuth.Enabled), formatManagedAuthConstraints(f.ManagedAuth)}, - {"Managed Proxies", formatOrgEntitlementEnabled(f.ManagedProxies.Enabled), ""}, - {"Profiles", formatOrgEntitlementEnabled(f.Profiles.Enabled), ""}, - {"Proxy Bypass Hosts", formatOrgEntitlementEnabled(f.ProxyBypassHosts.Enabled), ""}, - } - pterm.DefaultSection.Println("Features") - PrintTableNoPad(featureRows, true) - - l := ent.Limits - limitRows := pterm.TableData{ - {"Limit", "Value"}, - {"Max Concurrent Browsers", formatProjectLimitValue(l.MaxConcurrentBrowsers, l.JSON.MaxConcurrentBrowsers)}, - {"Max Concurrent Invocations", formatProjectLimitValue(l.MaxConcurrentInvocations, l.JSON.MaxConcurrentInvocations)}, - {"Default Max Concurrent Invocations Per App", formatProjectLimitValue(l.DefaultMaxConcurrentInvocationsPerApp, l.JSON.DefaultMaxConcurrentInvocationsPerApp)}, - } - pterm.DefaultSection.Println("Limits") - PrintTableNoPad(limitRows, true) -} - -func formatOrgEntitlementEnabled(enabled bool) string { - return lo.Ternary(enabled, "yes", "no") -} - -// formatManagedAuthConstraints summarizes the managed auth connection cap and the -// accepted health-check interval window in a single cell. -func formatManagedAuthConstraints(ma kernel.OrgEntitlementsFeaturesManagedAuth) string { - return fmt.Sprintf( - "max connections: %s, health check interval: %ds default (%ds-%ds)", - formatProjectLimitValue(ma.MaxConnections, ma.JSON.MaxConnections), - ma.HealthCheckIntervalDefaultSeconds, - ma.HealthCheckIntervalMinSeconds, - ma.HealthCheckIntervalMaxSeconds, - ) -} - -// formatOrgEntitlementDays renders a retention window, treating a null value as -// unlimited retention rather than zero days. -func formatOrgEntitlementDays(value int64, field respjson.Field) string { - if !field.Valid() { - return "unlimited" - } - return fmt.Sprintf("%d days", value) -} - -func formatOrgEntitlementString(value string, field respjson.Field) string { - if !field.Valid() || value == "" { - return "-" - } - return value -} - -func formatOrgEntitlementTime(value time.Time, field respjson.Field) string { - if !field.Valid() || value.IsZero() { - return "-" - } - return util.FormatLocal(value) -} - func renderOrgLimits(limits *kernel.OrgLimits) { if limits == nil { pterm.Info.Println("No organization limits found") @@ -370,22 +252,6 @@ var orgLimitsCmd = &cobra.Command{ }, } -var orgEntitlementsCmd = &cobra.Command{ - Use: "entitlements", - Short: "Read organization entitlements", - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var orgEntitlementsGetCmd = &cobra.Command{ - Use: "get", - Short: "Get organization entitlements", - Long: "Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides. Unlimited constraints are shown as \"unlimited\".", - Args: cobra.NoArgs, - RunE: runOrgEntitlementsGet, -} - var orgLimitsGetCmd = &cobra.Command{ Use: "get", Short: "Get organization limits", @@ -449,11 +315,8 @@ func init() { addJSONOutputFlag(orgLimitsSetCmd) addJSONOutputFlag(orgEntitlementsCmd) - addJSONOutputFlag(orgEntitlementsGetCmd) - orgLimitsCmd.AddCommand(orgLimitsGetCmd) orgLimitsCmd.AddCommand(orgLimitsSetCmd) - orgEntitlementsCmd.AddCommand(orgEntitlementsGetCmd) orgCmd.AddCommand(orgLimitsCmd) orgCmd.AddCommand(orgEntitlementsCmd) } diff --git a/cmd/org_test.go b/cmd/org_test.go index 464d2a9a..e55713fd 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -413,150 +413,3 @@ func TestOrgLimitsSet_RejectsNegative(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "must be non-negative") } - -type FakeOrgEntitlementsService struct { - GetFunc func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) -} - -func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - if f.GetFunc != nil { - return f.GetFunc(ctx, opts...) - } - return &kernel.OrgEntitlements{}, nil -} - -// populatedEntitlements builds an entitlements payload with every nullable field -// present, so renders exercise the non-"unlimited" branches. -func populatedEntitlements() *kernel.OrgEntitlements { - ent := &kernel.OrgEntitlements{} - - ent.Plan.ID = "START_UP" - ent.Plan.EffectiveID = "START_UP" - ent.Plan.IsTrialing = true - ent.Plan.Status = "ACTIVE" - ent.Plan.TrialEndsAt = time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC) - ent.Plan.JSON.Status = respjson.NewField(`"ACTIVE"`) - ent.Plan.JSON.TrialEndsAt = respjson.NewField(`"2030-01-02T03:04:05Z"`) - - ent.Features.BrowserExtensions.Enabled = true - ent.Features.BrowserExtensions.MaxStoredPerOrg = 25 - ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField("25") - ent.Features.BrowserPools.Enabled = true - ent.Features.BrowserReplays.Enabled = true - ent.Features.BrowserReplays.RetentionDays = 7 - ent.Features.BrowserReplays.JSON.RetentionDays = respjson.NewField("7") - ent.Features.CredentialProviders.Enabled = true - ent.Features.Credentials.Enabled = true - ent.Features.CustomProxies.Enabled = false - ent.Features.FileIo.Enabled = true - ent.Features.GPU.Enabled = false - ent.Features.ManagedAuth.Enabled = true - ent.Features.ManagedAuth.MaxConnections = 10 - ent.Features.ManagedAuth.HealthCheckIntervalDefaultSeconds = 600 - ent.Features.ManagedAuth.HealthCheckIntervalMinSeconds = 300 - ent.Features.ManagedAuth.HealthCheckIntervalMaxSeconds = 86400 - ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField("10") - ent.Features.ManagedProxies.Enabled = true - ent.Features.Profiles.Enabled = true - ent.Features.ProxyBypassHosts.Enabled = true - - ent.Limits.MaxConcurrentBrowsers = 50 - ent.Limits.MaxConcurrentInvocations = 20 - ent.Limits.DefaultMaxConcurrentInvocationsPerApp = 5 - ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField("50") - ent.Limits.JSON.MaxConcurrentInvocations = respjson.NewField("20") - ent.Limits.JSON.DefaultMaxConcurrentInvocationsPerApp = respjson.NewField("5") - - return ent -} - -func TestOrgEntitlementsGet_RendersPlanFeaturesAndLimits(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return populatedEntitlements(), nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - // Plan section - assert.Contains(t, out, "START_UP") - assert.Contains(t, out, "Effective Plan") - assert.Contains(t, out, "Trialing") - assert.Contains(t, out, "ACTIVE") - // Features section — every feature should get a row. - for _, feature := range []string{ - "Browser Extensions", "Browser Pools", "Browser Replays", "Credential Providers", - "Credentials", "Custom Proxies", "File I/O", "GPU", "Managed Auth", - "Managed Proxies", "Profiles", "Proxy Bypass Hosts", - } { - assert.Contains(t, out, feature) - } - assert.Contains(t, out, "max stored per org: 25") - assert.Contains(t, out, "retention: 7 days") - assert.Contains(t, out, "max connections: 10") - assert.Contains(t, out, "600s default (300s-86400s)") - // Limits section - assert.Contains(t, out, "Max Concurrent Browsers") - assert.Contains(t, out, "Max Concurrent Invocations") - assert.Contains(t, out, "Default Max Concurrent Invocations Per App") -} - -func TestOrgEntitlementsGet_NullConstraintsShownAsUnlimited(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - ent := populatedEntitlements() - // Null (not omitted) constraints mean unlimited. - ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField(respjson.Null) - ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField(respjson.Null) - ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField(respjson.Null) - return ent, nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - assert.Contains(t, out, "max stored per org: unlimited") - assert.Contains(t, out, "max connections: unlimited") - assert.Contains(t, out, "unlimited") -} - -func TestOrgEntitlementsGet_NullPlanFieldsShownAsDash(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - ent := populatedEntitlements() - ent.Plan.IsTrialing = false - ent.Plan.JSON.Status = respjson.NewField(respjson.Null) - ent.Plan.JSON.TrialEndsAt = respjson.NewField(respjson.Null) - return ent, nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - assert.Contains(t, out, "Billing Status") - assert.Contains(t, out, "Trial Ends At") - assert.NotContains(t, out, "ACTIVE") -} - -func TestOrgEntitlementsGet_RejectsUnknownOutput(t *testing.T) { - c := OrgCmd{entitlements: &FakeOrgEntitlementsService{}} - assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{Output: "yaml"})) -} - -func TestOrgEntitlementsGet_SurfacesAPIError(t *testing.T) { - capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return nil, errors.New("boom") - }, - } - c := OrgCmd{entitlements: fake} - assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) -} diff --git a/go.mod b/go.mod index 502421b9..900e422a 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.92.0 + github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 04679b80..4ec0e491 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= -github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a h1:VJcz+I1d/VTEHkKM4O7+Wf+ejSbXQtytUtDXnZ0+b+4= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 31d2462fcbd0d9f53d417493761424714b81da23 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:39:11 +0000 Subject: [PATCH 5/6] CLI: Update Go SDK to 467fea7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps kernel-go-sdk to v0.92.1-0.20260819203102-467fea72ee93, which adds the proxy_error browser telemetry event (BrowserProxyErrorEvent) to the telemetry event union. No CLI coverage gaps: a full enumeration of all 137 SDK methods in api.md found a corresponding CLI command for each, and the new event type needs no code change because the telemetry commands render category/type generically and accept --types values without a fixed allowlist. Tested: go build ./..., go vet ./..., go test ./... (all pass); browsers create --telemetry all, browsers curl, browsers telemetry events (table, --output json, --categories network --all, --types proxy_error), browsers telemetry stream --categories network --types proxy_error, browsers delete — all against the live API. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 900e422a..59cb47a0 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.92.1-0.20260819184853-796d4245c87a + github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 4ec0e491..662bb5e0 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.92.1-0.20260819184853-796d4245c87a h1:VJcz+I1d/VTEHkKM4O7+Wf+ejSbXQtytUtDXnZ0+b+4= -github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93 h1:p+OWj+8b1iK+Bx/5gSSTP9itGLbN5w2hY/CVT0eBdRM= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= From 16880f448f52ba874488156b50db24745f4685b3 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:06:00 +0000 Subject: [PATCH 6/6] CLI: Update Go SDK to v0.93.0 (0802326) Bumps github.com/kernel/kernel-go-sdk to 08023260493e4584c4d87638849ab4491b34ec49 (v0.93.0). The 0.93.0 release only changed version/changelog metadata relative to the SDK revision the CLI was already pinned to (467fea7); api.md and all generated Go sources are byte-identical, so there are no new methods, params, or fields to expose. Coverage analysis: full enumeration of all 140 SDK methods in api.md against the CLI command tree found no gaps. Every method has a command, and every param struct field is reachable via a flag, a positional arg, or a derived value. Tested: go build ./..., go vet ./..., go test ./... (all pass), plus live API smoke tests for browsers list/create/get/delete, browsers telemetry events, auth connections list, profiles list, telemetry destinations list. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 59cb47a0..0e346401 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.92.1-0.20260819203102-467fea72ee93 + github.com/kernel/kernel-go-sdk v0.93.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 662bb5e0..78374443 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.92.1-0.20260819203102-467fea72ee93 h1:p+OWj+8b1iK+Bx/5gSSTP9itGLbN5w2hY/CVT0eBdRM= -github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.93.0 h1:mPsZKoQlLsgsC0TehWJ/Q5XqWwKu33bKfnuqnfNHtjs= +github.com/kernel/kernel-go-sdk v0.93.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=