From 1425136b07942317522b91a72e025daf4a42c510 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:54:58 +0000 Subject: [PATCH 01/11] CLI: Update SDK to 0.86.0 and close list-endpoint flag coverage gaps Updates github.com/kernel/kernel-go-sdk to v0.86.0 (b5ea2f3dee2cbb2af9001e4558a28a807d31aeb3). A full enumeration of all 127 api.md methods against the CLI command tree found no missing commands. Enumerating every *Params field against every command's flags surfaced a set of filter/sort/pagination params the CLI never exposed; this closes those. New flags: - kernel api-keys list: --name, --query, --status, --include-deleted (deprecated alias for --status all), --sort-by, --sort-direction. Adds a Deleted At column when the filter can return deleted keys, matching `browsers list`. - kernel projects list: --page, --per-page, --name, --query. This command previously had no flags at all and always returned the API's default page. Uses the page-based pagination UX with a footer, as in `app list`. - kernel browser-pools list: --name, --query - kernel extensions list: --name, --query - kernel proxies list: --name, --query - kernel profiles list: --name - kernel auth connections list: --query - kernel credentials list: --query - kernel credential-providers list: --query - kernel deploy history: --query - kernel invoke history: --query - kernel browser-pools acquire: --start-url (BrowserPoolAcquireParams.StartURL) - kernel browsers telemetry events: --order asc|desc (BrowserTelemetryEventsParams.Order), rejecting `--order desc --since` locally since the endpoint 400s on that combination. SDK 0.86.0 changes: - ManagedAuthField.ReplaceExisting is now surfaced by `auth connections get` (as a `replace-existing` field marker) and `auth connections follow`, so it is visible before running `submit`. - ProjectLimits.MaxPooledSessions doc-only deprecation; no CLI change needed (the flag already exists and the API rejects it). Also forwards `browsers create --pool-id/--pool-name --start-url` through to the acquire call instead of warning and dropping it. start_url is a per-lease override on BrowserPoolAcquireParams, so it now sits alongside name/tag/telemetry/timeout in poolLeaseAllowedFlags. Tested against the live API: - api-keys list --status all --sort-by name --sort-direction asc (verified sort order and Deleted At population), --query, --include-deleted, and --sort-by/--sort-direction/--status validation errors - projects list --per-page 3 / --page 2 (footer + Next hint), --name, --query - --name and --query on proxies/extensions/browser-pools/profiles list - --query on credentials/credential-providers/auth connections list, deploy history, invoke history - browsers telemetry events --order desc (returned seq 22->18 vs asc 1->5) and the --order desc --since / --order bogus validation errors - browser-pools acquire --start-url and browsers create --pool-id --start-url (API echoed start_url on both) - go build, go vet, and go test ./... all pass Co-Authored-By: Claude Opus 5 --- cmd/api_keys.go | 96 +++++++++++++++++++++++++++++++++---- cmd/auth_connections.go | 16 ++++++- cmd/browser_pools.go | 30 ++++++++++-- cmd/browser_pools_test.go | 14 ++++-- cmd/browsers.go | 7 ++- cmd/browsers_telemetry.go | 14 ++++++ cmd/credential_providers.go | 7 +++ cmd/credentials.go | 7 +++ cmd/deploy.go | 8 ++++ cmd/extensions.go | 14 +++++- cmd/invoke.go | 5 ++ cmd/profiles.go | 10 ++++ cmd/projects.go | 74 ++++++++++++++++++++++++++-- cmd/proxies/list.go | 10 +++- cmd/proxies/proxies.go | 2 + cmd/proxies/types.go | 2 + go.mod | 2 +- go.sum | 4 +- 18 files changed, 290 insertions(+), 32 deletions(-) diff --git a/cmd/api_keys.go b/cmd/api_keys.go index c83f1401..428b9ccd 100644 --- a/cmd/api_keys.go +++ b/cmd/api_keys.go @@ -35,9 +35,15 @@ type APIKeysCreateInput struct { } type APIKeysListInput struct { - Limit int - Offset int - Output string + Limit int + Offset int + Name string + Query string + Status string + IncludeDeleted bool + SortBy string + SortDirection string + Output string } type APIKeysGetInput struct { @@ -116,6 +122,50 @@ func (c APIKeysCmd) List(ctx context.Context, in APIKeysListInput) error { if in.Offset > 0 { params.Offset = kernel.Int(int64(in.Offset)) } + if in.Name != "" { + params.Name = kernel.String(in.Name) + } + if in.Query != "" { + params.Query = kernel.String(in.Query) + } + // Prefer the newer --status filter; fall back to the deprecated + // --include-deleted so existing scripts keep working. + if in.Status != "" { + switch in.Status { + case "active": + params.Status = kernel.APIKeyListParamsStatusActive + case "deleted": + params.Status = kernel.APIKeyListParamsStatusDeleted + case "all": + params.Status = kernel.APIKeyListParamsStatusAll + 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.SortBy != "" { + switch in.SortBy { + case "created_at": + params.SortBy = kernel.APIKeyListParamsSortByCreatedAt + case "name": + params.SortBy = kernel.APIKeyListParamsSortByName + case "expires_at": + params.SortBy = kernel.APIKeyListParamsSortByExpiresAt + default: + return fmt.Errorf("invalid --sort-by value: %s (must be 'created_at', 'name', or 'expires_at')", in.SortBy) + } + } + if in.SortDirection != "" { + switch in.SortDirection { + case "asc": + params.SortDirection = kernel.APIKeyListParamsSortDirectionAsc + case "desc": + params.SortDirection = kernel.APIKeyListParamsSortDirectionDesc + default: + return fmt.Errorf("invalid --sort-direction value: %s (must be 'asc' or 'desc')", in.SortDirection) + } + } page, err := c.apiKeys.List(ctx, params) if err != nil { @@ -136,9 +186,15 @@ func (c APIKeysCmd) List(ctx context.Context, in APIKeysListInput) error { return nil } - table := pterm.TableData{{"ID", "Name", "Scope", "Project", "Masked Key", "Expires At", "Created At"}} + // Only surface Deleted At when the filter can actually return deleted keys. + showDeletedAt := in.IncludeDeleted || in.Status == "deleted" || in.Status == "all" + header := []string{"ID", "Name", "Scope", "Project", "Masked Key", "Expires At", "Created At"} + if showDeletedAt { + header = append(header, "Deleted At") + } + table := pterm.TableData{header} for _, key := range keys { - table = append(table, []string{ + row := []string{ key.ID, key.Name, formatAPIKeyScope(key), @@ -146,7 +202,11 @@ func (c APIKeysCmd) List(ctx context.Context, in APIKeysListInput) error { key.MaskedKey, formatAPIKeyExpiresAt(key), util.FormatLocal(key.CreatedAt), - }) + } + if showDeletedAt { + row = append(row, util.FormatLocal(key.DeletedAt)) + } + table = append(table, row) } PrintTableNoPad(table, true) return nil @@ -362,11 +422,23 @@ func runAPIKeysList(cmd *cobra.Command, args []string) error { c := getAPIKeysHandler(cmd) limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") + name, _ := cmd.Flags().GetString("name") + query, _ := cmd.Flags().GetString("query") + status, _ := cmd.Flags().GetString("status") + includeDeleted, _ := cmd.Flags().GetBool("include-deleted") + sortBy, _ := cmd.Flags().GetString("sort-by") + sortDirection, _ := cmd.Flags().GetString("sort-direction") output, _ := cmd.Flags().GetString("output") return c.List(cmd.Context(), APIKeysListInput{ - Limit: limit, - Offset: offset, - Output: output, + Limit: limit, + Offset: offset, + Name: name, + Query: query, + Status: status, + IncludeDeleted: includeDeleted, + SortBy: sortBy, + SortDirection: sortDirection, + Output: output, }) } @@ -468,6 +540,12 @@ func init() { addJSONOutputFlag(apiKeysListCmd) apiKeysListCmd.Flags().Int("limit", 0, "Maximum number of results to return") apiKeysListCmd.Flags().Int("offset", 0, "Number of results to skip") + apiKeysListCmd.Flags().String("name", "", "Exact-match filter on API key name (names are not unique, so several keys may match)") + apiKeysListCmd.Flags().String("query", "", "Search API keys by name, creator, or project (identifiers and masked keys match by exact value or prefix)") + apiKeysListCmd.Flags().String("status", "", "Filter by status: 'active' (default), 'deleted', or 'all'") + apiKeysListCmd.Flags().Bool("include-deleted", false, "Deprecated: Use --status all instead. Include soft-deleted API keys in the results") + apiKeysListCmd.Flags().String("sort-by", "", "Sort by: created_at, name, or expires_at") + apiKeysListCmd.Flags().String("sort-direction", "", "Sort direction: asc or desc") addJSONOutputFlag(apiKeysGetCmd) apiKeysGetCmd.Flags().Bool("include-deleted", false, "Include soft-deleted API keys in the lookup") diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 1f8b5f13..06ef62ec 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -93,6 +93,7 @@ type AuthConnectionUpdateInput struct { type AuthConnectionListInput struct { Domain string ProfileName string + Query string Limit int Offset int Output string @@ -429,6 +430,9 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e if f.Required { meta = append(meta, "required") } + if f.ReplaceExisting { + meta = append(meta, "replace-existing") + } entry := f.ID if f.Label != "" { entry = fmt.Sprintf("%s (%s)", f.ID, f.Label) @@ -557,6 +561,9 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput) if in.ProfileName != "" { params.ProfileName = kernel.Opt(in.ProfileName) } + if in.Query != "" { + params.Query = kernel.Opt(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Opt(int64(in.Limit)) } @@ -949,7 +956,11 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn if len(state.Fields) > 0 { fieldIDs := make([]string, 0, len(state.Fields)) for _, f := range state.Fields { - fieldIDs = append(fieldIDs, f.ID) + id := f.ID + if f.ReplaceExisting { + id += " (replace-existing)" + } + fieldIDs = append(fieldIDs, id) } pterm.Info.Printf(" Fields: %s\n", strings.Join(fieldIDs, ", ")) } @@ -1132,6 +1143,7 @@ func init() { addJSONOutputFlag(authConnectionsListCmd) authConnectionsListCmd.Flags().String("domain", "", "Filter by domain") authConnectionsListCmd.Flags().String("profile-name", "", "Filter by profile name") + authConnectionsListCmd.Flags().String("query", "", "Search auth connections by ID, domain, or profile name") authConnectionsListCmd.Flags().Int("limit", 0, "Maximum number of results to return") authConnectionsListCmd.Flags().Int("offset", 0, "Number of results to skip") @@ -1306,6 +1318,7 @@ func runAuthConnectionsList(cmd *cobra.Command, args []string) error { output, _ := cmd.Flags().GetString("output") domain, _ := cmd.Flags().GetString("domain") profileName, _ := cmd.Flags().GetString("profile-name") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") @@ -1314,6 +1327,7 @@ func runAuthConnectionsList(cmd *cobra.Command, args []string) error { return c.List(cmd.Context(), AuthConnectionListInput{ Domain: domain, ProfileName: profileName, + Query: query, Limit: limit, Offset: offset, Output: output, diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index ca753bda..f987cfa1 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -31,6 +31,8 @@ type BrowserPoolsCmd struct { } type BrowserPoolsListInput struct { + Name string + Query string Limit int Offset int Output string @@ -42,6 +44,12 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err } params := kernel.BrowserPoolListParams{} + if in.Name != "" { + params.Name = kernel.String(in.Name) + } + if in.Query != "" { + params.Query = kernel.String(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Int(int64(in.Limit)) } @@ -448,6 +456,7 @@ type BrowserPoolsAcquireInput struct { IDOrName string TimeoutSeconds int64 Name string + StartURL string Tags map[string]string Telemetry string Output string @@ -455,9 +464,10 @@ type BrowserPoolsAcquireInput struct { // buildAcquireParams builds the SDK params for acquiring a browser from a pool. // Shared by `browser-pools acquire` and the `browsers create --pool-id/--pool-name` -// path so the per-lease name/tags/telemetry forwarding cannot silently diverge -// between them. The telemetry override merges onto the pool's config for this lease. -func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64, telemetry string) (kernel.BrowserPoolAcquireParams, error) { +// path so the per-lease name/tags/start-url/telemetry forwarding cannot silently +// diverge between them. The telemetry override merges onto the pool's config for +// this lease. +func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64, telemetry, startURL string) (kernel.BrowserPoolAcquireParams, error) { params := kernel.BrowserPoolAcquireParams{} if timeoutSeconds > 0 { params.AcquireTimeoutSeconds = kernel.Int(timeoutSeconds) @@ -465,6 +475,9 @@ func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int6 if name != "" { params.Name = kernel.Opt(name) } + if startURL != "" { + params.StartURL = kernel.Opt(startURL) + } if len(tags) > 0 { params.Tags = kernel.Tags(tags) } @@ -483,7 +496,7 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu return err } - params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry) + params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry, in.StartURL) if err != nil { return err } @@ -627,6 +640,8 @@ var browserPoolsFlushCmd = &cobra.Command{ func init() { addJSONOutputFlag(browserPoolsListCmd) + browserPoolsListCmd.Flags().String("name", "", "Exact-match filter on browser pool name") + browserPoolsListCmd.Flags().String("query", "", "Search browser pools by name (IDs match by exact value)") browserPoolsListCmd.Flags().Int("limit", 0, "Maximum number of pools to return") browserPoolsListCmd.Flags().Int("offset", 0, "Number of pools to skip (for pagination)") @@ -679,6 +694,7 @@ func init() { browserPoolsAcquireCmd.Flags().Int64("timeout", 0, "Acquire timeout in seconds") browserPoolsAcquireCmd.Flags().String("name", "", "Optional name for the acquired session (applies to this lease; cleared on release)") + browserPoolsAcquireCmd.Flags().String("start-url", "", "URL to navigate the acquired browser to, overriding the pool's start URL for this acquire only (best-effort)") browserPoolsAcquireCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the acquired session (repeatable; applies to this lease)") browserPoolsAcquireCmd.Flags().String("telemetry", "", "Telemetry override for this lease only, merged onto the pool's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") addJSONOutputFlag(browserPoolsAcquireCmd) @@ -700,10 +716,12 @@ func init() { func runBrowserPoolsList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) out, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") c := BrowserPoolsCmd{client: &client.BrowserPools} - return c.List(cmd.Context(), BrowserPoolsListInput{Limit: limit, Offset: offset, Output: out}) + return c.List(cmd.Context(), BrowserPoolsListInput{Name: name, Query: query, Limit: limit, Offset: offset, Output: out}) } func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { @@ -829,6 +847,7 @@ func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) timeout, _ := cmd.Flags().GetInt64("timeout") name, _ := cmd.Flags().GetString("name") + startURL, _ := cmd.Flags().GetString("start-url") tags, _ := tagsFromFlag(cmd, "tag") telemetry, _ := cmd.Flags().GetString("telemetry") output, _ := cmd.Flags().GetString("output") @@ -837,6 +856,7 @@ func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { IDOrName: args[0], TimeoutSeconds: timeout, Name: name, + StartURL: startURL, Tags: tags, Telemetry: telemetry, Output: output, diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index 5a1ab9e1..ea8ff178 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -120,28 +120,32 @@ func TestBrowserPoolsList_ForwardsLimitOffset(t *testing.T) { assert.Equal(t, int64(8), captured.Offset.Value) } -// TestBuildAcquireParams covers the shared name/tags/timeout/telemetry forwarding -// used by both `browser-pools acquire` and the `browsers create --pool-id` lease path. +// TestBuildAcquireParams covers the shared name/tags/timeout/telemetry/start-url +// forwarding used by both `browser-pools acquire` and the `browsers create +// --pool-id` lease path. func TestBuildAcquireParams(t *testing.T) { - p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network") + p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network", "https://example.com") assert.NoError(t, err) assert.True(t, p.Name.Valid()) assert.Equal(t, "lease", p.Name.Value) assert.Equal(t, "prod", p.Tags["env"]) assert.True(t, p.AcquireTimeoutSeconds.Valid()) assert.Equal(t, int64(30), p.AcquireTimeoutSeconds.Value) + assert.True(t, p.StartURL.Valid()) + assert.Equal(t, "https://example.com", p.StartURL.Value) assert.True(t, p.Telemetry.Browser.Console.Enabled.Value) assert.True(t, p.Telemetry.Browser.Network.Enabled.Value) // Unset inputs produce an empty params struct (nothing forwarded). - empty, err := buildAcquireParams("", nil, 0, "") + empty, err := buildAcquireParams("", nil, 0, "", "") assert.NoError(t, err) assert.False(t, empty.Name.Valid()) assert.Len(t, empty.Tags, 0) assert.False(t, empty.AcquireTimeoutSeconds.Valid()) + assert.False(t, empty.StartURL.Valid()) // An invalid category surfaces an error rather than a partial param. - _, err = buildAcquireParams("", nil, 0, "bogus") + _, err = buildAcquireParams("", nil, 0, "bogus", "") assert.Error(t, err) } diff --git a/cmd/browsers.go b/cmd/browsers.go index 05611843..4ed18f11 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -2828,6 +2828,7 @@ followed automatically by Chromium.`, telemetryEvents := &cobra.Command{Use: "events ", Short: "Read historical telemetry events (paged)", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryEvents} telemetryEvents.Flags().Int64("limit", 0, "Maximum number of events per page (1-100, default 20)") telemetryEvents.Flags().Int64("offset", 0, "Pagination cursor: pass the X-Next-Offset from a previous response") + telemetryEvents.Flags().String("order", "", "Read direction: asc (default) reads oldest first, desc reads newest first (cannot be combined with --since)") telemetryEvents.Flags().String("since", "", "Window start: RFC-3339 timestamp or a duration like 5m (default 5m). Ignored when --offset is set") telemetryEvents.Flags().String("until", "", "Window end (exclusive): RFC-3339 timestamp or a duration like 5m") telemetryEvents.Flags().StringSlice("categories", []string{}, "Filter by event category (console,network,page,interaction,control,connection,system,screenshot,captcha,monitor)") @@ -2873,6 +2874,7 @@ func poolLeaseAllowedFlags() map[string]bool { "pool-name": true, "timeout": true, "name": true, + "start-url": true, "tag": true, "telemetry": true, "output": true, @@ -2918,7 +2920,8 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { if poolID != "" || poolName != "" { // When using a pool, configuration comes from the pool itself, but - // name, tags, and telemetry apply per-lease to the acquired session. + // name, start URL, tags, and telemetry apply per-lease to the acquired + // session — they mirror the fields BrowserPoolAcquireParams accepts. allowedFlags := poolLeaseAllowedFlags() // Check if any browser configuration flags were set (which would conflict). @@ -2968,7 +2971,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("timeout") && timeout > 0 { acquireTimeout = int64(timeout) } - acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry) + acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry, startURL) if err != nil { return err } diff --git a/cmd/browsers_telemetry.go b/cmd/browsers_telemetry.go index 3eccd748..67367c8a 100644 --- a/cmd/browsers_telemetry.go +++ b/cmd/browsers_telemetry.go @@ -44,6 +44,7 @@ type BrowsersTelemetryEventsInput struct { Identifier string Limit int64 Offset int64 + Order string Since string Until string Categories []string @@ -307,6 +308,14 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv return fmt.Errorf("invalid --categories value %q: must be one of %s", c, strings.Join(streamFilterCategories, ", ")) } } + if in.Order != "" && in.Order != "asc" && in.Order != "desc" { + return fmt.Errorf("invalid --order value %q: must be asc or desc", in.Order) + } + // The endpoint rejects desc combined with a window start, since desc pages + // backwards from --until (or the newest archived event) instead. + if in.Order == "desc" && in.Since != "" { + return fmt.Errorf("--order desc cannot be combined with --since; use --until to bound the window instead") + } // Resolve a name to a session ID. The events archive outlives the session, so // a 404 (ended or unknown session) is not fatal: fall back to the identifier @@ -328,6 +337,9 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv if in.Limit > 0 { params.Limit = kernel.Opt(in.Limit) } + if in.Order != "" { + params.Order = kernel.Opt(in.Order) + } if in.Offset > 0 && !fullScan { params.Offset = kernel.Opt(in.Offset) } else if in.Since != "" { @@ -428,6 +440,7 @@ func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error { out, _ := cmd.Flags().GetString("output") limit, _ := cmd.Flags().GetInt64("limit") offset, _ := cmd.Flags().GetInt64("offset") + order, _ := cmd.Flags().GetString("order") since, _ := cmd.Flags().GetString("since") until, _ := cmd.Flags().GetString("until") categories, _ := cmd.Flags().GetStringSlice("categories") @@ -438,6 +451,7 @@ func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error { Identifier: args[0], Limit: limit, Offset: offset, + Order: order, Since: since, Until: until, Categories: categories, diff --git a/cmd/credential_providers.go b/cmd/credential_providers.go index 2f472348..5b8041c7 100644 --- a/cmd/credential_providers.go +++ b/cmd/credential_providers.go @@ -32,6 +32,7 @@ type CredentialProvidersCmd struct { } type CredentialProvidersListInput struct { + Query string Limit int Offset int Output string @@ -81,6 +82,9 @@ func (c CredentialProvidersCmd) List(ctx context.Context, in CredentialProviders } params := kernel.CredentialProviderListParams{} + if in.Query != "" { + params.Query = kernel.String(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Int(int64(in.Limit)) } @@ -446,6 +450,7 @@ func init() { // List flags addJSONOutputFlag(credentialProvidersListCmd) + credentialProvidersListCmd.Flags().String("query", "", "Search credential providers by name (IDs match by exact value)") credentialProvidersListCmd.Flags().Int("limit", 0, "Maximum number of credential providers to return") credentialProvidersListCmd.Flags().Int("offset", 0, "Number of credential providers to skip (for pagination)") @@ -483,12 +488,14 @@ func init() { func runCredentialProvidersList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) output, _ := cmd.Flags().GetString("output") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") svc := client.CredentialProviders c := CredentialProvidersCmd{providers: &svc} return c.List(cmd.Context(), CredentialProvidersListInput{ + Query: query, Limit: limit, Offset: offset, Output: output, diff --git a/cmd/credentials.go b/cmd/credentials.go index 56fcbdc9..bf7b764c 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -32,6 +32,7 @@ type CredentialsCmd struct { type CredentialsListInput struct { Domain string + Query string Limit int Offset int Output string @@ -80,6 +81,9 @@ func (c CredentialsCmd) List(ctx context.Context, in CredentialsListInput) error if in.Domain != "" { params.Domain = kernel.Opt(in.Domain) } + if in.Query != "" { + params.Query = kernel.Opt(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Opt(int64(in.Limit)) } @@ -410,6 +414,7 @@ func init() { // List flags addJSONOutputFlag(credentialsListCmd) credentialsListCmd.Flags().String("domain", "", "Filter by domain") + credentialsListCmd.Flags().String("query", "", "Search credentials by name or domain (IDs match by exact value)") credentialsListCmd.Flags().Int("limit", 0, "Maximum number of results to return") credentialsListCmd.Flags().Int("offset", 0, "Number of results to skip") @@ -445,6 +450,7 @@ func runCredentialsList(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) output, _ := cmd.Flags().GetString("output") domain, _ := cmd.Flags().GetString("domain") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") @@ -452,6 +458,7 @@ func runCredentialsList(cmd *cobra.Command, args []string) error { c := CredentialsCmd{credentials: &svc} return c.List(cmd.Context(), CredentialsListInput{ Domain: domain, + Query: query, Limit: limit, Offset: offset, Output: output, diff --git a/cmd/deploy.go b/cmd/deploy.go index a5000d03..09a84983 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -93,6 +93,7 @@ func init() { deployHistoryCmd.Flags().Int("per-page", 20, "Items per page (alias of --limit)") deployHistoryCmd.Flags().Int("page", 1, "Page number (1-based)") deployHistoryCmd.Flags().String("app-version", "", "Filter by application version (requires app_name)") + deployHistoryCmd.Flags().String("query", "", "Search deployments by ID or app name") addJSONOutputFlag(deployHistoryCmd) deployCmd.AddCommand(deployHistoryCmd) @@ -492,6 +493,7 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { perPage, _ := cmd.Flags().GetInt("per-page") page, _ := cmd.Flags().GetInt("page") appVersionFilter, _ := cmd.Flags().GetString("app-version") + queryFilter, _ := cmd.Flags().GetString("query") output, _ := cmd.Flags().GetString("output") if err := validateJSONOutput(output); err != nil { @@ -530,6 +532,9 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { if appVersionFilter != "" { params.AppVersion = kernel.Opt(appVersionFilter) } + if queryFilter != "" { + params.Query = kernel.Opt(queryFilter) + } // Request one extra item to detect hasMore params.Limit = kernel.Opt(int64(perPage + 1)) params.Offset = kernel.Opt(int64((page - 1) * perPage)) @@ -589,6 +594,9 @@ func runDeployHistory(cmd *cobra.Command, args []string) error { if appVersionFilter != "" { nextCmd += fmt.Sprintf(" --app-version %s", quoteIfNeeded(appVersionFilter)) } + if queryFilter != "" { + nextCmd += fmt.Sprintf(" --query %s", quoteIfNeeded(queryFilter)) + } pterm.Printf("Next: %s\n", nextCmd) } // Concise notes when user-specified per-page/limit/page are outside API-allowed range diff --git a/cmd/extensions.go b/cmd/extensions.go index 80f15d74..b46a7f83 100644 --- a/cmd/extensions.go +++ b/cmd/extensions.go @@ -54,6 +54,8 @@ type ExtensionsService interface { } type ExtensionsListInput struct { + Name string + Query string Limit int Offset int Output string @@ -101,6 +103,12 @@ func (e ExtensionsCmd) List(ctx context.Context, in ExtensionsListInput) error { pterm.Info.Println("Fetching extensions...") } params := kernel.ExtensionListParams{} + if in.Name != "" { + params.Name = kernel.String(in.Name) + } + if in.Query != "" { + params.Query = kernel.String(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Int(int64(in.Limit)) } @@ -461,11 +469,13 @@ var extensionsListCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { client := getKernelClient(cmd) output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") svc := client.Extensions e := ExtensionsCmd{extensions: &svc} - return e.List(cmd.Context(), ExtensionsListInput{Limit: limit, Offset: offset, Output: output}) + return e.List(cmd.Context(), ExtensionsListInput{Name: name, Query: query, Limit: limit, Offset: offset, Output: output}) }, } @@ -595,6 +605,8 @@ func init() { addJSONOutputFlag(extensionsListCmd) addJSONOutputFlag(extensionsGetCmd) + extensionsListCmd.Flags().String("name", "", "Exact-match filter on extension name") + extensionsListCmd.Flags().String("query", "", "Search extensions by name (IDs match by exact value)") extensionsListCmd.Flags().Int("limit", 0, "Maximum number of extensions to return") extensionsListCmd.Flags().Int("offset", 0, "Number of extensions to skip (for pagination)") extensionsDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") diff --git a/cmd/invoke.go b/cmd/invoke.go index cca38044..196aef24 100644 --- a/cmd/invoke.go +++ b/cmd/invoke.go @@ -80,6 +80,7 @@ func init() { invocationHistoryCmd.Flags().String("action", "", "Filter by action name") invocationHistoryCmd.Flags().StringP("app", "a", "", "Filter by app name") invocationHistoryCmd.Flags().String("deployment-id", "", "Filter by deployment ID") + invocationHistoryCmd.Flags().String("query", "", "Search invocations by ID, app name, or action name") invocationHistoryCmd.Flags().Int("offset", 0, "Number of results to skip") invocationHistoryCmd.Flags().String("since", "", "Show invocations that started since the given time") invocationHistoryCmd.Flags().String("status", "", "Filter by invocation status: queued, running, succeeded, failed") @@ -421,6 +422,7 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { actionFilter, _ := cmd.Flags().GetString("action") appFilter, _ := cmd.Flags().GetString("app") deploymentID, _ := cmd.Flags().GetString("deployment-id") + query, _ := cmd.Flags().GetString("query") offset, _ := cmd.Flags().GetInt("offset") since, _ := cmd.Flags().GetString("since") statusFilter, _ := cmd.Flags().GetString("status") @@ -446,6 +448,9 @@ func runInvocationHistory(cmd *cobra.Command, args []string) error { if deploymentID != "" { params.DeploymentID = kernel.Opt(deploymentID) } + if query != "" { + params.Query = kernel.Opt(query) + } if offset > 0 { params.Offset = kernel.Opt(int64(offset)) } diff --git a/cmd/profiles.go b/cmd/profiles.go index 1820ad14..cbe89923 100644 --- a/cmd/profiles.go +++ b/cmd/profiles.go @@ -41,6 +41,7 @@ type ProfilesListInput struct { Output string Page int PerPage int + Name string Query string } @@ -91,6 +92,9 @@ func (p ProfilesCmd) List(ctx context.Context, in ProfilesListInput) error { } params := kernel.ProfileListParams{} + if in.Name != "" { + params.Name = kernel.Opt(in.Name) + } if in.Query != "" { params.Query = kernel.Opt(in.Query) } @@ -145,6 +149,9 @@ func (p ProfilesCmd) List(ctx context.Context, in ProfilesListInput) error { if hasMore { nextPage := page + 1 nextCmd := fmt.Sprintf("kernel profile list --page %d --per-page %d", nextPage, perPage) + if in.Name != "" { + nextCmd += fmt.Sprintf(" --name \"%s\"", in.Name) + } if in.Query != "" { nextCmd += fmt.Sprintf(" --query \"%s\"", in.Query) } @@ -451,6 +458,7 @@ func init() { addJSONOutputFlag(profilesListCmd) profilesListCmd.Flags().Int("per-page", 20, "Items per page (default 20)") profilesListCmd.Flags().Int("page", 1, "Page number (1-based)") + profilesListCmd.Flags().String("name", "", "Exact-match filter on profile name") profilesListCmd.Flags().String("query", "", "Search profiles by name or ID") addJSONOutputFlag(profilesGetCmd) addJSONOutputFlag(profilesCreateCmd) @@ -469,6 +477,7 @@ func runProfilesList(cmd *cobra.Command, args []string) error { output, _ := cmd.Flags().GetString("output") perPage, _ := cmd.Flags().GetInt("per-page") page, _ := cmd.Flags().GetInt("page") + name, _ := cmd.Flags().GetString("name") query, _ := cmd.Flags().GetString("query") svc := client.Profiles @@ -477,6 +486,7 @@ func runProfilesList(cmd *cobra.Command, args []string) error { Output: output, Page: page, PerPage: perPage, + Name: name, Query: query, }) } diff --git a/cmd/projects.go b/cmd/projects.go index 28cedeb6..8895d125 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -12,6 +12,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" ) @@ -37,7 +38,12 @@ type ProjectsCmd struct { limits ProjectLimitsService } -type ProjectsListInput struct{} +type ProjectsListInput struct { + Page int + PerPage int + Name string + Query string +} type ProjectsCreateInput struct { Name string @@ -87,21 +93,65 @@ func resolveProjectArg(ctx context.Context, projects ProjectListService, val str } func (c ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error { - projects, err := c.projects.List(ctx, kernel.ProjectListParams{}) + page := in.Page + perPage := in.PerPage + if page <= 0 { + page = 1 + } + if perPage <= 0 { + perPage = 20 + } + + params := kernel.ProjectListParams{} + if in.Name != "" { + params.Name = kernel.Opt(in.Name) + } + if in.Query != "" { + params.Query = kernel.Opt(in.Query) + } + // Request one extra item to detect whether another page exists without a + // second call; the pagination response headers are not exposed by the SDK. + params.Limit = kernel.Opt(int64(perPage + 1)) + params.Offset = kernel.Opt(int64((page - 1) * perPage)) + + projects, err := c.projects.List(ctx, params) if err != nil { return util.CleanedUpSdkError{Err: err} } - if projects == nil || len(projects.Items) == 0 { + var items []kernel.Project + if projects != nil { + items = projects.Items + } + + hasMore := len(items) > perPage + if hasMore { + items = items[:perPage] + } + itemsThisPage := len(items) + + if len(items) == 0 { pterm.Info.Println("No projects found") return nil } table := pterm.TableData{{"ID", "Name", "Status", "Created At"}} - for _, p := range projects.Items { + for _, p := range items { table = append(table, []string{p.ID, p.Name, string(p.Status), util.FormatLocal(p.CreatedAt)}) } PrintTableNoPad(table, true) + + pterm.Printf("\nPage: %d Per-page: %d Items this page: %d Has more: %s\n", page, perPage, itemsThisPage, lo.Ternary(hasMore, "yes", "no")) + if hasMore { + nextCmd := fmt.Sprintf("kernel projects list --page %d --per-page %d", page+1, perPage) + if in.Name != "" { + nextCmd += fmt.Sprintf(" --name %q", in.Name) + } + if in.Query != "" { + nextCmd += fmt.Sprintf(" --query %q", in.Query) + } + pterm.Printf("Next: %s\n", nextCmd) + } return nil } @@ -323,7 +373,16 @@ func getProjectsHandler(cmd *cobra.Command) ProjectsCmd { func runProjectsList(cmd *cobra.Command, args []string) error { c := getProjectsHandler(cmd) - return c.List(cmd.Context(), ProjectsListInput{}) + page, _ := cmd.Flags().GetInt("page") + perPage, _ := cmd.Flags().GetInt("per-page") + name, _ := cmd.Flags().GetString("name") + query, _ := cmd.Flags().GetString("query") + return c.List(cmd.Context(), ProjectsListInput{ + Page: page, + PerPage: perPage, + Name: name, + Query: query, + }) } func runProjectsCreate(cmd *cobra.Command, args []string) error { @@ -477,6 +536,11 @@ var projectsSetLimitsCompatCmd = &cobra.Command{ } func init() { + projectsListCmd.Flags().Int("page", 1, "Page number (1-based)") + projectsListCmd.Flags().Int("per-page", 20, "Items per page (default 20)") + projectsListCmd.Flags().String("name", "", "Exact-match filter on project name") + projectsListCmd.Flags().String("query", "", "Search projects by name") + projectsUpdateCmd.Flags().String("name", "", "New project name (1-255 characters)") projectsUpdateCmd.Flags().String("status", "", "New project status: active or archived") addJSONOutputFlag(projectsUpdateCmd) diff --git a/cmd/proxies/list.go b/cmd/proxies/list.go index 83a8f740..1efb5f34 100644 --- a/cmd/proxies/list.go +++ b/cmd/proxies/list.go @@ -22,6 +22,12 @@ func (p ProxyCmd) List(ctx context.Context, in ProxyListInput) error { } params := kernel.ProxyListParams{} + if in.Name != "" { + params.Name = kernel.String(in.Name) + } + if in.Query != "" { + params.Query = kernel.String(in.Query) + } if in.Limit > 0 { params.Limit = kernel.Int(int64(in.Limit)) } @@ -154,9 +160,11 @@ func formatProxyConfig(proxy *kernel.ProxyListResponse) string { func runProxiesList(cmd *cobra.Command, args []string) error { client := util.GetKernelClient(cmd) output, _ := cmd.Flags().GetString("output") + name, _ := cmd.Flags().GetString("name") + query, _ := cmd.Flags().GetString("query") limit, _ := cmd.Flags().GetInt("limit") offset, _ := cmd.Flags().GetInt("offset") svc := client.Proxies p := ProxyCmd{proxies: &svc} - return p.List(cmd.Context(), ProxyListInput{Limit: limit, Offset: offset, Output: output}) + return p.List(cmd.Context(), ProxyListInput{Name: name, Query: query, Limit: limit, Offset: offset, Output: output}) } diff --git a/cmd/proxies/proxies.go b/cmd/proxies/proxies.go index 76420010..a3229c92 100644 --- a/cmd/proxies/proxies.go +++ b/cmd/proxies/proxies.go @@ -93,6 +93,8 @@ func init() { // Add output flags addJSONOutputFlag(proxiesListCmd) + proxiesListCmd.Flags().String("name", "", "Exact-match filter on proxy name") + proxiesListCmd.Flags().String("query", "", "Search proxies by name, host, or IP address (IDs match by exact value)") proxiesListCmd.Flags().Int("limit", 0, "Maximum number of proxies to return") proxiesListCmd.Flags().Int("offset", 0, "Number of proxies to skip (for pagination)") addJSONOutputFlag(proxiesGetCmd) diff --git a/cmd/proxies/types.go b/cmd/proxies/types.go index 7ca96f9f..ee0b6247 100644 --- a/cmd/proxies/types.go +++ b/cmd/proxies/types.go @@ -27,6 +27,8 @@ type ProxyCmd struct { // Input types for proxy operations type ProxyListInput struct { + Name string + Query string Limit int Offset int Output string diff --git a/go.mod b/go.mod index 54ec0a77..b16ca81b 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.85.0 + github.com/kernel/kernel-go-sdk v0.86.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 a872a5b4..cf5cc8a2 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.85.0 h1:rACZOx5dcjO4rasFmMoh2GS14w4yRBbYXxb92eGvD2E= -github.com/kernel/kernel-go-sdk v0.85.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.0 h1:HixQQhL2llD+gLnmmexEh+iVFCs5Greryi1CK8Eim9E= +github.com/kernel/kernel-go-sdk v0.86.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 b5df240bf273683089abf9f1f398f2707499e9a9 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:11:11 +0000 Subject: [PATCH 02/11] CLI: Update Go SDK to b94cd58 Bump github.com/kernel/kernel-go-sdk to v0.86.1-0.20260804210355-b94cd585bac0 (commit b94cd58). The new SDK commit is byte-identical to v0.86.0 in module content -- the two upstream commits only touch CI promotion workflows, which are not part of the Go module. A full enumeration of all 127 SDK methods in api.md against the CLI command tree, and of all 95 *Params structs (345 fields, including nested unions) against every CLI flag, found no coverage gaps. Also fix the pagination footer hint in `kernel profiles list`, which suggested `kernel profile list` (singular, not a real command). Tested: go build ./..., go vet ./..., go test ./... (all pass); smoke-tested `kernel browsers list`, `kernel profiles list`, and `kernel profiles list --per-page 3` against the live API. Co-Authored-By: Claude Opus 5 --- cmd/profiles.go | 2 +- cmd/profiles_test.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/profiles.go b/cmd/profiles.go index cbe89923..1a9264fe 100644 --- a/cmd/profiles.go +++ b/cmd/profiles.go @@ -148,7 +148,7 @@ func (p ProfilesCmd) List(ctx context.Context, in ProfilesListInput) error { pterm.Printf("\nPage: %d Per-page: %d Items this page: %d Has more: %s\n", page, perPage, itemsThisPage, lo.Ternary(hasMore, "yes", "no")) if hasMore { nextPage := page + 1 - nextCmd := fmt.Sprintf("kernel profile list --page %d --per-page %d", nextPage, perPage) + nextCmd := fmt.Sprintf("kernel profiles list --page %d --per-page %d", nextPage, perPage) if in.Name != "" { nextCmd += fmt.Sprintf(" --name \"%s\"", in.Name) } diff --git a/cmd/profiles_test.go b/cmd/profiles_test.go index 35467493..d8af4798 100644 --- a/cmd/profiles_test.go +++ b/cmd/profiles_test.go @@ -108,7 +108,7 @@ func TestProfilesList_HasMore(t *testing.T) { _ = p.List(context.Background(), ProfilesListInput{Page: 1, PerPage: perPage}) out := buf.String() assert.Contains(t, out, "Has more: yes") - assert.Contains(t, out, "Next: kernel profile list --page 2 --per-page 2") + assert.Contains(t, out, "Next: kernel profiles list --page 2 --per-page 2") assert.Contains(t, out, "p0") assert.Contains(t, out, "p1") assert.NotContains(t, out, "p2") diff --git a/go.mod b/go.mod index b16ca81b..35e41851 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.86.0 + github.com/kernel/kernel-go-sdk v0.86.1-0.20260804210355-b94cd585bac0 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 cf5cc8a2..7cfaa6b5 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.86.0 h1:HixQQhL2llD+gLnmmexEh+iVFCs5Greryi1CK8Eim9E= -github.com/kernel/kernel-go-sdk v0.86.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260804210355-b94cd585bac0 h1:yVTB5w7bXV2E3p+bu4ebfOsf8p+UGstjM5rXsvWipLs= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260804210355-b94cd585bac0/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 25859452266315d03f3d67fd2db9becf1de5e680 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:13:03 +0000 Subject: [PATCH 03/11] CLI: Verify SDK coverage at 9e39767 and add audit-logs download --format Target SDK commit 9e39767e809b04f0786fb17377eb800fa624bddc contains no API changes: it adds only .github/workflows/stlc-{promote,sync}.yml on top of v0.86.0. `git diff v0.86.0..9e39767` touches zero .go files and no api.md. The SHA is also not resolvable as a module version on its own -- it exists only on the staging repo's main, so `go get @9e39767` fails with "unknown revision". go.mod needs no change here: the preceding commit already moved it to v0.86.1-0.20260804210355-b94cd585bac0, and b94cd58 is a direct child of 9e39767, so that pseudo-version already contains this target commit. Coverage was re-enumerated from scratch anyway rather than trusting the empty diff. All 127 methods in api.md map to an existing command; the seven x-cli-skip endpoints in openapi.yaml (auth connections exchange, audit-log export destinations) are absent from the SDK, so nothing to skip. Every *Params field was diffed against every command's flags, including expanding the embedded request structs (ManagedAuth*, CreateCredential*, CreateProject, Update*Limits, CreateCredentialProvider*) and the ProxyNewParamsConfig union variants, which the field-name comparison alone hides. That surfaced one real gap, pre-existing rather than new in this bump: - kernel audit-logs download: --format jsonl.gz|jsonl (AuditLogExportChunkParams.Format). The format was hardcoded to jsonl.gz, so uncompressed export was unreachable. The default output filename now derives its extension from the format instead of always ending .jsonl.gz. Two other unmatched fields were checked and are correctly not flags: - BrowserCurlParams.ResponseEncoding: `browsers curl` does not call the SDK Curl method at all. It streams raw bytes over the browser's HTTPClient, which is already binary-safe, making the JSON-transport encoding moot. - AuditLogListParams.PageToken / AuditLogExportChunkParams.Cursor: internal pagination, driven by response headers. Tested against the live API: - audit-logs download --format jsonl (25-day window): 74 chunks, 3,669,435 rows, wrote audit-logs-20260710-20260804.jsonl; `file` reports NDJSON and the lines parse as JSON. - audit-logs download (default): wrote audit-logs-20260803-20260804.jsonl.gz; gunzip yields 109,526 lines, matching the reported row count. - audit-logs download --format jsonl.gz --to explicit.jsonl.gz: byte-identical size to the default run. - audit-logs download --format csv: rejected locally with "invalid --format value: csv". - go build ./... and go test ./... pass, before and after rebasing onto the b94cd58 go.mod bump. Co-Authored-By: Claude Opus 5 --- cmd/audit_logs_download.go | 28 ++++++++++++++++++++-------- cmd/audit_logs_download_test.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/cmd/audit_logs_download.go b/cmd/audit_logs_download.go index a49954eb..56c5ec8c 100644 --- a/cmd/audit_logs_download.go +++ b/cmd/audit_logs_download.go @@ -30,6 +30,7 @@ type AuditLogsDownloadInput struct { Service string AuthStrategy string UserIDs []string + Format string To string Force bool } @@ -50,7 +51,7 @@ func (c AuditLogsCmd) Download(ctx context.Context, in AuditLogsDownloadInput) e outPath := in.To if outPath == "" { - outPath = defaultAuditLogsDownloadPath(params.Start, params.End) + outPath = defaultAuditLogsDownloadPath(params.Start, params.End, params.Format) } partialPath := outPath + ".partial" out, err := openAuditLogsDownloadOutput(partialPath, outPath, in.Force) @@ -202,7 +203,14 @@ func buildAuditLogsDownloadParams(in AuditLogsDownloadInput) (kernel.AuditLogExp params.Start = start params.End = end - params.Format = kernel.AuditLogExportChunkParamsFormatJSONLGz + switch in.Format { + case "", "jsonl.gz": + params.Format = kernel.AuditLogExportChunkParamsFormatJSONLGz + case "jsonl": + params.Format = kernel.AuditLogExportChunkParamsFormatJSONL + default: + return params, fmt.Errorf("invalid --format value: %s (must be 'jsonl.gz' or 'jsonl')", in.Format) + } if in.Search != "" { params.Search = kernel.String(in.Search) } @@ -222,9 +230,9 @@ func buildAuditLogsDownloadParams(in AuditLogsDownloadInput) (kernel.AuditLogExp return params, nil } -func defaultAuditLogsDownloadPath(start, end time.Time) string { +func defaultAuditLogsDownloadPath(start, end time.Time, format kernel.AuditLogExportChunkParamsFormat) string { const stamp = "20060102" - return fmt.Sprintf("audit-logs-%s-%s.jsonl.gz", start.UTC().Format(stamp), end.UTC().Format(stamp)) + return fmt.Sprintf("audit-logs-%s-%s.%s", start.UTC().Format(stamp), end.UTC().Format(stamp), format) } // checkAuditLogsDownloadTarget rejects paths that are not replaceable @@ -289,20 +297,23 @@ func runAuditLogsDownload(cmd *cobra.Command, args []string) error { service, _ := cmd.Flags().GetString("service") authStrategy, _ := cmd.Flags().GetString("auth-strategy") userIDs, _ := cmd.Flags().GetStringArray("user-id") + format, _ := cmd.Flags().GetString("format") to, _ := cmd.Flags().GetString("to") force, _ := cmd.Flags().GetBool("force") return c.Download(cmd.Context(), AuditLogsDownloadInput{ Start: start, End: end, Search: search, Method: method, ExcludeMethod: excludeMethod, IncludeGet: includeGet, Service: service, - AuthStrategy: authStrategy, UserIDs: userIDs, To: to, Force: force, + AuthStrategy: authStrategy, UserIDs: userIDs, Format: format, + To: to, Force: force, }) } var auditLogsDownloadCmd = &cobra.Command{ Use: "download", - Short: "Download audit logs as gzip-compressed JSONL", - Long: "Download audit logs as gzip-compressed JSONL in verified chunks. The time range is [start, end).\n\n" + + Short: "Download audit logs as JSONL", + Long: "Download audit logs as JSONL in verified chunks. The time range is [start, end).\n\n" + + "Output is gzip-compressed by default; pass --format jsonl for uncompressed JSONL.\n\n" + "The API allows at most 30 days per download.\n\n" + "GET requests are excluded by default; pass --include-get to include them.\n\n" + "The output file is published only after every chunk is downloaded.", @@ -321,7 +332,8 @@ func init() { auditLogsDownloadCmd.Flags().String("service", "", "Filter by service") auditLogsDownloadCmd.Flags().String("auth-strategy", "", "Filter by authentication strategy") auditLogsDownloadCmd.Flags().StringArray("user-id", nil, "Filter by user ID (repeatable)") - auditLogsDownloadCmd.Flags().String("to", "", "Output .jsonl.gz file path") + auditLogsDownloadCmd.Flags().String("format", "jsonl.gz", "Export format: jsonl.gz or jsonl") + auditLogsDownloadCmd.Flags().String("to", "", "Output file path (defaults to a name derived from the window and --format)") auditLogsDownloadCmd.Flags().Bool("force", false, "Overwrite the output file") _ = auditLogsDownloadCmd.MarkFlagRequired("start") _ = auditLogsDownloadCmd.MarkFlagRequired("end") diff --git a/cmd/audit_logs_download_test.go b/cmd/audit_logs_download_test.go index 9df3d67a..9a9fcda5 100644 --- a/cmd/audit_logs_download_test.go +++ b/cmd/audit_logs_download_test.go @@ -246,9 +246,36 @@ func TestAuditLogsDownloadDoesNotRetryClientErrors(t *testing.T) { func TestDefaultAuditLogsDownloadPath(t *testing.T) { start := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2026, 6, 28, 0, 0, 0, 0, time.UTC) - path := defaultAuditLogsDownloadPath(start, end) - assert.Equal(t, "audit-logs-20260601-20260628.jsonl.gz", path) + gz := defaultAuditLogsDownloadPath(start, end, kernel.AuditLogExportChunkParamsFormatJSONLGz) + assert.Equal(t, "audit-logs-20260601-20260628.jsonl.gz", gz) + + plain := defaultAuditLogsDownloadPath(start, end, kernel.AuditLogExportChunkParamsFormatJSONL) + assert.Equal(t, "audit-logs-20260601-20260628.jsonl", plain) +} + +func TestAuditLogsDownloadFormatParam(t *testing.T) { + base := AuditLogsDownloadInput{Start: "2026-06-01", End: "2026-06-02"} + + for _, tc := range []struct { + format string + want kernel.AuditLogExportChunkParamsFormat + }{ + {format: "", want: kernel.AuditLogExportChunkParamsFormatJSONLGz}, + {format: "jsonl.gz", want: kernel.AuditLogExportChunkParamsFormatJSONLGz}, + {format: "jsonl", want: kernel.AuditLogExportChunkParamsFormatJSONL}, + } { + in := base + in.Format = tc.format + params, err := buildAuditLogsDownloadParams(in) + require.NoError(t, err) + assert.Equal(t, tc.want, params.Format) + } + + in := base + in.Format = "csv" + _, err := buildAuditLogsDownloadParams(in) + require.ErrorContains(t, err, "invalid --format value") } func TestAuditLogsDownloadRejectsBadChunkBeforeWriting(t *testing.T) { From 1d24d11195b65b0dbf08870916e980489edd4fb6 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:14:15 +0000 Subject: [PATCH 04/11] CLI: Update Go SDK to f026272 Bumps github.com/kernel/kernel-go-sdk to v0.86.1-0.20260805130902-f0262729f366 (commit f026272). The SDK diff between b94cd58 and f026272 is empty for api.md and all Go source files -- the only change in that range is a CI workflow file (.github/workflows/stlc-promote.yml). No new methods, no new param fields, so no CLI commands or flags were added. Coverage verification: - Enumerated all 127 SDK methods from api.md; every one has a corresponding CLI command. - Enumerated all 95 SDK *Params structs and their fields, diffed against all 205 CLI flags. Every remaining discrepancy resolved to a naming difference (--tag vs Tags, --kiosk vs KioskMode, --bypass-host vs BypassHosts, --env vs EnvVars), a positional argument, an embedded request-body struct, or a path parameter. - BrowserCurlParams.ResponseEncoding remains intentionally unexposed: `kernel browsers curl` bypasses the SDK Curl method and streams raw bytes through the browser's HTTP client, so response encoding does not apply. Tested: go build ./..., go vet ./..., go test ./... (all pass); smoke-tested `browsers list --limit 3` and `app list --per-page 2` against the live API -- both return expected output and the app list pagination footer renders correctly. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 35e41851..1135e6ef 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.86.1-0.20260804210355-b94cd585bac0 + github.com/kernel/kernel-go-sdk v0.86.1-0.20260805130902-f0262729f366 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 7cfaa6b5..af733e46 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.86.1-0.20260804210355-b94cd585bac0 h1:yVTB5w7bXV2E3p+bu4ebfOsf8p+UGstjM5rXsvWipLs= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260804210355-b94cd585bac0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260805130902-f0262729f366 h1:bvUr2cwFy9WP9CTFRBnC81HMf26n7TMczb4thcfbwo8= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260805130902-f0262729f366/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 9f4d3fd1ea607eb82460de6b5ff5fc480df5fdce Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:39:24 +0000 Subject: [PATCH 05/11] CLI: Update Go SDK to f607274 Bumps kernel-go-sdk to v0.86.1-0.20260805143436-f6072746e884 (kernel/kernel-go-sdk@f6072746e884cecb4a59f8d7cdc49ac94af9e6a4). The SDK diff between f026272 and f607274 contains no changes to api.md or any Go source file - f607274 is the stainless release merge commit and f026272 was a CI-only change. No new methods, params, or fields. Coverage analysis: performed a full enumeration anyway. All 126 SDK methods in api.md map to existing CLI commands, and every field of every *Params struct maps to an existing flag. The 7 endpoints marked x-cli-skip in openapi.yaml (auth connection exchange, audit-log export destinations) are absent from the SDK surface, as expected. One intentional non-gap: BrowserCurlParams.ResponseEncoding has no flag because `kernel browsers curl` bypasses the Browsers.Curl SDK method and streams raw bytes through the browser's HTTP client, so the base64 JSON transport option does not apply. Tested: go build ./..., go vet ./..., go test ./... (all pass), plus live API smoke tests of profiles list, browsers list, proxies list, api-keys list against the rebuilt binary. 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 1135e6ef..dee64d64 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.86.1-0.20260805130902-f0262729f366 + github.com/kernel/kernel-go-sdk v0.86.1-0.20260805143436-f6072746e884 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 af733e46..52130471 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.86.1-0.20260805130902-f0262729f366 h1:bvUr2cwFy9WP9CTFRBnC81HMf26n7TMczb4thcfbwo8= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260805130902-f0262729f366/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260805143436-f6072746e884 h1:+FHbZA5buCeET31xevgC4fCxpbPtWmncrS82Y5w0+EU= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260805143436-f6072746e884/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 eaf3d8a475af0642ddf9618da3245cbce47d4581 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:03:08 +0000 Subject: [PATCH 06/11] CLI: Update Go SDK to a42a4e3 and add telemetry OTLP export flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps kernel-go-sdk to a42a4e3500ad02f7089f2903435d45ae907a1ff8. The SDK adds telemetry.export.otlp (destination by id or name, plus an enabled toggle) to every schema that embeds the shared telemetry request config. Exposes it as --telemetry-export-otlp on the endpoints that actually honor it: - browsers create - auth connections create / update / login Not added to browser-pools create/update/acquire or browsers update: the SDK carries the field there because the schema is shared, but the API rejects export on pools and ignores it on a browser update, so a flag there would be inert. A destination requires capture to be enabled in the same request — the API validates the payload on its own rather than consulting the stored config. Create implies --telemetry=all (nothing to clobber); update and login instead error with an actionable message, since enabling capture there would replace the connection's current category selection. Also surfaces export state in output: the browser create/update summary (where the resolved destination is an ID string) and the auth connection details table (where the stored request is echoed back as an {id, name} object). Full enumeration of all 127 api.md methods against the CLI command tree found no missing commands. Tested against the live API: browsers create --telemetry-export-otlp with off / destination-by-name / destination-by-CUID; auth connections create + get + update round-trip; both validation guards. Destination lookup errors confirm the field reaches the server on each path (the test org has no OTLP destinations configured, and no API exists to create one, so a successful export bind could not be exercised end to end). Co-Authored-By: Claude Opus 5 --- README.md | 20 ++- cmd/auth_connections.go | 48 ++++--- cmd/auth_connections_test.go | 54 ++++++++ cmd/browsers.go | 10 +- cmd/browsers_telemetry.go | 234 +++++++++++++++++++++++++++++---- cmd/browsers_telemetry_test.go | 150 ++++++++++++++++++++- go.mod | 2 +- go.sum | 4 +- 8 files changed, 469 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index c54f458c..71a54464 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,7 @@ Commands with JSON output support: - `--telemetry=all` - Enable telemetry for all categories - `--telemetry=off` - Disable telemetry - `--telemetry=` - Per-category config, e.g. `--telemetry=network=on,page=off` + - `--telemetry-export-otlp ` - Export captured telemetry over OTLP to one of the org's configured destinations. Implies `--telemetry=all` when `--telemetry` is not set, since export requires capture. Use `--telemetry-export-otlp=off` to disable export. - `--chrome-policy ` - Custom Chrome enterprise policy as a JSON object. Kernel-managed policies (extensions, proxy, automation) are rejected server-side. - `--chrome-policy-file ` - Read the Chrome enterprise policy from a file (use `-` for stdin). Mutually exclusive with `--chrome-policy`. - `--output json`, `-o json` - Output raw JSON object @@ -320,6 +321,16 @@ Telemetry config is a sub-field of the browser session. Use `browsers create` or Per-category updates are partial — only categories you name are changed; others retain their current state. `--telemetry=all` and `--telemetry=off` reset the entire config. +#### Exporting telemetry + +Captured telemetry can be exported over OTLP to one of the org's configured destinations with `--telemetry-export-otlp `. A value that looks like an ID is sent as one; anything else is resolved as a destination name, which must match exactly one destination in the org. + +- Capture and export: `kernel browsers create --telemetry-export-otlp my-collector` +- Capture without exporting: `kernel browsers create --telemetry=all` +- Stop exporting: `--telemetry-export-otlp=off` + +Export is bound at session creation, so it is available on `browsers create` and on the managed-auth commands that create a browser (`auth connections create`, `update`, and `login`). A browser session keeps the destination it was created with — `browsers update` cannot change it — and browser pools do not support export. + - `kernel browsers telemetry stream ` - Stream live telemetry events (NDJSON with `-o json`) - `--categories ` - Filter by event category (`console`, `network`, `page`, `interaction`, `control`, `connection`, `system`, `screenshot`, `captcha`, `monitor`) - `--types ` - Filter by event type (e.g. `network_response`, `console_error`) @@ -536,12 +547,15 @@ Managed auth connections (`kernel auth connections`). The commands below are new - `--page ` - Page number (1-based, default: 1) - `--per-page ` - Items per page (default: 20) - `--output json`, `-o json` - Output raw JSON array -- `kernel auth connections create` - New flag: +- `kernel auth connections create` - New flags: - `--telemetry=all` / `--telemetry=off` / `--telemetry=` - Default telemetry for this connection's browser sessions. Same semantics as `kernel browsers create` -- `kernel auth connections update ` - New flag: + - `--telemetry-export-otlp ` - Export this connection's captured telemetry over OTLP to one of the org's configured destinations. Implies `--telemetry=all` when `--telemetry` is not set. Use `=off` to disable export. +- `kernel auth connections update ` - New flags: - `--telemetry=all` / `--telemetry=off` / `--telemetry=` - Update telemetry for future browser sessions -- `kernel auth connections login ` - New flag: + - `--telemetry-export-otlp ` - Update where future sessions export captured telemetry. Naming a destination requires passing `--telemetry` in the same command, since the API validates capture and export together and enabling capture here would replace the connection's current category selection. Use `=off` to disable export. +- `kernel auth connections login ` - New flags: - `--telemetry=all` / `--telemetry=off` / `--telemetry=` - Telemetry override for this login only, merged onto the connection's config + - `--telemetry-export-otlp ` - Export override for this login only. Naming a destination requires passing `--telemetry` in the same command. Use `=off` to disable export. - `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 diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 06ef62ec..6209a8bc 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -55,6 +55,7 @@ type AuthConnectionCreateInput struct { NoAutoReauth bool RecordSession BoolFlag Telemetry string + TelemetryExport string Output string } @@ -87,6 +88,7 @@ type AuthConnectionUpdateInput struct { AutoReauth BoolFlag RecordSession BoolFlag Telemetry string + TelemetryExport string Output string } @@ -105,12 +107,13 @@ type AuthConnectionDeleteInput struct { } type AuthConnectionLoginInput struct { - ID string - ProxyID string - ProxyName string - RecordSession BoolFlag - Telemetry string - Output string + ID string + ProxyID string + ProxyName string + RecordSession BoolFlag + Telemetry string + TelemetryExport string + Output string } type AuthConnectionSubmitInput struct { @@ -221,8 +224,8 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" { - t, err := buildAuthConnectionCreateTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildAuthConnectionCreateTelemetryParam(in.Telemetry, in.TelemetryExport) if err != nil { return err } @@ -346,8 +349,8 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn hasChanges = true } - if in.Telemetry != "" { - t, err := buildAuthConnectionUpdateTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildAuthConnectionUpdateTelemetryParam(in.Telemetry, in.TelemetryExport) if err != nil { return err } @@ -662,8 +665,8 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu params.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" { - t, err := buildAuthConnectionLoginTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildAuthConnectionLoginTelemetryParam(in.Telemetry, in.TelemetryExport) if err != nil { return err } @@ -1108,6 +1111,7 @@ func init() { authConnectionsCreateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication (auto re-auth is enabled by default)") authConnectionsCreateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default (useful for debugging)") authConnectionsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for this connection's browser sessions (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") + authConnectionsCreateCmd.Flags().String("telemetry-export-otlp", "", "Export this connection's captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") _ = authConnectionsCreateCmd.MarkFlagRequired("domain") _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") authConnectionsCreateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") @@ -1134,6 +1138,7 @@ func init() { authConnectionsUpdateCmd.Flags().Bool("no-auto-reauth", false, "Mark expired sessions as NEEDS_AUTH instead of attempting automatic re-authentication") authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Set whether browser sessions are recorded by default; use --record-session=false to disable") authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") + authConnectionsUpdateCmd.Flags().String("telemetry-export-otlp", "", "Update where future sessions export captured telemetry over OTLP, by destination ID or name; --telemetry-export-otlp=off disables export. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks") @@ -1156,6 +1161,7 @@ func init() { authConnectionsLoginCmd.Flags().String("proxy-name", "", "Proxy name to use for this login") authConnectionsLoginCmd.Flags().Bool("record-session", false, "Override whether this login's browser session is recorded; use --record-session=false to disable") authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") + authConnectionsLoginCmd.Flags().String("telemetry-export-otlp", "", "Export override for this login only: an OTLP destination ID or name; --telemetry-export-otlp=off disables export for this login. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") // Submit flags addJSONOutputFlag(authConnectionsSubmitCmd) @@ -1208,6 +1214,7 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks") noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} @@ -1228,6 +1235,7 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { NoAutoReauth: noAutoReauth, RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, + TelemetryExport: telemetryExport, Output: output, }) } @@ -1259,6 +1267,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials") healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") saveCredentialsFlag := BoolFlag{} @@ -1309,6 +1318,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { AutoReauth: togglePair("auto-reauth", "no-auto-reauth"), RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, + TelemetryExport: telemetryExport, Output: output, }) } @@ -1352,16 +1362,18 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { proxyID, _ := cmd.Flags().GetString("proxy-id") proxyName, _ := cmd.Flags().GetString("proxy-name") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ - ID: args[0], - ProxyID: proxyID, - ProxyName: proxyName, - RecordSession: readBoolFlag(cmd.Flags(), "record-session"), - Telemetry: telemetry, - Output: output, + ID: args[0], + ProxyID: proxyID, + ProxyName: proxyName, + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), + Telemetry: telemetry, + TelemetryExport: telemetryExport, + Output: output, }) } diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index ada9cb18..a1ed08f8 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -869,6 +869,60 @@ func TestAuthConnectionsGet_TelemetryEnabledWithoutCategories(t *testing.T) { assert.Contains(t, outBuf.String(), "enabled (default categories)") } +func TestAuthConnectionsGet_TelemetryShowsExportDestination(t *testing.T) { + setupStdoutCapture(t) + // A connection bound to an OTLP destination reports where its sessions export, + // so the destination is visible without reading the raw JSON. Unlike a browser + // session's resolved config (where destination is the ID string), a connection + // echoes the stored request verbatim, so destination is an {id, name} object. + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + var auth kernel.ManagedAuth + require.NoError(t, json.Unmarshal([]byte(`{"id":"conn-1","browser_telemetry":{"enabled":true,"browser":{"console":{"enabled":true}},"export":{"otlp":{"enabled":true,"destination":{"id":"dest-abc"}}}}}`), &auth)) + return &auth, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "conn-1"})) + out := outBuf.String() + assert.Contains(t, out, "console") + assert.Contains(t, out, "exporting to dest-abc") +} + +func TestAuthConnectionsGet_TelemetryShowsExportDestinationByName(t *testing.T) { + setupStdoutCapture(t) + // A connection created with --telemetry-export-otlp stores the name, so + // that is what comes back; fall back to it when no ID is present. + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + var auth kernel.ManagedAuth + require.NoError(t, json.Unmarshal([]byte(`{"id":"conn-1","browser_telemetry":{"enabled":true,"export":{"otlp":{"enabled":true,"destination":{"name":"my-collector"}}}}}`), &auth)) + return &auth, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "conn-1"})) + assert.Contains(t, outBuf.String(), "exporting to my-collector") +} + +func TestAuthConnectionsGet_TelemetryOmitsExportWhenDisabled(t *testing.T) { + setupStdoutCapture(t) + // An export block that is present but disabled must not read as "exporting". + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + var auth kernel.ManagedAuth + require.NoError(t, json.Unmarshal([]byte(`{"id":"conn-1","browser_telemetry":{"enabled":true,"export":{"otlp":{"enabled":false}}}}`), &auth)) + return &auth, nil + }, + } + c := AuthConnectionCmd{svc: fake} + + require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "conn-1"})) + assert.NotContains(t, outBuf.String(), "exporting to") +} + func TestAuthConnectionsGet_TelemetryRowOmittedWhenOff(t *testing.T) { setupStdoutCapture(t) // Telemetry that is off is not reported at all, rather than shown as a diff --git a/cmd/browsers.go b/cmd/browsers.go index 4ed18f11..ee8ab266 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -290,6 +290,7 @@ type BrowsersCreateInput struct { Extensions []string Viewport string Telemetry string + TelemetryExport string ChromePolicy string ChromePolicyFile string Name string @@ -540,8 +541,8 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } } - if in.Telemetry != "" { - t, err := buildNewTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildNewTelemetryParam(in.Telemetry, in.TelemetryExport) if err != nil { return err } @@ -576,7 +577,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.StartURL, browser.Name, browser.Tags) - if in.Telemetry != "" { + if in.Telemetry != "" || in.TelemetryExport != "" { printTelemetrySummary(browser.Telemetry) } return nil @@ -2783,6 +2784,7 @@ func init() { browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)") browsersCreateCmd.Flags().String("pool-name", "", "Browser pool name to acquire from (mutually exclusive with --pool-id)") browsersCreateCmd.Flags().String("telemetry", "", "Configure telemetry (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") + browsersCreateCmd.Flags().String("telemetry-export-otlp", "", "Export captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") browsersCreateCmd.Flags().String("name", "", "Optional unique name for the browser session (used to find it later; can be changed with 'browsers update --name')") browsersCreateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the session (repeatable; up to 50 pairs)") browsersCreateCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts") @@ -2906,6 +2908,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { poolID, _ := cmd.Flags().GetString("pool-id") poolName, _ := cmd.Flags().GetString("pool-name") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") name, _ := cmd.Flags().GetString("name") tags, _ := tagsFromFlag(cmd, "tag") chromePolicy, _ := cmd.Flags().GetString("chrome-policy") @@ -3027,6 +3030,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { Extensions: extensions, Viewport: viewport, Telemetry: telemetry, + TelemetryExport: telemetryExport, ChromePolicy: chromePolicy, ChromePolicyFile: chromePolicyFile, Name: name, diff --git a/cmd/browsers_telemetry.go b/cmd/browsers_telemetry.go index 67367c8a..a5aa98fe 100644 --- a/cmd/browsers_telemetry.go +++ b/cmd/browsers_telemetry.go @@ -108,10 +108,102 @@ func resolveTelemetryFlag(s string) (param.Opt[bool], kernel.BrowserTelemetryCat } } -// buildNewTelemetryParam converts a --telemetry flag value to the create API param. -func buildNewTelemetryParam(s string) (kernel.BrowserNewParamsTelemetry, error) { +// telemetryExportOff is the --telemetry-export-otlp value that turns export off +// rather than naming a destination. +const telemetryExportOff = "off" + +// resolveTelemetryExportFlag interprets a --telemetry-export-otlp flag value: +// "off" disables OTLP export, and any other value selects a destination by ID or +// name. Setting a destination implies enabled=true server-side, so enabled is only +// sent for "off" (the API rejects enabled=false combined with a destination). +// It returns the resolved (enabled, id, name) triple so each endpoint can assemble +// its own param type. +func resolveTelemetryExportFlag(s string) (enabled param.Opt[bool], id, name string, err error) { + val := strings.TrimSpace(s) + if val == telemetryExportOff { + return kernel.Opt(false), "", "", nil + } + if val == "" { + return param.Opt[bool]{}, "", "", fmt.Errorf("empty --telemetry-export-otlp value: pass an OTLP destination ID or name, or %q to disable export", telemetryExportOff) + } + // Destinations have no list endpoint to disambiguate against, so fall back to + // the same CUID shape test the CLI uses for other ID-or-name references. + if cuidRegex.MatchString(val) { + return param.Opt[bool]{}, val, "", nil + } + return param.Opt[bool]{}, "", val, nil +} + +// telemetryFlagEnablesCapture reports whether a --telemetry value turns capture on. +// A destination requires capture to be enabled, so the create paths use this to +// decide whether to imply it. +func telemetryFlagEnablesCapture(s string) bool { + return s != "" && s != "off" +} + +// validateTelemetryExportCombo checks an export destination against the --telemetry +// value in the same command. The API validates the request payload on its own — it +// does not consult the stored config — so a destination is rejected unless that +// same request also enables capture, whether via enabled=true or category settings. +// +// canImply is true on the create paths, where there is no stored selection to +// clobber and capture can safely be turned on for the user. On update and login it +// is false: enabling capture there would replace the connection's current category +// selection, so the user has to say what to capture. +// +// Error messages never lead with a flag token — the error style title-cases the +// first word and treats - and = as word boundaries inside it. +func validateTelemetryExportCombo(telemetry, id, name string, canImply bool) error { + if id == "" && name == "" { + return nil + } + if telemetry == "off" { + return fmt.Errorf("cannot combine --telemetry=off with an export destination: export requires telemetry capture to be enabled") + } + if telemetry == "" && !canImply { + return fmt.Errorf("setting an export destination also requires --telemetry in the same command: use --telemetry=all for the default set, or --telemetry=console,network to select categories") + } + return nil +} + +// buildNewTelemetryParam converts --telemetry and --telemetry-export-otlp flag +// values to the create API param. +func buildNewTelemetryParam(s, export string) (kernel.BrowserNewParamsTelemetry, error) { enabled, browser, err := resolveTelemetryFlag(s) - return kernel.BrowserNewParamsTelemetry{Enabled: enabled, Browser: browser}, err + p := kernel.BrowserNewParamsTelemetry{Enabled: enabled, Browser: browser} + if err != nil || export == "" { + return p, err + } + exEnabled, id, name, err := resolveTelemetryExportFlag(export) + if err != nil { + return p, err + } + if err := validateTelemetryExportCombo(s, id, name, true); err != nil { + return p, err + } + // A destination needs capture on. Nothing exists yet to clobber on create, so + // imply the default set rather than making the user repeat --telemetry=all. + if (id != "" || name != "") && !telemetryFlagEnablesCapture(s) { + p.Enabled = kernel.Opt(true) + } + p.Export = kernel.BrowserNewParamsTelemetryExport{ + Otlp: kernel.BrowserNewParamsTelemetryExportOtlp{ + Enabled: exEnabled, + Destination: kernel.BrowserNewParamsTelemetryExportOtlpDestination{ + ID: optIfSet(id), + Name: optIfSet(name), + }, + }, + } + return p, nil +} + +// optIfSet wraps a non-empty string as a set param, leaving it omitted otherwise. +func optIfSet(s string) param.Opt[string] { + if s == "" { + return param.Opt[string]{} + } + return kernel.Opt(s) } // buildUpdateTelemetryParam converts a --telemetry flag value to the update API param. @@ -120,40 +212,127 @@ func buildUpdateTelemetryParam(s string) (kernel.BrowserUpdateParamsTelemetry, e return kernel.BrowserUpdateParamsTelemetry{Enabled: enabled, Browser: browser}, err } -// buildAuthConnectionCreateTelemetryParam converts a --telemetry flag value to the -// browser telemetry default stored on a new auth connection. -func buildAuthConnectionCreateTelemetryParam(s string) (kernel.ManagedAuthCreateRequestBrowserTelemetryParam, error) { +// buildAuthConnectionCreateTelemetryParam converts --telemetry and +// --telemetry-export-otlp flag values to the browser telemetry default stored on a +// new auth connection. +func buildAuthConnectionCreateTelemetryParam(s, export string) (kernel.ManagedAuthCreateRequestBrowserTelemetryParam, error) { enabled, browser, err := resolveTelemetryFlag(s) - return kernel.ManagedAuthCreateRequestBrowserTelemetryParam{Enabled: enabled, Browser: browser}, err + p := kernel.ManagedAuthCreateRequestBrowserTelemetryParam{Enabled: enabled, Browser: browser} + if err != nil || export == "" { + return p, err + } + exEnabled, id, name, err := resolveTelemetryExportFlag(export) + if err != nil { + return p, err + } + if err := validateTelemetryExportCombo(s, id, name, true); err != nil { + return p, err + } + if (id != "" || name != "") && !telemetryFlagEnablesCapture(s) { + p.Enabled = kernel.Opt(true) + } + p.Export = kernel.ManagedAuthCreateRequestBrowserTelemetryExportParam{ + Otlp: kernel.ManagedAuthCreateRequestBrowserTelemetryExportOtlpParam{ + Enabled: exEnabled, + Destination: kernel.ManagedAuthCreateRequestBrowserTelemetryExportOtlpDestinationParam{ + ID: optIfSet(id), + Name: optIfSet(name), + }, + }, + } + return p, nil } -// buildAuthConnectionUpdateTelemetryParam converts a --telemetry flag value to the -// browser telemetry default for future sessions of an existing auth connection. -func buildAuthConnectionUpdateTelemetryParam(s string) (kernel.ManagedAuthUpdateRequestBrowserTelemetryParam, error) { +// buildAuthConnectionUpdateTelemetryParam converts --telemetry and +// --telemetry-export-otlp flag values to the browser telemetry default for future +// sessions of an existing auth connection. Unlike the create paths this never +// implies capture: the connection already has a stored config, and enabled=true +// would replace its category selection rather than merge onto it. +func buildAuthConnectionUpdateTelemetryParam(s, export string) (kernel.ManagedAuthUpdateRequestBrowserTelemetryParam, error) { enabled, browser, err := resolveTelemetryFlag(s) - return kernel.ManagedAuthUpdateRequestBrowserTelemetryParam{Enabled: enabled, Browser: browser}, err + p := kernel.ManagedAuthUpdateRequestBrowserTelemetryParam{Enabled: enabled, Browser: browser} + if err != nil || export == "" { + return p, err + } + exEnabled, id, name, err := resolveTelemetryExportFlag(export) + if err != nil { + return p, err + } + if err := validateTelemetryExportCombo(s, id, name, false); err != nil { + return p, err + } + p.Export = kernel.ManagedAuthUpdateRequestBrowserTelemetryExportParam{ + Otlp: kernel.ManagedAuthUpdateRequestBrowserTelemetryExportOtlpParam{ + Enabled: exEnabled, + Destination: kernel.ManagedAuthUpdateRequestBrowserTelemetryExportOtlpDestinationParam{ + ID: optIfSet(id), + Name: optIfSet(name), + }, + }, + } + return p, nil } -// buildAuthConnectionLoginTelemetryParam converts a --telemetry flag value to the -// per-login browser telemetry override. -func buildAuthConnectionLoginTelemetryParam(s string) (kernel.AuthConnectionLoginParamsBrowserTelemetry, error) { +// buildAuthConnectionLoginTelemetryParam converts --telemetry and +// --telemetry-export-otlp flag values to the per-login browser telemetry override. +// The override merges onto the connection's stored config, which may already +// enable capture, so this does not imply it either. +func buildAuthConnectionLoginTelemetryParam(s, export string) (kernel.AuthConnectionLoginParamsBrowserTelemetry, error) { enabled, browser, err := resolveTelemetryFlag(s) - return kernel.AuthConnectionLoginParamsBrowserTelemetry{Enabled: enabled, Browser: browser}, err + p := kernel.AuthConnectionLoginParamsBrowserTelemetry{Enabled: enabled, Browser: browser} + if err != nil || export == "" { + return p, err + } + exEnabled, id, name, err := resolveTelemetryExportFlag(export) + if err != nil { + return p, err + } + if err := validateTelemetryExportCombo(s, id, name, false); err != nil { + return p, err + } + p.Export = kernel.AuthConnectionLoginParamsBrowserTelemetryExport{ + Otlp: kernel.AuthConnectionLoginParamsBrowserTelemetryExportOtlp{ + Enabled: exEnabled, + Destination: kernel.AuthConnectionLoginParamsBrowserTelemetryExportOtlpDestination{ + ID: optIfSet(id), + Name: optIfSet(name), + }, + }, + } + return p, nil } // formatManagedAuthTelemetry renders an auth connection's default browser telemetry // config for the details table. func formatManagedAuthTelemetry(cfg kernel.ManagedAuthBrowserTelemetry) string { - if on := telemetryEnabledCategories(kernel.BrowserTelemetryConfig{Browser: cfg.Browser}); len(on) > 0 { - return strings.Join(on, ", ") + base := func() string { + if on := telemetryEnabledCategories(kernel.BrowserTelemetryConfig{Browser: cfg.Browser}); len(on) > 0 { + return strings.Join(on, ", ") + } + // The API preserves the create-browser config verbatim rather than resolving + // it, so `{"enabled": true}` with no per-category settings means the default + // set. Reporting that as "disabled" would invert the connection's state. + if cfg.Enabled { + return "enabled (default categories)" + } + return "disabled" + }() + if dest := managedAuthExportDestination(cfg.Export); dest != "" { + return base + " (exporting to " + dest + ")" + } + return base +} + +// managedAuthExportDestination returns the OTLP destination an auth connection's +// sessions export to, or "" when export is off or unset. +func managedAuthExportDestination(ex kernel.ManagedAuthBrowserTelemetryExport) string { + if !ex.Otlp.Enabled { + return "" } - // The API preserves the create-browser config verbatim rather than resolving - // it, so `{"enabled": true}` with no per-category settings means the default - // set. Reporting that as "disabled" would invert the connection's state. - if cfg.Enabled { - return "enabled (default categories)" + if id := ex.Otlp.Destination.ID; id != "" { + return id } - return "disabled" + return ex.Otlp.Destination.Name } // settableCategories are the categories accepted by --telemetry=. @@ -204,6 +383,15 @@ func printTelemetrySummary(cfg kernel.BrowserTelemetryConfig) { return } pterm.Info.Printf("Telemetry capturing: %s\n", strings.Join(on, ", ")) + if cfg.Export.Otlp.Enabled { + // The response reports the resolved destination by ID even when the request + // selected it by name. + if dest := cfg.Export.Otlp.Destination; dest != "" { + pterm.Info.Printf("Telemetry exporting over OTLP to: %s\n", dest) + } else { + pterm.Info.Println("Telemetry exporting over OTLP") + } + } } // shouldEmit applies client-side category/type filters to a telemetry event. diff --git a/cmd/browsers_telemetry_test.go b/cmd/browsers_telemetry_test.go index 982d2d89..50ced5e6 100644 --- a/cmd/browsers_telemetry_test.go +++ b/cmd/browsers_telemetry_test.go @@ -336,21 +336,21 @@ func TestParseTelemetryCategories_WhitespaceTolerance(t *testing.T) { // listed categories enabled (Enabled unset). func TestBuildTelemetryParam_WireEncoding(t *testing.T) { t.Run("all", func(t *testing.T) { - p, err := buildNewTelemetryParam("all") + p, err := buildNewTelemetryParam("all", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.True(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("off", func(t *testing.T) { - p, err := buildNewTelemetryParam("off") + p, err := buildNewTelemetryParam("off", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.False(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("opt-in list sets only Browser", func(t *testing.T) { - p, err := buildNewTelemetryParam("network,control") + p, err := buildNewTelemetryParam("network,control", "") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "Enabled must be unset for an opt-in selection") assert.True(t, p.Browser.Network.Enabled.Valid()) @@ -360,6 +360,150 @@ func TestBuildTelemetryParam_WireEncoding(t *testing.T) { }) } +// TestBuildTelemetryParam_ExportWireEncoding locks in the OTLP export wire shapes. +// A destination is sent as id or name (never both) and implies capture on create; +// "off" sends enabled=false with no destination, since the API rejects +// enabled=false combined with one. +func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { + t.Run("destination by CUID sets id", func(t *testing.T) { + p, err := buildNewTelemetryParam("", "abcdefghijklmnopqrstuvwx") + assert.NoError(t, err) + otlp := p.Export.Otlp + assert.True(t, otlp.Destination.ID.Valid()) + assert.Equal(t, "abcdefghijklmnopqrstuvwx", otlp.Destination.ID.Value) + assert.False(t, otlp.Destination.Name.Valid(), "name must be unset when id is sent") + assert.False(t, otlp.Enabled.Valid(), "a destination implies enabled server-side") + }) + t.Run("destination by name sets name", func(t *testing.T) { + p, err := buildNewTelemetryParam("", "my-collector") + assert.NoError(t, err) + otlp := p.Export.Otlp + assert.True(t, otlp.Destination.Name.Valid()) + assert.Equal(t, "my-collector", otlp.Destination.Name.Value) + assert.False(t, otlp.Destination.ID.Valid(), "id must be unset when name is sent") + }) + t.Run("destination implies capture on create", func(t *testing.T) { + p, err := buildNewTelemetryParam("", "my-collector") + assert.NoError(t, err) + assert.True(t, p.Enabled.Valid(), "export requires capture, so create implies it") + assert.True(t, p.Enabled.Value) + }) + t.Run("explicit --telemetry selection is preserved", func(t *testing.T) { + p, err := buildNewTelemetryParam("network,control", "my-collector") + assert.NoError(t, err) + assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be overridden") + assert.True(t, p.Browser.Network.Enabled.Value) + assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) + }) + t.Run("off disables export without a destination", func(t *testing.T) { + p, err := buildNewTelemetryParam("all", "off") + assert.NoError(t, err) + otlp := p.Export.Otlp + assert.True(t, otlp.Enabled.Valid()) + assert.False(t, otlp.Enabled.Value) + assert.False(t, otlp.Destination.ID.Valid()) + assert.False(t, otlp.Destination.Name.Valid()) + }) + t.Run("off does not imply capture", func(t *testing.T) { + p, err := buildNewTelemetryParam("", "off") + assert.NoError(t, err) + assert.False(t, p.Enabled.Valid(), "disabling export must not turn capture on") + }) + // The API validates the request payload on its own rather than consulting the + // stored config, so a destination needs a capture-enabling --telemetry in the + // same request. Update and login refuse to supply one: doing so would replace + // the connection's current category selection. + t.Run("update requires an explicit --telemetry alongside a destination", func(t *testing.T) { + _, err := buildAuthConnectionUpdateTelemetryParam("", "my-collector") + assert.Error(t, err) + assert.Contains(t, err.Error(), "also requires --telemetry") + }) + t.Run("login requires an explicit --telemetry alongside a destination", func(t *testing.T) { + _, err := buildAuthConnectionLoginTelemetryParam("", "my-collector") + assert.Error(t, err) + assert.Contains(t, err.Error(), "also requires --telemetry") + }) + t.Run("update never implies capture when --telemetry is given", func(t *testing.T) { + p, err := buildAuthConnectionUpdateTelemetryParam("console", "my-collector") + assert.NoError(t, err) + assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be widened to the default set") + assert.True(t, p.Browser.Console.Enabled.Value) + assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) + }) + t.Run("login never implies capture when --telemetry is given", func(t *testing.T) { + p, err := buildAuthConnectionLoginTelemetryParam("console", "my-collector") + assert.NoError(t, err) + assert.False(t, p.Enabled.Valid()) + assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) + }) + t.Run("update and login allow export=off without --telemetry", func(t *testing.T) { + u, err := buildAuthConnectionUpdateTelemetryParam("", "off") + assert.NoError(t, err) + assert.False(t, u.Export.Otlp.Enabled.Value) + l, err := buildAuthConnectionLoginTelemetryParam("", "off") + assert.NoError(t, err) + assert.False(t, l.Export.Otlp.Enabled.Value) + }) + t.Run("auth connection create implies capture", func(t *testing.T) { + p, err := buildAuthConnectionCreateTelemetryParam("", "my-collector") + assert.NoError(t, err) + assert.True(t, p.Enabled.Valid()) + assert.True(t, p.Enabled.Value) + }) + t.Run("invalid category still errors with export set", func(t *testing.T) { + _, err := buildNewTelemetryParam("bogus", "my-collector") + assert.Error(t, err) + }) + t.Run("telemetry=off with a destination is rejected", func(t *testing.T) { + for _, tc := range []struct { + name string + fn func() error + }{ + {"create", func() error { _, e := buildNewTelemetryParam("off", "my-collector"); return e }}, + {"auth create", func() error { _, e := buildAuthConnectionCreateTelemetryParam("off", "my-collector"); return e }}, + {"auth update", func() error { _, e := buildAuthConnectionUpdateTelemetryParam("off", "my-collector"); return e }}, + {"auth login", func() error { _, e := buildAuthConnectionLoginTelemetryParam("off", "my-collector"); return e }}, + } { + err := tc.fn() + assert.Error(t, err, tc.name) + assert.Contains(t, err.Error(), "requires telemetry capture", tc.name) + } + }) + t.Run("telemetry=off with export=off is allowed", func(t *testing.T) { + p, err := buildNewTelemetryParam("off", "off") + assert.NoError(t, err) + assert.False(t, p.Enabled.Value) + assert.False(t, p.Export.Otlp.Enabled.Value) + }) + t.Run("empty export value errors", func(t *testing.T) { + _, err := buildNewTelemetryParam("all", " ") + assert.Error(t, err) + }) +} + +// TestPrintTelemetrySummary_Export covers the browser-session shape, where the +// resolved config reports destination as the ID string (not the {id, name} object +// an auth connection echoes back). +func TestPrintTelemetrySummary_Export(t *testing.T) { + parse := func(raw string) kernel.BrowserTelemetryConfig { + var cfg kernel.BrowserTelemetryConfig + if err := json.Unmarshal([]byte(raw), &cfg); err != nil { + t.Fatalf("unmarshal: %v", err) + } + return cfg + } + t.Run("reports the destination", func(t *testing.T) { + setupStdoutCapture(t) + printTelemetrySummary(parse(`{"browser":{"control":{"enabled":true}},"export":{"otlp":{"enabled":true,"destination":"dest-abc"}}}`)) + assert.Contains(t, outBuf.String(), "OTLP to: dest-abc") + }) + t.Run("stays quiet when export is off", func(t *testing.T) { + setupStdoutCapture(t) + printTelemetrySummary(parse(`{"browser":{"control":{"enabled":true}},"export":{"otlp":{"enabled":false}}}`)) + assert.NotContains(t, outBuf.String(), "OTLP") + }) +} + func TestTelemetryEnabledCategories(t *testing.T) { var cfg kernel.BrowserTelemetryConfig raw := `{"browser":{"control":{"enabled":true},"system":{"enabled":true},"network":{"enabled":false}}}` diff --git a/go.mod b/go.mod index dee64d64..b33eeff1 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.86.1-0.20260805143436-f6072746e884 + github.com/kernel/kernel-go-sdk v0.86.1-0.20260806004355-a42a4e3500ad 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 52130471..8089a5a0 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.86.1-0.20260805143436-f6072746e884 h1:+FHbZA5buCeET31xevgC4fCxpbPtWmncrS82Y5w0+EU= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260805143436-f6072746e884/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806004355-a42a4e3500ad h1:nqnQWhVOSd7czqFJdkFmt0ZSEWs0VP2+gIAGtYtge3I= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806004355-a42a4e3500ad/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 b4c161b2478537a5c28f7edee50d333541ddf7bc Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:25:45 +0000 Subject: [PATCH 07/11] CLI: Update Go SDK to 10e64888bbba7139d815dde7eec7cfa895093aa1 Bumps github.com/kernel/kernel-go-sdk to v0.86.1-0.20260806131931-10e64888bbba (commit 10e6488). The upstream SDK commit only touched scripts/utils/upload-artifact.sh (restoring a Stainless artifact download slug); no api.md entries, service methods, or param structs changed. Coverage analysis: full enumeration of all 130 SDK methods in api.md against the 138-command CLI tree, plus a field-by-field comparison of every *Params struct (including nested request bodies such as ManagedAuthCreateRequestParam, CreateCredentialRequestParam, and UpdateProjectLimitsRequestParam) against the flags of each command. No coverage gaps found. Endpoints marked x-cli-skip in openapi.yaml (/auth/connections/{id}/exchange, audit-log export destinations) are absent from the SDK and correctly absent from the CLI. Tested: go vet ./... and go test ./... pass; smoke tested `kernel app list` and `kernel browsers list` against the production API. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b33eeff1..842b9ecd 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.86.1-0.20260806004355-a42a4e3500ad + github.com/kernel/kernel-go-sdk v0.86.1-0.20260806131931-10e64888bbba 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 8089a5a0..ab298909 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.86.1-0.20260806004355-a42a4e3500ad h1:nqnQWhVOSd7czqFJdkFmt0ZSEWs0VP2+gIAGtYtge3I= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260806004355-a42a4e3500ad/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806131931-10e64888bbba h1:uMH7RO6vRG/6IAhp5YeqiY/ksR6eSoz/cl201QUzPnw= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806131931-10e64888bbba/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 909c48a69b04ed623694bd336586d0efc7be3c3a Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 13:49:55 +0000 Subject: [PATCH 08/11] CLI: Update Go SDK to f64bbb81fe7cf5d5738264af775bd853f911734a Bumps github.com/kernel/kernel-go-sdk to v0.86.1-0.20260806134418-f64bbb81fe7c. The upstream commit is a merge whose tree is identical to the previously pinned 10e64888bbba, so there are no API surface changes: the generated api.md and *.go sources are byte-identical between the two versions. Coverage analysis performed anyway: - Enumerated all 127 SDK methods from api.md (including nested resources: Browsers.{Fs,Fs.Watch,Process,Computer,Telemetry,Replays,Logs,Playwright}, Auth.Connections, Organization.Limits). - Enumerated all 138 CLI leaf commands and their flags. - Expanded every *Params struct plus the nested request params (ManagedAuthCreate/Update, CreateCredential, CreateCredentialProvider, SubmitFields, UpdateOrgLimits, UpdateProjectLimits, CreateProject). - No missing commands and no missing flags. Verified the x-cli-skip endpoints (/auth/connections/{id}/exchange and the /audit-logs/export/destinations family) are absent from the SDK surface, so they need no CLI coverage. Tested: go build ./..., go vet ./..., go test ./... all pass; `kernel status` and `kernel profiles list` smoke-tested 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 842b9ecd..26a64355 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.86.1-0.20260806131931-10e64888bbba + github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134418-f64bbb81fe7c 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 ab298909..87b85f54 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.86.1-0.20260806131931-10e64888bbba h1:uMH7RO6vRG/6IAhp5YeqiY/ksR6eSoz/cl201QUzPnw= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260806131931-10e64888bbba/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134418-f64bbb81fe7c h1:qsnAB/mcaTqA6tSSzlLxoq551Mn4411mNK0Hr3Z3zvQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134418-f64bbb81fe7c/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 896f18a5a7e728e140dd3666073fab12f4fd4e64 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" Date: Thu, 6 Aug 2026 13:57:27 +0000 Subject: [PATCH 09/11] CLI: Update Go SDK to 05231c85962558ae3d4681646697ab7175d91189 Bumps github.com/kernel/kernel-go-sdk to v0.86.1-0.20260806134905-05231c859625. The SDK diff between the CLI's previous pin (v0.86.1-0.20260806134418-f64bbb81fe7c) and this commit is empty: the only SDK change is CI-only (.github/workflows/release-please.yml). No API surface changed. Coverage analysis: full enumeration of all 127 methods in api.md against the CLI command tree found no gaps. All 127 map to existing commands, and all 94 param structs' fields map to existing flags, positional args, or the --page/--per-page pagination pattern. Tested: go build ./..., go test ./... (all pass), and smoke tested `browsers list`, `profiles list`, `app list` 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 26a64355..709b3e35 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.86.1-0.20260806134418-f64bbb81fe7c + github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134905-05231c859625 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 87b85f54..bc1dbb8a 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.86.1-0.20260806134418-f64bbb81fe7c h1:qsnAB/mcaTqA6tSSzlLxoq551Mn4411mNK0Hr3Z3zvQ= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134418-f64bbb81fe7c/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134905-05231c859625 h1:wW0PkVAlGs63OmTnbCtEBnbV7ihTDBktobgAIPSyGh8= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134905-05231c859625/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 2c11de1735add70eaeb4aeb354c3c9c09abd928e Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:45:03 +0000 Subject: [PATCH 10/11] CLI: Update Go SDK to 2bc9fe850fb46a37807363711ee8503e849a8235 Bumps github.com/kernel/kernel-go-sdk to v0.86.1-0.20260806144011-2bc9fe850fb4. The SDK commit range 05231c85..2bc9fe85 contains no source changes (release/CI plumbing only), so there are no new methods or params. Coverage analysis: full enumeration of all 127 SDK methods in api.md against the CLI command tree found no gaps. Every method maps to a command, and every param struct field maps to a flag or positional argument (e.g. HoldKeys -> --hold-key, LastEventID -> --seq, AuditLogListParams.SearchUserID -> --user-id, BrowserCurlParams.TimeoutMs -> --max-time). The 7 x-cli-skip endpoints (auth connection exchange, audit-log export destinations) are absent from the SDK as expected. Tested: go build ./..., go test ./... (all pass), kernel app list against the production 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 709b3e35..68f1ab15 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.86.1-0.20260806134905-05231c859625 + github.com/kernel/kernel-go-sdk v0.86.1-0.20260806144011-2bc9fe850fb4 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 bc1dbb8a..283dabd2 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.86.1-0.20260806134905-05231c859625 h1:wW0PkVAlGs63OmTnbCtEBnbV7ihTDBktobgAIPSyGh8= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260806134905-05231c859625/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806144011-2bc9fe850fb4 h1:6UGIVetH9I/RaEUSq8eVj998+sSY9I6yqWCckBaCG6M= +github.com/kernel/kernel-go-sdk v0.86.1-0.20260806144011-2bc9fe850fb4/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 2f66bf852196c22fcbaa4c9a69c49c1f005dc9c6 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:51:24 +0000 Subject: [PATCH 11/11] CLI: Update Go SDK to v0.86.1 (4511ca3) Bumps github.com/kernel/kernel-go-sdk from v0.86.1-0.20260806144011-2bc9fe850fb4 to v0.86.1 (commit 4511ca3). The SDK change between these two commits is release-only (CHANGELOG.md, README.md, internal/version.go, .release-please-manifest.json). No API surface changed, so there are no new commands or flags to add. Coverage analysis: performed a full enumeration of all 127 SDK methods in api.md and all 95 *Params structs against the existing CLI commands and flags. No gaps found. - All 127 SDK methods have CLI commands. The 7 streaming methods (Auth.Connections.Follow, Browsers.Curl, Browsers.Logs.Stream, Browsers.Process.StdoutStream, Browsers.Telemetry.Stream, Deployments.Follow, Invocations.Follow) are covered via the SDK's *Streaming variants or a raw HTTP client. - All param fields are exposed as flags, positional args, or are intentionally handled (pagination via --page/--per-page, audit-logs via the SDK PageToken auto-pager, and curl via browsers.HTTPClient() for streaming/binary-safe semantics where --max-time covers TimeoutMs). - x-cli-skip endpoints (/auth/connections/{id}/exchange and the /audit-logs/export/destinations family) are absent from the SDK. Tested: go build ./..., go vet ./..., go test ./... (all pass); against the live API: browsers list, profiles list, app list (verified pagination footer and --page/--per-page on profiles and app), browsers create -t 30 -o json, browsers delete (cleaned up). 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 68f1ab15..d1c254bf 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.86.1-0.20260806144011-2bc9fe850fb4 + github.com/kernel/kernel-go-sdk v0.86.1 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 283dabd2..1ad02cea 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.86.1-0.20260806144011-2bc9fe850fb4 h1:6UGIVetH9I/RaEUSq8eVj998+sSY9I6yqWCckBaCG6M= -github.com/kernel/kernel-go-sdk v0.86.1-0.20260806144011-2bc9fe850fb4/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.86.1 h1:Dq2BCjt8FIN0PY30BHIpcZpDW3/puriX5rEhhPnP4Vs= +github.com/kernel/kernel-go-sdk v0.86.1/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=