From 6c94f6ca62cc2cf37321f1eaac9c094e4b515a6c Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:34:47 +0000 Subject: [PATCH 01/16] CLI: Update Go SDK to 99b6c2b2751cf65675a01d172e79a0e58a321155 SDK change is documentation-only (BrowserPlaywrightService.Execute comment). Full enumeration of SDK methods vs CLI commands found no coverage gaps (config-registry endpoints are x-cli-skip). Tested: go build ./... (no new commands/flags to smoke test) Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c29ae9c7..3b13c777 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.123.1-0.20261012183001-8e8343ff3131 + github.com/kernel/kernel-go-sdk v0.123.1-0.20261013222937-99b6c2b2751c 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 a4fdfc2c..80caf535 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.123.1-0.20261012183001-8e8343ff3131 h1:ArVew3GmkyvVSd9yW3gBOktEeejeMc4HRmRWoSSffxY= -github.com/kernel/kernel-go-sdk v0.123.1-0.20261012183001-8e8343ff3131/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.123.1-0.20261013222937-99b6c2b2751c h1:aCwKHu8i724n06ge0LNfDe0tizVVLo16IId4i+dB/Qc= +github.com/kernel/kernel-go-sdk v0.123.1-0.20261013222937-99b6c2b2751c/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 6de9efd29d01a55562095dacd86f3bba82a715dc Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:32:18 +0000 Subject: [PATCH 02/16] CLI: Update Go SDK to 80a3ce316e4f298419b9e14561d12abce778d344 Bump kernel-go-sdk to v0.124.0 (80a3ce3). Full SDK/CLI coverage enumeration found no gaps; no new commands or flags needed. Tested: go build ./... (version bump only, no new commands to smoke test) Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3b13c777..7487d102 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.123.1-0.20261013222937-99b6c2b2751c + github.com/kernel/kernel-go-sdk v0.124.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 80caf535..5aad930b 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.123.1-0.20261013222937-99b6c2b2751c h1:aCwKHu8i724n06ge0LNfDe0tizVVLo16IId4i+dB/Qc= -github.com/kernel/kernel-go-sdk v0.123.1-0.20261013222937-99b6c2b2751c/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.124.0 h1:NU2bO6QVkg8tHfOxOofhGNAajApwdQbWRtSzJKfUyXs= +github.com/kernel/kernel-go-sdk v0.124.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 6ebe6ee5754fa260f65c082c7d4f5fcb2cb0a7ad Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:52:27 +0000 Subject: [PATCH 03/16] CLI: Update SDK to 4ecfaa017984165d5c22b033f5f9a8787e4b5d54 and add API key expiry/policy flags - Bump kernel-go-sdk to 4ecfaa017984165d5c22b033f5f9a8787e4b5d54 - api-keys create: add --expires-at (APIKeyNewParams.ExpiresAt) and repeatable --policy-statement ACTION=RESOURCE (APIKeyNewParams.Policy) - api-keys get/create: show the key's policy (APIKey.Policy) Tested: api-keys create --expires-at, api-keys create --project-id --policy-statement 'browsers:use=projects//*', api-keys get (Policy row rendered), mutual-exclusion and parse-error validation, cleanup via api-keys delete. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++- cmd/api_keys.go | 84 ++++++++++++++++++++++++++++++++++++++------ cmd/api_keys_test.go | 47 +++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- 5 files changed, 127 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8bf3edc5..0d123418 100644 --- a/README.md +++ b/README.md @@ -1428,8 +1428,10 @@ Automated authentication for web services. The `run` command orchestrates the fu - `kernel api-keys create` - Create a new API key - `--name ` - API key name (required) - - `--days-to-expire ` - Number of days until expiry (1-3650); omit for never + - `--days-to-expire ` - Number of days until expiry (1-3650); omit for never. Cannot be combined with `--expires-at` + - `--expires-at ` - Exact expiry time as an RFC 3339 timestamp (e.g. `2026-01-15T09:00:00Z`); must be in the future. Cannot be combined with `--days-to-expire` - `--project-id ` - Create a project-scoped API key for this project ID; omit for org-wide. This is different from global `--project`, which scopes the CLI request to a project ID. + - `--policy-statement ` - Issue a scoped key allowed only the listed actions on the listed resources (repeatable), e.g. `browsers:use=projects/proj_abc123/*`. Everything not listed is denied. - `--output json`, `-o json` - Output raw JSON object, including the one-time plaintext key - `kernel api-keys list` - List API keys diff --git a/cmd/api_keys.go b/cmd/api_keys.go index 33a3344a..f530ec96 100644 --- a/cmd/api_keys.go +++ b/cmd/api_keys.go @@ -3,6 +3,8 @@ package cmd import ( "context" "fmt" + "strings" + "time" "github.com/kernel/cli/pkg/interactive" "github.com/kernel/cli/pkg/util" @@ -28,10 +30,12 @@ type APIKeysCmd struct { } type APIKeysCreateInput struct { - Name string - DaysToExpire Int64Flag - ProjectID string - Output string + Name string + DaysToExpire Int64Flag + ExpiresAt string + ProjectID string + PolicyStatements []string + Output string } type APIKeysListInput struct { @@ -86,9 +90,26 @@ func (c APIKeysCmd) Create(ctx context.Context, in APIKeysCreateInput) error { } params.DaysToExpire = kernel.Int(in.DaysToExpire.Value) } + if in.ExpiresAt != "" { + if in.DaysToExpire.Set { + return fmt.Errorf("--expires-at cannot be combined with --days-to-expire") + } + expiresAt, err := time.Parse(time.RFC3339, in.ExpiresAt) + if err != nil { + return fmt.Errorf("invalid --expires-at value %q: must be an RFC 3339 timestamp (e.g. 2026-01-15T09:00:00Z)", in.ExpiresAt) + } + params.ExpiresAt = kernel.Time(expiresAt) + } if in.ProjectID != "" { params.ProjectID = kernel.String(in.ProjectID) } + if len(in.PolicyStatements) > 0 { + statements, err := parseAPIKeyPolicyStatements(in.PolicyStatements) + if err != nil { + return err + } + params.Policy = kernel.APIKeyNewParamsPolicy{Statements: statements} + } key, err := c.apiKeys.New(ctx, params) if err != nil { @@ -332,6 +353,25 @@ func (c APIKeysCmd) Delete(ctx context.Context, in APIKeysDeleteInput) error { return nil } +// parseAPIKeyPolicyStatements parses ACTION=RESOURCE pairs into policy allow +// statements. Actions never contain '=', so the first '=' is the separator. +func parseAPIKeyPolicyStatements(values []string) ([]kernel.APIKeyNewParamsPolicyStatement, error) { + if len(values) > 256 { + return nil, fmt.Errorf("--policy-statement may be specified at most 256 times") + } + statements := make([]kernel.APIKeyNewParamsPolicyStatement, 0, len(values)) + for _, value := range values { + action, resource, ok := strings.Cut(value, "=") + action = strings.TrimSpace(action) + resource = strings.TrimSpace(resource) + if !ok || action == "" || resource == "" { + return nil, fmt.Errorf("invalid --policy-statement value %q: expected ACTION=RESOURCE (e.g. browsers:use=projects/proj_abc123/*)", value) + } + statements = append(statements, kernel.APIKeyNewParamsPolicyStatement{Action: action, Resource: resource}) + } + return statements, nil +} + func renderCreatedAPIKey(key *kernel.CreatedAPIKey) { rows := pterm.TableData{ {"Field", "Value"}, @@ -342,6 +382,7 @@ func renderCreatedAPIKey(key *kernel.CreatedAPIKey) { {"Project", formatAPIKeyProject(key.APIKey)}, {"Masked Key", key.MaskedKey}, {"Expires At", formatAPIKeyExpiresAt(key.APIKey)}, + {"Policy", formatAPIKeyPolicy(key.APIKey)}, } PrintTableNoPad(rows, true) } @@ -357,6 +398,7 @@ func renderAPIKeyDetails(key *kernel.APIKey) { {"Created By", formatAPIKeyCreator(*key)}, {"Expires At", formatAPIKeyExpiresAt(*key)}, {"Created At", util.FormatLocal(key.CreatedAt)}, + {"Policy", formatAPIKeyPolicy(*key)}, } PrintTableNoPad(rows, true) } @@ -378,6 +420,17 @@ func formatAPIKeyScope(key kernel.APIKey) string { return "Org" } +func formatAPIKeyPolicy(key kernel.APIKey) string { + if !key.JSON.Policy.Valid() || len(key.Policy.Statements) == 0 { + return "Unrestricted" + } + lines := make([]string, 0, len(key.Policy.Statements)) + for _, statement := range key.Policy.Statements { + lines = append(lines, statement.Action+" on "+statement.Resource) + } + return strings.Join(lines, "\n") +} + func formatAPIKeyCreator(key kernel.APIKey) string { if key.CreatedBy.JSON.Name.Valid() && key.CreatedBy.Name != "" { return key.CreatedBy.Name @@ -404,7 +457,9 @@ func runAPIKeysCreate(cmd *cobra.Command, args []string) error { c := getAPIKeysHandler(cmd) name, _ := cmd.Flags().GetString("name") daysToExpire, _ := cmd.Flags().GetInt64("days-to-expire") + expiresAt, _ := cmd.Flags().GetString("expires-at") projectID, _ := cmd.Flags().GetString("project-id") + policyStatements, _ := cmd.Flags().GetStringArray("policy-statement") output, _ := cmd.Flags().GetString("output") return c.Create(cmd.Context(), APIKeysCreateInput{ @@ -413,8 +468,10 @@ func runAPIKeysCreate(cmd *cobra.Command, args []string) error { Set: cmd.Flags().Changed("days-to-expire"), Value: daysToExpire, }, - ProjectID: projectID, - Output: output, + ExpiresAt: expiresAt, + ProjectID: projectID, + PolicyStatements: policyStatements, + Output: output, }) } @@ -489,9 +546,14 @@ var apiKeysCmd = &cobra.Command{ var apiKeysCreateCmd = &cobra.Command{ Use: "create", Short: "Create an API key", - Long: "Create an API key.\n\nBy default the new key is org-wide. Use --project-id to create a key whose own access is scoped to that project. The global --project flag only scopes this CLI request.", - Args: cobra.NoArgs, - RunE: runAPIKeysCreate, + Long: "Create an API key.\n\nBy default the new key is org-wide. Use --project-id to create a key whose own access is scoped to that project. The global --project flag only scopes this CLI request.\n\nUse --policy-statement (repeatable) to issue a scoped key that may only perform the listed actions on the listed resources; every action and resource not listed is denied.", + Example: strings.Join([]string{ + "create --name ci --days-to-expire 30", + "create --name ci --expires-at 2026-01-15T09:00:00Z", + "create --name browser-only --project-id proj_abc123 --policy-statement 'browsers:use=projects/proj_abc123/*'", + }, "\n"), + Args: cobra.NoArgs, + RunE: runAPIKeysCreate, } var apiKeysListCmd = &cobra.Command{ @@ -533,8 +595,10 @@ var apiKeysDeleteCmd = &cobra.Command{ func init() { addJSONOutputFlag(apiKeysCreateCmd) apiKeysCreateCmd.Flags().String("name", "", "API key name (required)") - apiKeysCreateCmd.Flags().Int64("days-to-expire", 0, "Number of days until expiry (1-3650); omit for never") + apiKeysCreateCmd.Flags().Int64("days-to-expire", 0, "Number of days until expiry (1-3650); omit for never. Cannot be combined with --expires-at") + apiKeysCreateCmd.Flags().String("expires-at", "", "Exact expiry time as an RFC 3339 timestamp (e.g. 2026-01-15T09:00:00Z); must be in the future. Cannot be combined with --days-to-expire") apiKeysCreateCmd.Flags().String("project-id", "", "Create a project-scoped API key for this project ID; omit for org-wide") + apiKeysCreateCmd.Flags().StringArray("policy-statement", nil, "Allow statement for a scoped key as ACTION=RESOURCE, e.g. browsers:use=projects/proj_abc123/* (repeatable). Resources are projects/{id}/{kind}/{id}; use projects/{id}/* for everything in a project") _ = apiKeysCreateCmd.MarkFlagRequired("name") addJSONOutputFlag(apiKeysListCmd) diff --git a/cmd/api_keys_test.go b/cmd/api_keys_test.go index 23d12167..3abddcef 100644 --- a/cmd/api_keys_test.go +++ b/cmd/api_keys_test.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "testing" + "time" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" @@ -127,6 +128,52 @@ func TestAPIKeysCreateRejectsInvalidDaysToExpire(t *testing.T) { assert.Contains(t, err.Error(), "--days-to-expire must be between 1 and 3650") } +func TestAPIKeysCreateBuildsExpiresAtAndPolicy(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeAPIKeysService{ + NewFunc: func(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error) { + assert.False(t, body.DaysToExpire.Valid()) + assert.True(t, body.ExpiresAt.Valid()) + assert.Equal(t, time.Date(2030, 1, 15, 9, 0, 0, 0, time.UTC), body.ExpiresAt.Value.UTC()) + require.Len(t, body.Policy.Statements, 2) + assert.Equal(t, "browsers:use", body.Policy.Statements[0].Action) + assert.Equal(t, "projects/proj_123/browsers/*", body.Policy.Statements[0].Resource) + assert.Equal(t, "vaults:read", body.Policy.Statements[1].Action) + assert.Equal(t, "projects/proj_123/vaults/v=1", body.Policy.Statements[1].Resource) + return createdAPIKeyFromJSON(`{"id":"key_123","name":"ci","key":"sk_live_123","masked_key":"sk_...123","created_at":"2026-05-27T12:00:00Z","created_by":{"id":"user_123"},"expires_at":"2030-01-15T09:00:00Z","project_id":"proj_123","project_name":"Prod","policy":{"statements":[{"action":"browsers:use","resource":"projects/proj_123/browsers/*"}]}}`), nil + }, + } + c := APIKeysCmd{apiKeys: fake} + + err := c.Create(context.Background(), APIKeysCreateInput{ + Name: "ci", + ExpiresAt: "2030-01-15T09:00:00Z", + PolicyStatements: []string{"browsers:use=projects/proj_123/browsers/*", "vaults:read=projects/proj_123/vaults/v=1"}, + }) + require.NoError(t, err) + assert.Contains(t, buf.String(), "browsers:use on projects/proj_123/browsers/*") +} + +func TestAPIKeysCreateRejectsInvalidExpiresAtAndPolicy(t *testing.T) { + c := APIKeysCmd{apiKeys: &FakeAPIKeysService{}} + + err := c.Create(context.Background(), APIKeysCreateInput{Name: "ci", ExpiresAt: "tomorrow"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid --expires-at") + + err = c.Create(context.Background(), APIKeysCreateInput{ + Name: "ci", + ExpiresAt: "2030-01-15T09:00:00Z", + DaysToExpire: Int64Flag{Set: true, Value: 30}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot be combined") + + err = c.Create(context.Background(), APIKeysCreateInput{Name: "ci", PolicyStatements: []string{"browsers:use"}}) + require.Error(t, err) + assert.Contains(t, err.Error(), "expected ACTION=RESOURCE") +} + func TestAPIKeysRejectInvalidOutputBeforeCallingAPI(t *testing.T) { fake := &FakeAPIKeysService{ NewFunc: func(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error) { diff --git a/go.mod b/go.mod index 7487d102..5512fc17 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.124.0 + github.com/kernel/kernel-go-sdk v0.124.1-0.20261013234425-4ecfaa017984 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 5aad930b..e23ae94a 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.124.0 h1:NU2bO6QVkg8tHfOxOofhGNAajApwdQbWRtSzJKfUyXs= -github.com/kernel/kernel-go-sdk v0.124.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.124.1-0.20261013234425-4ecfaa017984 h1:hcO2DiAd4ZPM4QbPeyLbsF4s1v2bpdNlAWB7OsaavqA= +github.com/kernel/kernel-go-sdk v0.124.1-0.20261013234425-4ecfaa017984/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 1ecbae5361ec3aaf3357a9f66e9cd202fddc6080 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:01:28 +0000 Subject: [PATCH 04/16] CLI: Update Go SDK to 7aac51875bc3041bcc5975f6eaddff69de7df30b SDK change is release metadata only (v0.125.0); full enumeration of api.md methods vs CLI commands found no coverage gaps. Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5512fc17..e3253da1 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.124.1-0.20261013234425-4ecfaa017984 + github.com/kernel/kernel-go-sdk v0.124.1-0.20261014000000-7aac51875bc3 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 e23ae94a..575881a8 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.124.1-0.20261013234425-4ecfaa017984 h1:hcO2DiAd4ZPM4QbPeyLbsF4s1v2bpdNlAWB7OsaavqA= -github.com/kernel/kernel-go-sdk v0.124.1-0.20261013234425-4ecfaa017984/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.124.1-0.20261014000000-7aac51875bc3 h1:6ccNn4IOPFjUdzeCCxPLAstb3uAozh+ArMkmRdo9h6c= +github.com/kernel/kernel-go-sdk v0.124.1-0.20261014000000-7aac51875bc3/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 5f16ea44948ba001b400dda29b5658873a05f223 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:20:51 +0000 Subject: [PATCH 05/16] CLI: Update SDK to e7ab5d320b3e and add vault authorize checkout context - Bump kernel-go-sdk to e7ab5d320b3e3324e4c73394d4506ed4e5059495 - vaults items invoke authorize now accepts --params/--spec-file with browser_id and page_url (AuthorizeVaultItemOperationRequestParam.BrowserID, .PageURL); both together or neither; rejected locally for kernel cards - fill fields are now optional (FillVaultItemOperationRequestParam.Fields no longer required); up to 32 bindings when the item requires them - Surface wallet item description (VaultItemWallet.Description) in items get output (json and text) Tested: go test ./...; built binary and checked help text, authorize param validation (browser_id without page_url rejected locally; valid params reach the API), fill without fields reaches the API, vaults items get json against the real API. Live Link authorize was not exercised to avoid initiating real payments. Co-Authored-By: Claude Opus 5.5 --- cmd/vaults.go | 5 +++ cmd/vaults_authorize_test.go | 61 ++++++++++++++++++++++++++++++++++ cmd/vaults_commands.go | 13 +++++--- cmd/vaults_operation_params.go | 58 ++++++++++++++++++++++++++++---- cmd/vaults_output.go | 7 ++-- go.mod | 2 +- go.sum | 4 +-- 7 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 cmd/vaults_authorize_test.go diff --git a/cmd/vaults.go b/cmd/vaults.go index dc75a9ec..a494b365 100644 --- a/cmd/vaults.go +++ b/cmd/vaults.go @@ -335,6 +335,11 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par return fmt.Errorf("prepare_checkout requires an AgentCard card") } request.OfPrepareCheckout = &kernel.PrepareCheckoutVaultItemOperationRequestParam{Type: "prepare_checkout", Checkout: *params.Checkout} + } else if params != nil && params.Authorize != nil { + if item.Spec.Provider == "kernel" && (params.Authorize.BrowserID.Valid() || params.Authorize.PageURL.Valid()) { + return fmt.Errorf("kernel cards do not accept browser_id or page_url; authorize was not invoked") + } + request.OfAuthorize = params.Authorize } else if operation == "collect" { request.OfCollect = &kernel.CollectVaultItemOperationRequestParam{Type: "collect"} } else { diff --git a/cmd/vaults_authorize_test.go b/cmd/vaults_authorize_test.go new file mode 100644 index 00000000..dacca6af --- /dev/null +++ b/cmd/vaults_authorize_test.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVaultInvokeAuthorizeSendsCheckoutContext(t *testing.T) { + t.Setenv("KERNEL_PROJECT", "") + for name, tc := range map[string]struct{ params, body string }{ + "checkout": {`{"browser_id":"browser-1","page_url":"https://shop.example/checkout"}`, `{"type":"authorize","browser_id":"browser-1","page_url":"https://shop.example/checkout"}`}, + "none": {"", `{"type":"authorize"}`}, + } { + t.Run(name, func(t *testing.T) { + calls := 0 + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + calls++ + if calls == 2 { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.JSONEq(t, tc.body, string(body)) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, requestedCardFixture) + }) + args := []string{"vaults", "items", "invoke", "checkout", "order-1", "authorize", "-o", "json"} + if tc.params != "" { + args = append(args, "--params", tc.params) + } + _, _, err := executeVaultCommand(t, client, args...) + require.NoError(t, err) + assert.Equal(t, 2, calls) + }) + } +} + +func TestParseVaultAuthorizeParamsValidates(t *testing.T) { + for raw, want := range map[string]string{ + `{"browser_id":"browser-1"}`: "together", + `{"page_url":"https://shop.example/checkout"}`: "together", + `{"browser_id":"","page_url":"https://shop.example"}`: "browser_id", + `{"browser_id":"b","page_url":"checkout"}`: "page_url", + `{"browser_id":"b","page_url":"https://x","x":1}`: "supported", + } { + _, err := parseVaultOperationParams("authorize", raw, true, false) + require.Error(t, err, raw) + assert.Contains(t, err.Error(), want, raw) + } +} + +func TestParseVaultFillParamsFieldsOptional(t *testing.T) { + params, err := parseVaultOperationParams("fill", `{"browser_id":"browser-1","page_url":"https://shop.example/checkout"}`, true, false) + require.NoError(t, err) + assert.Empty(t, params.Fill.Fields) + _, err = parseVaultOperationParams("fill", `{"browser_id":"browser-1","fields":null}`, true, false) + require.Error(t, err) +} diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index c1246288..59bf00c0 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -189,10 +189,14 @@ each vault has its own key; it is created on first request and may be cached.`, invoke := &cobra.Command{Use: "invoke ", Short: "invoke an operation advertised by an item", Args: cobra.ExactArgs(3), PreRunE: vaultPreRun, Long: `retrieve the item and invoke only an operation listed in available_operations. collect returns a time-scoped url for the full credential form without clearing values. -authorize sends {"type":"authorize"} for payment authorization. +authorize sends {"type":"authorize"} for payment authorization. for a Link card, optionally pass +--params with browser_id and page_url together while the browser is on the final checkout page: +kernel inspects that checkout, chooses how the credential is delivered, and limits fill to that +checkout's origin. omitting both issues a virtual card that fills any page on the merchant origin. +once approval has been requested, browser_id and page_url are ignored. kernel cards do not accept them. read the operation description and follow any approval requirements before invoking. fill requires --params json or --spec-file with browser_id (session id, not name) -and 1-32 ordered fields (field, selector). do not include type, values, or frame ids. +and, when the item's advertised fill operation requires them, up to 32 ordered fields (field, selector). do not include type, values, or frame ids. the vault must already be attached to the browser. page_url selects an existing page; fill never navigates. credentials use declared field names, must omit format, and may omit page_url only when the api can resolve a unique page. totp codes stay server-generated. @@ -282,6 +286,7 @@ JSON {"browser_id":"","tool_ref":"","page_url":"https://example.com/login","input":{"email":null,"password":null},"bindings":[{"field":"email","input_path":"/email"},{"field":"password","input_path":"/password"}]} JSON kernel vaults items invoke checkout order-1 authorize --open + kernel vaults items invoke checkout order-1 authorize --params '{"browser_id":"","page_url":"https://shop.example/checkout"}' kernel vaults items invoke user-vault github 1pw_create_access_request --params '{"reason":"Sign in to GitHub"}' kernel vaults items invoke user-vault github 1pw_access_request_status --params '{"timeout_seconds":60}' kernel vaults items invoke user-vault github 1pw_fill --params '{"browser_id":"","page_url":"https://github.com/login"}' @@ -298,7 +303,7 @@ JSON } if cmd.Flags().Changed("spec-file") { if !vaultOperationTakesParams(args[2]) { - return fmt.Errorf("--spec-file is only supported for fill, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations with parameters") + return fmt.Errorf("--spec-file is only supported for fill, authorize, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations with parameters") } data, err := readVaultJSONFile(cmd, "spec-file") if err != nil { @@ -312,7 +317,7 @@ JSON } return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], params, vaultOutput(cmd), open) }} - invoke.Flags().String("params", "", "operation parameters json for fill, webmcp_invoke, prepare_checkout, confirm_transaction, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file") + invoke.Flags().String("params", "", "operation parameters json for fill, authorize, webmcp_invoke, prepare_checkout, confirm_transaction, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file") invoke.Flags().String("spec-file", "", "operation parameters json file (use '-' for stdin; maximum 128 KiB)") invoke.MarkFlagsMutuallyExclusive("params", "spec-file") invoke.Flags().Bool("open", false, "open a returned https action url; cardholder links are human-only, agents must share privately") diff --git a/cmd/vaults_operation_params.go b/cmd/vaults_operation_params.go index 3f71ca57..9bdb4464 100644 --- a/cmd/vaults_operation_params.go +++ b/cmd/vaults_operation_params.go @@ -13,6 +13,7 @@ import ( type vaultOperationParams struct { Fill *vaultFillParams + Authorize *kernel.AuthorizeVaultItemOperationRequestParam WebMCP *kernel.WebmcpInvokeVaultItemOperationRequestParam Checkout *kernel.VaultCheckoutContextParam ConfirmTransaction *vaultConfirmTransactionParams @@ -26,13 +27,13 @@ func isOnePasswordOperation(operation string) bool { // vaultOperationTakesParams reports whether an operation accepts --params or --spec-file. func vaultOperationTakesParams(operation string) bool { - return operation == "fill" || operation == "webmcp_invoke" || operation == "prepare_checkout" || operation == "confirm_transaction" || (isOnePasswordOperation(operation) && operation != "1pw_recover") + return operation == "fill" || operation == "authorize" || operation == "webmcp_invoke" || operation == "prepare_checkout" || operation == "confirm_transaction" || (isOnePasswordOperation(operation) && operation != "1pw_recover") } type vaultFillParams struct { BrowserID string `json:"browser_id"` PageURL string `json:"page_url,omitempty"` - Fields []vaultFillField `json:"fields"` + Fields []vaultFillField `json:"fields,omitempty"` TimeoutMS *int `json:"timeout_ms,omitempty"` } @@ -142,14 +143,24 @@ func parseVaultOperationParams(operation, raw string, paramsSet, openSet bool) ( } return &vaultOperationParams{WebMCP: webMCP}, nil } + if operation == "authorize" { + if !paramsSet { + return nil, nil + } + authorize, err := parseVaultAuthorizeParams(raw) + if err != nil { + return nil, err + } + return &vaultOperationParams{Authorize: authorize}, nil + } if operation != "fill" { if paramsSet { - return nil, fmt.Errorf("--params is only supported for fill, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations; authorize and restart_verification take no parameters") + return nil, fmt.Errorf("--params is only supported for fill, authorize, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations; restart_verification and collect take no parameters") } return nil, nil } if !paramsSet { - return nil, fmt.Errorf("fill requires --params or --spec-file with browser_id and fields") + return nil, fmt.Errorf("fill requires --params or --spec-file with browser_id and, when the item's fill operation requires them, fields") } fill, err := parseVaultFillParams(raw) if err != nil { @@ -181,9 +192,14 @@ func parseVaultFillParams(raw string) (*vaultFillParams, error) { return nil, fmt.Errorf("timeout_ms must be an integer between 1 and 30000") } } + rawFields, present := object["fields"] + if !present { + // Some items fill without bindings; the advertised operation says when fields are needed. + return ¶ms, nil + } var fields []json.RawMessage - if json.Unmarshal(object["fields"], &fields) != nil || len(fields) < 1 || len(fields) > 32 { - return nil, fmt.Errorf("fields must be an array of 1-32 field bindings") + if json.Unmarshal(rawFields, &fields) != nil || fields == nil || len(fields) > 32 { + return nil, fmt.Errorf("fields must be an array of at most 32 field bindings") } params.Fields = make([]vaultFillField, 0, len(fields)) for i, rawField := range fields { @@ -208,6 +224,36 @@ func parseVaultFillParams(raw string) (*vaultFillParams, error) { return ¶ms, nil } +// parseVaultAuthorizeParams accepts the optional Link checkout context for authorize. +func parseVaultAuthorizeParams(raw string) (*kernel.AuthorizeVaultItemOperationRequestParam, error) { + object, err := vaultParamsObject(raw, "browser_id page_url") + if err != nil { + return nil, err + } + request := &kernel.AuthorizeVaultItemOperationRequestParam{Type: kernel.AuthorizeVaultItemOperationRequestTypeAuthorize} + if len(object) == 0 { + return request, nil + } + rawBrowser, hasBrowser := object["browser_id"] + rawURL, hasURL := object["page_url"] + if !hasBrowser || !hasURL { + return nil, fmt.Errorf("authorize requires browser_id and page_url together, or neither") + } + var browserID, pageURL string + if json.Unmarshal(rawBrowser, &browserID) != nil || strings.TrimSpace(browserID) == "" { + return nil, fmt.Errorf("browser_id must be a non-empty browser session ID, not a name") + } + if json.Unmarshal(rawURL, &pageURL) != nil { + return nil, fmt.Errorf("page_url must be the exact absolute URL of the final checkout page") + } + if u, err := url.ParseRequestURI(pageURL); err != nil || u.Scheme == "" { + return nil, fmt.Errorf("page_url must be the exact absolute URL of the final checkout page") + } + request.BrowserID = kernel.Opt(browserID) + request.PageURL = kernel.Opt(pageURL) + return request, nil +} + func parseOnePasswordOperationParams(operation, raw string) (*kernel.VaultItemPerformOperationParams, error) { allowed := map[string]string{ "1pw_create_access_request": "goal reason keywords", diff --git a/cmd/vaults_output.go b/cmd/vaults_output.go index a3e7f5ef..adc08680 100644 --- a/cmd/vaults_output.go +++ b/cmd/vaults_output.go @@ -37,7 +37,7 @@ var vaultMethodFields = vaultOutputFields{ "capabilities": {"single_use_card": vaultFieldsOf("eligible reasons")}, } var vaultItemFields = vaultOutputFields{ - "id": nil, "key": nil, "type": nil, "version": nil, "created_at": nil, "updated_at": nil, "expires_at": nil, + "id": nil, "key": nil, "type": nil, "version": nil, "created_at": nil, "updated_at": nil, "expires_at": nil, "description": nil, "available_operations": vaultOperationFields, "available_expansions": vaultOperationFields, "action": vaultFieldsOf("name url expires_at instructions"), @@ -335,7 +335,7 @@ func printVaultOperationHints(item *kernel.VaultItemUnion, vault, key, project s } for _, op := range actions.Operations { command := prefix - if vaultOperationTakesParams(op.Type) { + if vaultOperationTakesParams(op.Type) && op.Type != "authorize" { // authorize params are optional command += " --params ''" } pterm.Printf("invoke: %s -- %s %s %s\n", command, vaultShellArgument(vault), vaultShellArgument(key), vaultShellArgument(op.Type)) @@ -493,6 +493,9 @@ func printVaultItemGuidance(item *kernel.VaultItemUnion, actions vaultItemAction } return } + if item.Description != "" { + pterm.Printf("description: %s\n", item.Description) + } if item.Type == "wallet" && item.Spec.Provider == "link" && item.Spec.Authorization.Client.Type == "customer_managed" && item.State.Status == "degraded" { pterm.Warning.Println("imported grant is degraded; there is no in-place reauthorization. import a fresh backend OAuth grant under a new wallet key for new work only. existing cards stay bound to the old wallet; retain them and reconcile uncertain payments before further action.") } diff --git a/go.mod b/go.mod index e3253da1..0b4e291e 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.124.1-0.20261014000000-7aac51875bc3 + github.com/kernel/kernel-go-sdk v0.125.1-0.20261014011053-e7ab5d320b3e 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 575881a8..168bad8b 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.124.1-0.20261014000000-7aac51875bc3 h1:6ccNn4IOPFjUdzeCCxPLAstb3uAozh+ArMkmRdo9h6c= -github.com/kernel/kernel-go-sdk v0.124.1-0.20261014000000-7aac51875bc3/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.125.1-0.20261014011053-e7ab5d320b3e h1:jWgFyHpVyhJYALQk4SpY/QlZab/ZzTv9x4By+3KanSI= +github.com/kernel/kernel-go-sdk v0.125.1-0.20261014011053-e7ab5d320b3e/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 ee04cb91d7a449aec2c8a244a0131bb5b2c7fbfd Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:20:51 +0000 Subject: [PATCH 06/16] CLI: Update Go SDK to 3296561611713cb31143d25f2d7a75beb39a87bb Bump kernel-go-sdk to v0.126.0 (3296561). The SDK diff since e7ab5d320b3e contains only release metadata; full enumeration found no coverage gaps. Tested: go build ./... (version bump only, no new commands/flags) Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b4e291e..899b3c29 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.125.1-0.20261014011053-e7ab5d320b3e + github.com/kernel/kernel-go-sdk v0.126.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 168bad8b..a8ea4ec3 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.125.1-0.20261014011053-e7ab5d320b3e h1:jWgFyHpVyhJYALQk4SpY/QlZab/ZzTv9x4By+3KanSI= -github.com/kernel/kernel-go-sdk v0.125.1-0.20261014011053-e7ab5d320b3e/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.0 h1:uv3jdi4DooDGwZ9oF8WrS0YcakKv3Op/k6B8AlgUgXM= +github.com/kernel/kernel-go-sdk v0.126.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 247c1d1010443c57d9d98a819a69d0efdda7b5c0 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:04:13 +0000 Subject: [PATCH 07/16] CLI: Update SDK to 5fcd5cb3d70a and document credential field add/remove and passkeys Bump kernel-go-sdk to 5fcd5cb3d70a57e4eaa55cf9a4e31d8826a0d4bd, which adds the passkey credential field type and add_fields/remove_fields on credential vault item updates. `vaults credentials update --spec-file` already passes the spec through CredentialVaultItemSpecUpdateParam, so the new fields work as-is; update the help text, example, and README (definitions are no longer immutable; passkey type rules). Also fix the earlier merge of main, which duplicated the authorize field in vaultOperationParams and broke the build. Take main's authorize handling (--browser-id/--page-url from #298), which supersedes this branch's --params variant, and drop its now-obsolete test. Tested: vaults create; vaults credentials create (text/password fields); vaults credentials update with add_fields (totp + passkey), fields values, and remove_fields -> version 2, ready, fields username/otp/pk; vaults items get; vaults delete. go build ./..., go vet ./cmd, go test ./cmd pass. Co-Authored-By: Claude Opus 5.5 --- README.md | 12 ++++--- cmd/vaults.go | 5 --- cmd/vaults_authorize_test.go | 61 ---------------------------------- cmd/vaults_commands.go | 4 +-- cmd/vaults_credentials.go | 22 +++++++----- cmd/vaults_operation_params.go | 45 ++----------------------- cmd/vaults_output.go | 5 +-- go.mod | 2 +- go.sum | 4 +-- 9 files changed, 29 insertions(+), 131 deletions(-) delete mode 100644 cmd/vaults_authorize_test.go diff --git a/README.md b/README.md index 20eb6609..3f9d4b01 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ Use `credentials update --version --spec-file changes.js with a spec such as `{"fields":{"password":{"value":"replacement"}}}`; update specs key `fields` by name rather than using the ordered array. Keep actual secrets in protected files or stdin, never shell arguments. Omission preserves values; -null or an empty string clears supported fields, including required text/email/password fields (returning them to pending collection). The form still requires nonempty required inputs. Field definitions cannot change. Stale versions fail, +null or an empty string clears supported fields, including required text/email/password fields (returning them to pending collection). The form still requires nonempty required inputs. Add fields with `add_fields` (an array of definitions with the same shape and defaults as create; set their values in `fields` in the same update) and delete fields and their values with `remove_fields` (an array of names). Names must be unique after removals, at most 32 fields may result, and at least one must remain. Stale versions fail, without retries. `items invoke collect` reopens the full form without clearing values; compare versions to observe edits to already-ready items. When an update is bound to an earlier read, also pass `--expected-item-id ` to reject a replacement @@ -379,11 +379,13 @@ Do not use credential items to store, collect, or fill credit card data, includi Set `description` to the recognizable site name only, such as `Hacker News`, not `Hacker News sign-in credentials`. Set `sensitive: false` explicitly for ordinary usernames and email addresses. Reserve `sensitive: true` for passwords, API tokens, and TOTP seeds; the omitted default remains true for safety. -Types are `text`, `email`, `password`, and `totp`. TOTP seeds must be provided through -create/update, never the form; only generated codes enter the browser. Unrestricted +Types are `text`, `email`, `password`, `totp`, and `passkey`. TOTP seeds and passkeys must be provided through +create/update, never the form; only generated TOTP codes enter the browser. A passkey value is a JSON-encoded +FIDO Credential Exchange Format passkey object; passkeys are never filled into pages. Password, TOTP, and passkey +fields must be sensitive. Unrestricted browser access can read filled values. CLI JSON output retains explicitly non-sensitive -text/email values, definitions, version, and `has_value`. Sensitive values and TOTP -seeds are omitted. +text/email values, definitions, version, and `has_value`. Sensitive values, TOTP +seeds, and passkeys are omitted. Credential spec input is capped at 128 KiB; write errors are redacted. To reuse a managed auth connection's saved credential, create a credential with diff --git a/cmd/vaults.go b/cmd/vaults.go index d11076b7..79218f87 100644 --- a/cmd/vaults.go +++ b/cmd/vaults.go @@ -342,11 +342,6 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par return fmt.Errorf("prepare_checkout requires an AgentCard card") } request.OfPrepareCheckout = &kernel.PrepareCheckoutVaultItemOperationRequestParam{Type: "prepare_checkout", Checkout: *params.Checkout} - } else if params != nil && params.Authorize != nil { - if item.Spec.Provider == "kernel" && (params.Authorize.BrowserID.Valid() || params.Authorize.PageURL.Valid()) { - return fmt.Errorf("kernel cards do not accept browser_id or page_url; authorize was not invoked") - } - request.OfAuthorize = params.Authorize } else if operation == "collect" { request.OfCollect = &kernel.CollectVaultItemOperationRequestParam{Type: "collect"} } else if params != nil && params.Authorize != nil { diff --git a/cmd/vaults_authorize_test.go b/cmd/vaults_authorize_test.go deleted file mode 100644 index dacca6af..00000000 --- a/cmd/vaults_authorize_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package cmd - -import ( - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestVaultInvokeAuthorizeSendsCheckoutContext(t *testing.T) { - t.Setenv("KERNEL_PROJECT", "") - for name, tc := range map[string]struct{ params, body string }{ - "checkout": {`{"browser_id":"browser-1","page_url":"https://shop.example/checkout"}`, `{"type":"authorize","browser_id":"browser-1","page_url":"https://shop.example/checkout"}`}, - "none": {"", `{"type":"authorize"}`}, - } { - t.Run(name, func(t *testing.T) { - calls := 0 - client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { - calls++ - if calls == 2 { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - assert.JSONEq(t, tc.body, string(body)) - } - w.Header().Set("Content-Type", "application/json") - _, _ = io.WriteString(w, requestedCardFixture) - }) - args := []string{"vaults", "items", "invoke", "checkout", "order-1", "authorize", "-o", "json"} - if tc.params != "" { - args = append(args, "--params", tc.params) - } - _, _, err := executeVaultCommand(t, client, args...) - require.NoError(t, err) - assert.Equal(t, 2, calls) - }) - } -} - -func TestParseVaultAuthorizeParamsValidates(t *testing.T) { - for raw, want := range map[string]string{ - `{"browser_id":"browser-1"}`: "together", - `{"page_url":"https://shop.example/checkout"}`: "together", - `{"browser_id":"","page_url":"https://shop.example"}`: "browser_id", - `{"browser_id":"b","page_url":"checkout"}`: "page_url", - `{"browser_id":"b","page_url":"https://x","x":1}`: "supported", - } { - _, err := parseVaultOperationParams("authorize", raw, true, false) - require.Error(t, err, raw) - assert.Contains(t, err.Error(), want, raw) - } -} - -func TestParseVaultFillParamsFieldsOptional(t *testing.T) { - params, err := parseVaultOperationParams("fill", `{"browser_id":"browser-1","page_url":"https://shop.example/checkout"}`, true, false) - require.NoError(t, err) - assert.Empty(t, params.Fill.Fields) - _, err = parseVaultOperationParams("fill", `{"browser_id":"browser-1","fields":null}`, true, false) - require.Error(t, err) -} diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index 216f381d..3e0b9c9c 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -312,7 +312,7 @@ JSON } if cmd.Flags().Changed("spec-file") { if !vaultOperationTakesParams(args[2]) { - return fmt.Errorf("--spec-file is only supported for fill, authorize, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations with parameters") + return fmt.Errorf("--spec-file is only supported for fill, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations with parameters") } data, err := readVaultJSONFile(cmd, "spec-file") if err != nil { @@ -333,7 +333,7 @@ JSON } return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], params, vaultOutput(cmd), open) }} - invoke.Flags().String("params", "", "operation parameters json for fill, authorize, webmcp_invoke, prepare_checkout, confirm_transaction, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file") + invoke.Flags().String("params", "", "operation parameters json for fill, webmcp_invoke, prepare_checkout, confirm_transaction, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file") invoke.Flags().String("spec-file", "", "operation parameters json file (use '-' for stdin; maximum 128 KiB)") invoke.MarkFlagsMutuallyExclusive("params", "spec-file") invoke.Flags().Bool("open", false, "open a returned https action url; cardholder links are human-only, agents must share privately") diff --git a/cmd/vaults_credentials.go b/cmd/vaults_credentials.go index f67f1c40..85a709ee 100644 --- a/cmd/vaults_credentials.go +++ b/cmd/vaults_credentials.go @@ -106,15 +106,16 @@ fields as an ordered array of named definitions. inspect the website and list fields in its natural top-to-bottom order because the user-facing collection form renders that order unchanged. definitions accept a stable name and an optional non-secret human-readable label; forms fall back to name. updates, -state, and browser fills always use name. field types are text, email, password, and -totp; definitions also accept required, sensitive, and value. +state, and browser fills always use name. field types are text, email, password, totp, +and passkey; definitions also accept required, sensitive, and value. a passkey value is +a json-encoded FIDO CXF passkey object; passkeys are never filled into pages. custom collection web apps that encrypt in the browser may send encrypted_value (a compact jwe to the key from vaults get-encryption-key) instead of value; never both. set description to the recognizable site name only, e.g. "Hacker News", not "Hacker News sign-in credentials". this text is the user-facing form title. set sensitive:false explicitly for ordinary usernames and email addresses. reserve sensitive:true for secrets such as passwords, api tokens, and totp seeds. -password and totp must be sensitive. omitted sensitive defaults to true for safety. +password, totp, and passkey must be sensitive. omitted sensitive defaults to true for safety. omit required values to receive a collection url to present to the user. poll items get --wait 60 until state.status is ready. then choose an advertised operation: - ordinary web forms: items invoke fill writes field values without submitting. @@ -124,9 +125,9 @@ poll items get --wait 60 until state.status is ready. then choose an advertised side effects, and its output may include the supplied values. fill never submits, so a retry is safe. retry only after checking the page, and never in an automatic loop. ready means populated, not a successful login. an agent controlling the browser -can read filled values. totp seeds must not be collected through the hosted form. +can read filled values. totp seeds and passkeys must not be collected through the hosted form. get/list output includes definitions, has_value, and explicitly non-sensitive text/email values. -sensitive values and totp seeds are omitted. +sensitive values, totp seeds, and passkeys are omitted. collection urls are bearer credentials: share only with the intended user. ` + vaultOnePasswordCredentialHelp + ` @@ -140,7 +141,7 @@ func newVaultCredentialsCommand() *cobra.Command { for _, update := range []bool{false, true} { name, short := "create", "create a credential and return its collection url" if update { - name, short = "update", "update credential values or description using an expected version" + name, short = "update", "update credential values, fields, or description using an expected version" } cmd := &cobra.Command{Use: name + " --spec-file ", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun, Long: vaultCredentialHelp, RunE: func(cmd *cobra.Command, args []string) error { @@ -158,11 +159,16 @@ func newVaultCredentialsCommand() *cobra.Command { }, } if update { - cmd.Long += "\nupdate applies to KERNEL-hosted credentials only. update spec fields are an object keyed by field name, not the ordered array used on create.\nupdate preserves omitted fields, replaces nonempty string values (value, or encrypted_value from a browser-encrypting collection app), and clears supported values with null or an empty string. clearing a required text/email/password field returns pending_collection; form submissions still require a nonempty value.\nfield definitions are immutable. do not automatically retry version conflicts." + cmd.Long += "\nupdate applies to KERNEL-hosted credentials only. update spec fields are an object keyed by field name, not the ordered array used on create.\nupdate preserves omitted fields, replaces nonempty string values (value, or encrypted_value from a browser-encrypting collection app), and clears supported values with null or an empty string. clearing a required text/email/password field returns pending_collection; form submissions still require a nonempty value.\nadd_fields appends field definitions (same shape and defaults as create; set their values in fields in the same update) and remove_fields deletes fields by name with their values. names must be unique after removals, at most 32 fields may result, and at least one must remain. a required totp or passkey field needs a value. do not automatically retry version conflicts." cmd.Flags().Int64("version", 0, "expected version from items get (required; never auto-refreshed)") _ = cmd.MarkFlagRequired("version") cmd.Flags().String("expected-item-id", "", "immutable item id from the original read; reject an update if the key now refers to a replacement item") - cmd.Example = " kernel vaults credentials update user-vault login --version 2 --spec-file changes.json" + cmd.Example = ` kernel vaults credentials update user-vault login --version 2 --spec-file changes.json + + # Add a TOTP field and remove an obsolete one + kernel vaults credentials update user-vault login --version 3 --spec-file - <<'JSON' +{"add_fields":[{"name":"otp","type":"totp"}],"fields":{"otp":{"value":""}},"remove_fields":["pin"]} +JSON` } else { cmd.Example = ` # Kernel-hosted collection kernel vaults credentials create user-vault login --spec-file - <<'JSON' diff --git a/cmd/vaults_operation_params.go b/cmd/vaults_operation_params.go index a9db0eb0..9650e261 100644 --- a/cmd/vaults_operation_params.go +++ b/cmd/vaults_operation_params.go @@ -14,7 +14,6 @@ import ( type vaultOperationParams struct { Authorize *kernel.AuthorizeVaultItemOperationRequestParam Fill *vaultFillParams - Authorize *kernel.AuthorizeVaultItemOperationRequestParam WebMCP *kernel.WebmcpInvokeVaultItemOperationRequestParam Checkout *kernel.VaultCheckoutContextParam ConfirmTransaction *vaultConfirmTransactionParams @@ -28,13 +27,13 @@ func isOnePasswordOperation(operation string) bool { // vaultOperationTakesParams reports whether an operation accepts --params or --spec-file. func vaultOperationTakesParams(operation string) bool { - return operation == "fill" || operation == "authorize" || operation == "webmcp_invoke" || operation == "prepare_checkout" || operation == "confirm_transaction" || (isOnePasswordOperation(operation) && operation != "1pw_recover") + return operation == "fill" || operation == "webmcp_invoke" || operation == "prepare_checkout" || operation == "confirm_transaction" || (isOnePasswordOperation(operation) && operation != "1pw_recover") } type vaultFillParams struct { BrowserID string `json:"browser_id"` PageURL string `json:"page_url,omitempty"` - Fields []vaultFillField `json:"fields,omitempty"` + Fields []vaultFillField `json:"fields"` TimeoutMS *int `json:"timeout_ms,omitempty"` } @@ -144,16 +143,6 @@ func parseVaultOperationParams(operation, raw string, paramsSet, openSet bool) ( } return &vaultOperationParams{WebMCP: webMCP}, nil } - if operation == "authorize" { - if !paramsSet { - return nil, nil - } - authorize, err := parseVaultAuthorizeParams(raw) - if err != nil { - return nil, err - } - return &vaultOperationParams{Authorize: authorize}, nil - } if operation != "fill" { if paramsSet { return nil, fmt.Errorf("--params is only supported for fill, webmcp_invoke, prepare_checkout, confirm_transaction, and 1Password operations; authorize takes --browser-id and --page-url, and restart_verification takes no parameters") @@ -225,36 +214,6 @@ func parseVaultFillParams(raw string) (*vaultFillParams, error) { return ¶ms, nil } -// parseVaultAuthorizeParams accepts the optional Link checkout context for authorize. -func parseVaultAuthorizeParams(raw string) (*kernel.AuthorizeVaultItemOperationRequestParam, error) { - object, err := vaultParamsObject(raw, "browser_id page_url") - if err != nil { - return nil, err - } - request := &kernel.AuthorizeVaultItemOperationRequestParam{Type: kernel.AuthorizeVaultItemOperationRequestTypeAuthorize} - if len(object) == 0 { - return request, nil - } - rawBrowser, hasBrowser := object["browser_id"] - rawURL, hasURL := object["page_url"] - if !hasBrowser || !hasURL { - return nil, fmt.Errorf("authorize requires browser_id and page_url together, or neither") - } - var browserID, pageURL string - if json.Unmarshal(rawBrowser, &browserID) != nil || strings.TrimSpace(browserID) == "" { - return nil, fmt.Errorf("browser_id must be a non-empty browser session ID, not a name") - } - if json.Unmarshal(rawURL, &pageURL) != nil { - return nil, fmt.Errorf("page_url must be the exact absolute URL of the final checkout page") - } - if u, err := url.ParseRequestURI(pageURL); err != nil || u.Scheme == "" { - return nil, fmt.Errorf("page_url must be the exact absolute URL of the final checkout page") - } - request.BrowserID = kernel.Opt(browserID) - request.PageURL = kernel.Opt(pageURL) - return request, nil -} - func parseOnePasswordOperationParams(operation, raw string) (*kernel.VaultItemPerformOperationParams, error) { allowed := map[string]string{ "1pw_create_access_request": "goal reason keywords", diff --git a/cmd/vaults_output.go b/cmd/vaults_output.go index a2e478eb..18f3eff6 100644 --- a/cmd/vaults_output.go +++ b/cmd/vaults_output.go @@ -335,7 +335,7 @@ func printVaultOperationHints(item *kernel.VaultItemUnion, vault, key, project s } for _, op := range actions.Operations { command := prefix - if vaultOperationTakesParams(op.Type) && op.Type != "authorize" { // authorize params are optional + if vaultOperationTakesParams(op.Type) { command += " --params ''" } if op.Type == "authorize" && item.Type == "card" && item.Spec.Provider == "link" && item.State.Status == "requested" { @@ -496,9 +496,6 @@ func printVaultItemGuidance(item *kernel.VaultItemUnion, actions vaultItemAction } return } - if item.Description != "" { - pterm.Printf("description: %s\n", item.Description) - } if item.Type == "wallet" && item.Spec.Provider == "link" && item.Spec.Authorization.Client.Type == "customer_managed" && item.State.Status == "degraded" { pterm.Warning.Println("imported grant is degraded; there is no in-place reauthorization. import a fresh backend OAuth grant under a new wallet key for new work only. existing cards stay bound to the old wallet; retain them and reconcile uncertain payments before further action.") } diff --git a/go.mod b/go.mod index 899b3c29..aecc548b 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.126.0 + github.com/kernel/kernel-go-sdk v0.126.1-0.20261014235602-5fcd5cb3d70a 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 a8ea4ec3..c9513f98 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.0 h1:uv3jdi4DooDGwZ9oF8WrS0YcakKv3Op/k6B8AlgUgXM= -github.com/kernel/kernel-go-sdk v0.126.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261014235602-5fcd5cb3d70a h1:HviivgY2n1TxdCFqCFadGQvwsaMttmslvmKMI3GJenQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261014235602-5fcd5cb3d70a/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 e2677a751c23a031d1ae7b49bcd20cf514fa84f9 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:33:29 +0000 Subject: [PATCH 08/16] CLI: Update Go SDK to c01e7cb51570 and refresh Kernel card fill guidance Update kernel-go-sdk to c01e7cb515705a9e8a82a6334b6b88727f649ced (v0.126.1-0.20261015002534-c01e7cb51570). The SDK change is documentation-only (Kernel card details delivered through fill). No new methods or params. CLI help text now matches it: fill delivers card details for a few minutes, submit checkout promptly, repeated fill is safe, and supported cards without an agentic network token are authorized without a one-time code. Coverage: full enumeration of all api.md methods against CLI commands; no gaps (config-registry endpoints are x-cli-skip). Tested: go build, go vet, go test ./cmd; vaults list against the live API; vaults items invoke --help and vaults cards create --help render the updated text. Co-Authored-By: Claude Opus 5.5 --- cmd/vaults_commands.go | 3 ++- cmd/vaults_help.go | 13 ++++++++----- cmd/vaults_output.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index 3e0b9c9c..9ec34d9f 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -216,7 +216,8 @@ any write. enter billing details on the merchant page. the api searches the page and descendant frames, including payment iframes. fill is available for credential items and ready Link or kernel cards when advertised, not AgentCard. Link and kernel cards do not expose aliases or support egress substitution. kernel card fill -is locked to merchant_url's origin and its one-time code expires; submit checkout before expiry. +is locked to merchant_url's origin and delivers card details for a few minutes; submit checkout +promptly after filling. a repeated kernel card fill is safe. fill writes real values into the browser; unrestricted browser/cdp access can read them. fill never explicitly submits forms or clicks buttons, but input/change events may trigger site behavior. completed means fields were filled, not website acceptance, login, or payment success. diff --git a/cmd/vaults_help.go b/cmd/vaults_help.go index 9150a019..2aa5f67e 100644 --- a/cmd/vaults_help.go +++ b/cmd/vaults_help.go @@ -91,11 +91,14 @@ type AgentCardCardSpec = { checkout_origin?: string; // top-level checkout origin for autopilot matching; update omission removes it }; -// one live purchase with a KERNEL-enrolled card. authorize obtains a network token and -// one-time code for fill on merchant_url's origin until expires_at. Mastercard purchases -// need no hosted approval. a Visa purchase returns a spend_approval action: the cardholder -// approves it with a Visa passkey (the link expires after 30 minutes) before the code is -// issued. updates are not supported. +// one live purchase with a KERNEL-enrolled card. authorize, then fill on merchant_url's +// origin, then submit checkout before expires_at (one hour after authorization). fill +// delivers card details for a few minutes; submit promptly. repeated fill is safe and never +// submits. supported cards without an agentic network token are authorized without a +// one-time code. Mastercard purchases need no hosted approval. a Visa purchase returns a +// spend_approval action: the cardholder approves it with a Visa passkey (the link expires +// after 30 minutes) before details are issued. amount, merchant_url and merchant_country +// are recorded but do not restrict browser payment requests. updates are not supported. type KernelCardSpec = { provider: "kernel"; wallet: string; // KERNEL wallet item key diff --git a/cmd/vaults_output.go b/cmd/vaults_output.go index 18f3eff6..b965d4ef 100644 --- a/cmd/vaults_output.go +++ b/cmd/vaults_output.go @@ -539,7 +539,7 @@ func printVaultItemGuidance(item *kernel.VaultItemUnion, actions vaultItemAction if item.Spec.Provider == "kernel" { switch item.State.Status { case "requested", "pending_authorization", "ready": - pterm.Info.Println("Visa may require the returned spend_approval action; share it privately, never open it for the cardholder. ready means a one-time code is available, not that the merchant charged the card. fill and submit checkout before expires_at.") + pterm.Info.Println("Visa may require the returned spend_approval action; share it privately, never open it for the cardholder. ready means card details can be filled, not that the merchant charged the card. fill delivers details for a few minutes; submit checkout promptly after filling and before expires_at.") } switch item.State.Status { case "ready", "declined", "expired", "consumed": diff --git a/go.mod b/go.mod index aecc548b..00854d31 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.126.1-0.20261014235602-5fcd5cb3d70a + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015002534-c01e7cb51570 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 c9513f98..b72a7265 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261014235602-5fcd5cb3d70a h1:HviivgY2n1TxdCFqCFadGQvwsaMttmslvmKMI3GJenQ= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261014235602-5fcd5cb3d70a/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015002534-c01e7cb51570 h1:yBtcbORuAanYI2XECdNEwwY8MzIfLmRwHnhcc5LXdyw= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015002534-c01e7cb51570/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 4f7ad731f491960b44dbac4e65480b51c4513dbb Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:00:54 +0000 Subject: [PATCH 09/16] CLI: Update Go SDK to aaa851dc850d and drop Link card expires_at - Bump kernel-go-sdk to aaa851dc850d81283fbe553be52a759de4842b39 - Remove expires_at from the LinkCardSpec help and README; the API and SDK no longer accept it (mirrors the help/test change in #299) No new SDK methods or param fields; full enumeration found no gaps (config-registry endpoints are x-cli-skip). Tested: go test ./..., vaults cards create --help (LinkCardSpec has no expires_at), vaults list against the real API Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- cmd/vaults_help.go | 1 - cmd/vaults_spec_test.go | 5 +++-- go.mod | 2 +- go.sum | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3f9d4b01..6cb5cdc6 100644 --- a/README.md +++ b/README.md @@ -456,7 +456,7 @@ TypeScript-style types, which must stay in sync with the [API spec](https://api. `merchant_name`, and HTTPS `merchant_url`. Supply two-letter `merchant_country` for Visa. Kernel card updates are unsupported. - **Link card:** include the required fields shown in help. Optional `line_items`, `totals`, - `metadata`, and `expires_at` are supported through JSON. `amount` is at most 500000 minor + and `metadata` are supported through JSON. `amount` is at most 500000 minor units; a virtual card allows at most 50000. - **AgentCard card:** uses `merchant`, not Link's `merchant_name`. Its optional `card_id` selects a vaulted card; otherwise the cardholder selects one at approval. diff --git a/cmd/vaults_help.go b/cmd/vaults_help.go index 2aa5f67e..c330767a 100644 --- a/cmd/vaults_help.go +++ b/cmd/vaults_help.go @@ -78,7 +78,6 @@ type LinkCardSpec = { line_items?: LinkLineItem[]; totals?: LinkTotal[]; metadata?: Record; - expires_at?: number; // int64 }; type AgentCardCardSpec = { diff --git a/cmd/vaults_spec_test.go b/cmd/vaults_spec_test.go index 17392f98..4a9702b6 100644 --- a/cmd/vaults_spec_test.go +++ b/cmd/vaults_spec_test.go @@ -19,7 +19,7 @@ func TestVaultRawSpecForwarding(t *testing.T) { `{}`, `{"custom_option":false,"amount":0,"currency":"USD","metadata":null}`, `{"checkout_origin":"https://shop.example.com","amount":1234}`, - `{"expires_at":9223372036854775807,"line_items":[{"name":"Item","quantity":2,"unit_amount":100,"totals":[{"type":"tax","display_text":"Tax","amount":10}]}],"totals":[],"metadata":{"reference":"order-1"}}`, + `{"line_items":[{"name":"Item","quantity":2,"unit_amount":100,"totals":[{"type":"tax","display_text":"Tax","amount":10}]}],"totals":[],"metadata":{"reference":"order-1"}}`, } { t.Run(path+"/"+provider+"/"+raw, func(t *testing.T) { var expected map[string]json.RawMessage @@ -89,8 +89,9 @@ func TestVaultSpecHelpAndFlags(t *testing.T) { assert.Contains(t, cmd.Example, "--spec '") assert.NotContains(t, cmd.Long, "test: boolean") assert.NotContains(t, cmd.Long, "sandbox/live") + assert.NotContains(t, cmd.Long, "expires_at?:") if strings.HasPrefix(path, "cards") { - for _, field := range []string{"merchant_name:", "merchant:", "line_items?:", "metadata?:", "expires_at?:", "checkout_origin?:", "type LinkLineItem", "type LinkTotal"} { + for _, field := range []string{"merchant_name:", "merchant:", "line_items?:", "metadata?:", "checkout_origin?:", "type LinkLineItem", "type LinkTotal"} { assert.Contains(t, cmd.Long, field) } for _, guidance := range []string{"eligible", "autopilot may not apply", "user approval may", "does not guarantee payment success", "KERNEL does not validate this declaration against the page open in the browser.", "preparation merchant_origin instead", "card updates replace the whole spec", "omitting checkout_origin from an update removes"} { diff --git a/go.mod b/go.mod index 00854d31..1c1724fb 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.126.1-0.20261015002534-c01e7cb51570 + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015175338-aaa851dc850d 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 b72a7265..35ab10d6 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015002534-c01e7cb51570 h1:yBtcbORuAanYI2XECdNEwwY8MzIfLmRwHnhcc5LXdyw= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015002534-c01e7cb51570/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015175338-aaa851dc850d h1:t+ps10BgbcPRiUp89vax+2R3qG6WN+3PcLoZJ5kb0uU= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015175338-aaa851dc850d/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 a1ca56995d50dc44ee042cd76a6d908d49239ab5 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:23:44 +0000 Subject: [PATCH 10/16] CLI: Update Go SDK to c5435759242a and raise Kernel card amount cap to 1000000 - Bump kernel-go-sdk to c5435759242aaa83f6280dcd3506c8dd185752ae - Kernel card spec help: amount is now 1..1000000 minor units ($10,000) - confirm_transaction: accept amounts up to 1000000 minor units, matching ConfirmTransactionVaultItemOperationRequest.amount in the API spec No new SDK methods or param fields; full api.md enumeration found no gaps (config-registry analyses remain x-cli-skip). Tested: go build ./..., go test ./cmd/ (boundary tests moved to 1000000/1000001) Co-Authored-By: Claude Opus 5.5 --- cmd/vaults_commands.go | 2 +- cmd/vaults_confirm_transaction.go | 4 ++-- cmd/vaults_confirm_transaction_test.go | 4 ++-- cmd/vaults_help.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index 9ec34d9f..930c2073 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -242,7 +242,7 @@ preparations are single-use, including after failure or expiry; never retry auto confirm_transaction requires --params or --spec-file without type, with all of: status (APPROVED, DECLINED, PENDING, ERROR, CANCELLED), transaction_type (PURCHASE, AUTHORIZATION, CAPTURE, REFUND, REVERSAL, VERIFICATION, CHARGEBACK, FRAUD), amount -(integer minor units 0..50000, no greater than the approved amount), currency (three +(integer minor units 0..1000000, no greater than the approved amount), currency (three uppercase letters matching the approved purchase currency), and occurred_at (RFC3339). no other fields are accepted. ` + kernelTransactionHelp + `. restart_verification takes no parameters. in recovery_required, ` + vaultRecoveryHelp + ` diff --git a/cmd/vaults_confirm_transaction.go b/cmd/vaults_confirm_transaction.go index 4eb74ad8..e21778af 100644 --- a/cmd/vaults_confirm_transaction.go +++ b/cmd/vaults_confirm_transaction.go @@ -32,8 +32,8 @@ func parseVaultConfirmTransactionParams(raw string) (*vaultConfirmTransactionPar return nil, fmt.Errorf("transaction_type must be PURCHASE, AUTHORIZATION, CAPTURE, REFUND, REVERSAL, VERIFICATION, CHARGEBACK, or FRAUD") } var amount *int64 - if json.Unmarshal(object["amount"], &amount) != nil || amount == nil || *amount < 0 || *amount > 50000 { - return nil, fmt.Errorf("amount must be an integer between 0 and 50000 minor units, no greater than the approved amount") + if json.Unmarshal(object["amount"], &amount) != nil || amount == nil || *amount < 0 || *amount > 1000000 { + return nil, fmt.Errorf("amount must be an integer between 0 and 1000000 minor units, no greater than the approved amount") } params.Amount = *amount if json.Unmarshal(object["currency"], ¶ms.Currency) != nil || !vaultCurrencyPattern.MatchString(params.Currency) { diff --git a/cmd/vaults_confirm_transaction_test.go b/cmd/vaults_confirm_transaction_test.go index 5e726a94..cc352945 100644 --- a/cmd/vaults_confirm_transaction_test.go +++ b/cmd/vaults_confirm_transaction_test.go @@ -78,7 +78,7 @@ func TestVaultConfirmTransactionInvalidParamsDoNotRequest(t *testing.T) { for field, values := range map[string][]string{ "status": {`"approved"`, `"UNKNOWN"`, `42`}, "transaction_type": {`"purchase"`, `"SALE"`, `false`}, - "amount": {`-1`, `50001`, `100.5`, `100.0`, `"100"`, `9223372036854775808`}, + "amount": {`-1`, `1000001`, `100.5`, `100.0`, `"100"`, `9223372036854775808`}, "currency": {`"usd"`, `"US"`, `"USDD"`, `"U1D"`, `123`}, "occurred_at": {`"2026-09-01"`, `"2026-09-01T12:00:00"`, `"not-a-date"`, `42`}, } { @@ -110,7 +110,7 @@ func TestVaultConfirmTransactionInvalidParamsDoNotRequest(t *testing.T) { func TestVaultConfirmTransactionValidValues(t *testing.T) { for _, status := range []string{"APPROVED", "DECLINED", "PENDING", "ERROR", "CANCELLED"} { for _, transactionType := range []string{"PURCHASE", "AUTHORIZATION", "CAPTURE", "REFUND", "REVERSAL", "VERIFICATION", "CHARGEBACK", "FRAUD"} { - for _, amount := range []int{0, 50000} { + for _, amount := range []int{0, 1000000} { raw := fmt.Sprintf(`{"status":%q,"transaction_type":%q,"amount":%d,"currency":"USD","occurred_at":"2026-09-01T12:00:00.123+01:00"}`, status, transactionType, amount) params, err := parseVaultOperationParams("confirm_transaction", raw, true, false) require.NoError(t, err) diff --git a/cmd/vaults_help.go b/cmd/vaults_help.go index c330767a..685c16ed 100644 --- a/cmd/vaults_help.go +++ b/cmd/vaults_help.go @@ -101,7 +101,7 @@ type AgentCardCardSpec = { type KernelCardSpec = { provider: "kernel"; wallet: string; // KERNEL wallet item key - amount: number; // integer minor units; 1..50000 + amount: number; // integer minor units; 1..1000000 currency: string; // iso 4217 three letters merchant_name: string; // 1..255 characters merchant_url: string; // https merchant checkout url; fill is locked to its origin diff --git a/go.mod b/go.mod index 1c1724fb..273963ef 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.126.1-0.20261015175338-aaa851dc850d + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015201259-c5435759242a 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 35ab10d6..996f1138 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015175338-aaa851dc850d h1:t+ps10BgbcPRiUp89vax+2R3qG6WN+3PcLoZJ5kb0uU= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015175338-aaa851dc850d/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015201259-c5435759242a h1:vLBINDj3W3ypjOHknpR0CWwLWBOpsXuIHx84ZP33XXU= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015201259-c5435759242a/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 394ae96ebbe3029b93ae603f318ec5715ff19011 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:23:56 +0000 Subject: [PATCH 11/16] README: raise Kernel card amount cap to 1000000 minor units Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6cb5cdc6..38ae624e 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ TypeScript-style types, which must stay in sync with the [API spec](https://api. `card_enrollment` action collects card details from the cardholder, never from the CLI. - **Link wallet:** supply `authorization: {method: "oauth", client: {type: "kernel_managed"}}`. - **AgentCard wallet:** use `{}` to enroll, or supply `user_id` for a user enrolled in the same organization and provider configuration. -- **Kernel card:** supply `wallet`, `amount` (minor units, at most 50000), `currency`, +- **Kernel card:** supply `wallet`, `amount` (minor units, at most 1000000), `currency`, `merchant_name`, and HTTPS `merchant_url`. Supply two-letter `merchant_country` for Visa. Kernel card updates are unsupported. - **Link card:** include the required fields shown in help. Optional `line_items`, `totals`, @@ -633,7 +633,7 @@ kernel vaults items invoke checkout order-1 confirm_transaction --params '{ `type` or any extra properties. `status` must be `APPROVED`, `DECLINED`, `PENDING`, `ERROR`, or `CANCELLED`. `transaction_type` must be `PURCHASE`, `AUTHORIZATION`, `CAPTURE`, `REFUND`, `REVERSAL`, `VERIFICATION`, `CHARGEBACK`, or `FRAUD`. `amount` is an integer in minor units, -0..50000 and no greater than the approved amount; `currency` is three uppercase letters +0..1000000 and no greater than the approved amount; `currency` is three uppercase letters matching the approved purchase currency; `occurred_at` is the observed outcome's RFC3339 timestamp. The operation is no longer advertised after reporting. Never infer success from fill. `PENDING` means explicit From f2407495aa7338b38a628e9c4b3be485f525a8d8 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:15:33 +0000 Subject: [PATCH 12/16] CLI: Update Go SDK to 4ba1a16b77cf and add browser-pools update --expected-network Bump github.com/kernel/kernel-go-sdk to 4ba1a16b77cf49e77fb755de50602e9ceb96f767. Add `kernel browser-pools update --expected-network ` for BrowserPoolUpdateParams.ExpectedNetwork, an atomic optimistic-concurrency precondition on the pool's network configuration. The JSON object is sent raw so explicit empty lists (distinct from omitted lists to the API) are preserved. A 409 stale_network response prints a hint to re-read the pool. Tested against the live API: - browser-pools update --expected-network '{}' --private-host ... (pool with no network) -> success - browser-pools update --expected-network '{}' --size 2 (network changed) -> 409 stale_network, pool unchanged - browser-pools update --expected-network "" --clear-private-hosts -> success - browser-pools update --expected-network null -> rejected client-side - go test ./... passes Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + cmd/browser_pools.go | 36 ++++++++++++++++++++++++++ cmd/browser_pools_test.go | 53 +++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- 5 files changed, 93 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38ae624e..825ff11e 100644 --- a/README.md +++ b/README.md @@ -984,6 +984,7 @@ exists. - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools update ` - Update pool configuration - Same flags as create (except `--region`, which is fixed at creation and cannot be updated) plus `--clear-profile`, `--clear-proxy`, `--clear-start-url`, `--clear-extensions`, `--clear-chrome-policy`, `--clear-private-hosts`, `--clear-proxy-routes`, and `--clear-allowed-hosts` for removing durable configuration. `--clear-private-hosts` removes the whole network configuration (restoring the default private IP ranges and dropping proxy routes and the allowlist). `--private-host`, `--proxy-route`, `--allowed-host`, `--clear-proxy-routes`, and `--clear-allowed-hosts` replace the pool's whole network configuration, so pass the other network flags to keep them; any network update without `--allowed-host` removes the pool's allowlist. Any change to the pool's allowlist, including removing it, automatically replaces idle browsers; leased browsers keep their allowlist until release, when it is reset to the new list or the browser is replaced. `--fill-rate 0` pauses automatic filling. `--discard-all-idle` discards all idle browsers and refills the pool. `--telemetry`, `--memory`, and other network updates only apply to browsers warmed after the update. + - `--expected-network ` - Optimistic concurrency precondition: pass the pool's network configuration from the last pool response (`browser-pools get -o json`, the `browser_pool_config.network` object), or `'{}'` when it has none. The whole update is rejected with 409 `stale_network` if the saved network differs, checked atomically with the write. Explicit empty lists differ from omitted lists, and proxy route targets must match the saved references (resolved IDs, not equivalent proxy names). Omit for unconditional updates - `--output json`, `-o json` - Output raw JSON object - `kernel browser-pools delete ` - Delete a pool - `--force` - Force delete even if browsers are leased diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index e8881004..8ffa1841 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -3,7 +3,9 @@ package cmd import ( "context" "encoding/json" + "errors" "fmt" + "net/http" "strings" "github.com/kernel/cli/pkg/util" @@ -387,6 +389,7 @@ type BrowserPoolsUpdateInput struct { AllowedHosts []string AllowedHostsProvided bool ClearAllowedHosts bool + ExpectedNetwork string Viewport string ChromePolicy string ChromePolicyFile string @@ -397,6 +400,24 @@ type BrowserPoolsUpdateInput struct { Output string } +// parseExpectedNetwork decodes --expected-network into a raw JSON object. It is +// kept untyped so explicit empty lists, which the API treats differently from +// omitted ones, survive the SDK's omitzero encoding. +func parseExpectedNetwork(data string) (map[string]any, error) { + data = strings.TrimSpace(data) + if data == "" { + return nil, nil + } + network := map[string]any{} + if err := json.Unmarshal([]byte(data), &network); err != nil { + return nil, fmt.Errorf("invalid JSON in --expected-network (must be a JSON object, e.g. '{}'): %w", err) + } + if network == nil { + return nil, fmt.Errorf("invalid --expected-network: must be a JSON object, not null; use '{}' for a pool with no network configuration") + } + return network, nil +} + func validateBrowserPoolUpdateInput(in BrowserPoolsUpdateInput) error { if in.StartURL != "" && in.ClearStartURL { return fmt.Errorf("cannot specify both --start-url and --clear-start-url") @@ -546,9 +567,17 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) params.Network = network } + expectedNetwork, err := parseExpectedNetwork(in.ExpectedNetwork) + if err != nil { + return err + } + extraFields := map[string]any{} // The SDK's omitzero encoder drops empty collections, so explicit clears use // its extra-fields escape hatch to preserve {} and [] on the wire. + if expectedNetwork != nil { + extraFields["expected_network"] = expectedNetwork + } if in.ClearExtensions { extraFields["extensions"] = []kernel.BrowserExtensionParam{} } @@ -589,6 +618,10 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) pool, err := c.client.Update(ctx, in.IDOrName, params) if err != nil { + var apiErr *kernel.Error + if expectedNetwork != nil && errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusConflict { + pterm.Warning.Println("The pool's network configuration no longer matches --expected-network; nothing was updated. Re-read it with 'kernel browser-pools get -o json' and retry.") + } return util.CleanedUpSdkError{Err: err} } @@ -913,6 +946,7 @@ func init() { browserPoolsUpdateCmd.Flags().Bool("clear-proxy-routes", false, "Remove the pool's proxy routes (combine with --private-host or --allowed-host to keep those settings; an omitted --allowed-host removes the pool's allowlist)") browserPoolsUpdateCmd.Flags().StringSlice("allowed-host", nil, "Set or replace the pool's egress allowlist (repeat or comma-separated, max 100), using the same entry rules as 'browsers create --allowed-host'. An empty list is invalid. Changing the allowlist automatically replaces idle browsers; leased browsers keep theirs until release, when it is reset to the new list or the browser is replaced. Network flags replace the pool's whole network configuration, so pass --private-host and --proxy-route too to keep them. Requires proxy v3") browserPoolsUpdateCmd.Flags().Bool("clear-allowed-hosts", false, "Remove the pool's egress allowlist and replace idle browsers (combine with --private-host or --proxy-route to keep those settings)") + browserPoolsUpdateCmd.Flags().String("expected-network", "", "Optimistic concurrency precondition: the pool's network configuration as a JSON object, copied from the last pool response ('browser-pools get -o json'), or '{}' when it has none. The whole update is rejected with 409 stale_network if the saved network differs, checked atomically with the write. Explicit empty lists differ from omitted lists; proxy route targets must match the saved references (resolved IDs, not equivalent proxy names)") browserPoolsUpdateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)") browserPoolsUpdateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object") browserPoolsUpdateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)") @@ -1068,6 +1102,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { clearProxyRoutes, _ := cmd.Flags().GetBool("clear-proxy-routes") allowedHosts, _ := cmd.Flags().GetStringSlice("allowed-host") clearAllowedHosts, _ := cmd.Flags().GetBool("clear-allowed-hosts") + expectedNetwork, _ := cmd.Flags().GetString("expected-network") viewport, _ := cmd.Flags().GetString("viewport") chromePolicy, _ := cmd.Flags().GetString("chrome-policy") chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file") @@ -1104,6 +1139,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { AllowedHosts: allowedHosts, AllowedHostsProvided: cmd.Flags().Changed("allowed-host"), ClearAllowedHosts: clearAllowedHosts, + ExpectedNetwork: expectedNetwork, Viewport: viewport, ChromePolicy: chromePolicy, ChromePolicyFile: chromePolicyFile, diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index 2b48c059..afcd981f 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -797,6 +797,59 @@ func TestBrowserPoolsUpdate_PrivateHostModes(t *testing.T) { } } +func TestBrowserPoolsUpdate_ExpectedNetwork(t *testing.T) { + tests := []struct { + name string + input BrowserPoolsUpdateInput + wantJSON string + }{ + { + name: "no network configuration", + input: BrowserPoolsUpdateInput{Size: 2, ExpectedNetwork: "{}"}, + wantJSON: `"expected_network":{}`, + }, + { + name: "preserves explicit empty lists", + input: BrowserPoolsUpdateInput{AllowedHosts: []string{"example.com"}, ExpectedNetwork: `{"private_hosts":[],"proxy_routes":[{"hosts":["a.example"],"proxy":{"id":"proxy-1"}}]}`}, + wantJSON: `"expected_network":{"private_hosts":[],"proxy_routes":[{"hosts":["a.example"],"proxy":{"id":"proxy-1"}}]}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setupStdoutCapture(t) + var captured kernel.BrowserPoolUpdateParams + fake := &FakeBrowserPoolsService{ + UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + captured = body + return &kernel.BrowserPool{ID: id}, nil + }, + } + tt.input.IDOrName = "pool-network" + require.NoError(t, (BrowserPoolsCmd{client: fake}).Update(context.Background(), tt.input)) + raw, err := captured.MarshalJSON() + require.NoError(t, err) + assert.Contains(t, string(raw), tt.wantJSON) + }) + } +} + +func TestBrowserPoolsUpdate_RejectsInvalidExpectedNetwork(t *testing.T) { + for _, value := range []string{"null", "[]", "{bad"} { + t.Run(value, func(t *testing.T) { + setupStdoutCapture(t) + fake := &FakeBrowserPoolsService{ + UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + t.Fatal("Update should not be called when --expected-network is invalid") + return nil, nil + }, + } + err := (BrowserPoolsCmd{client: fake}).Update(context.Background(), BrowserPoolsUpdateInput{IDOrName: "pool-1", ExpectedNetwork: value}) + assert.ErrorContains(t, err, "--expected-network") + }) + } +} + func TestBrowserPoolsCreate_WithTelemetry(t *testing.T) { setupStdoutCapture(t) diff --git a/go.mod b/go.mod index 273963ef..35d9a46c 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.126.1-0.20261015201259-c5435759242a + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015220752-4ba1a16b77cf 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 996f1138..ff24bc7a 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015201259-c5435759242a h1:vLBINDj3W3ypjOHknpR0CWwLWBOpsXuIHx84ZP33XXU= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015201259-c5435759242a/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015220752-4ba1a16b77cf h1:rQRCTzQSNJ4jlOMSLIjfKUshEszmHftLEp8wdI77VTo= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015220752-4ba1a16b77cf/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 75566ada4da0a200f1f726c34bd31f05ca6f3200 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:59:23 +0000 Subject: [PATCH 13/16] CLI: Update Go SDK to c29cc0a2acc9 and align vault recovery guidance Bump github.com/kernel/kernel-go-sdk to c29cc0a2acc90a2a8cb85009e0b675795f9fbbde. The SDK change is doc-only, but it reflects an API behavior change: Link and Kernel cards in recovery_required for issuance may now be deleted and replaced (deleting cancels the known original request). Update the CLI recovery guidance (items get/invoke warnings, cards create/update help, README) so it no longer says such cards must never be deleted or replaced, and so uncertain updates point to delete-and-recreate instead of "must not be retried". Kernel recovery while reporting a merchant transaction still requires support. Coverage: full enumeration of api.md methods against CLI commands found no gaps (config-registry endpoints are x-cli-skip). No params changed. Tested: go build, go vet, go test ./cmd; vaults cards update --help shows the new guidance; vaults list against the live API. Co-Authored-By: Claude Opus 5.5 --- README.md | 30 +++++++++++++++++++----------- cmd/vaults_commands.go | 5 +++-- cmd/vaults_kernel_errors.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 825ff11e..bc29f037 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,8 @@ TypeScript-style types, which must stay in sync with the [API spec](https://api. cannot be updated. Pending issuance updates preserve omitted optional fields; explicit empty lists clear them. Wallet/provider bindings and unsupported fields cannot change after authorization starts. Checkout cards can be edited between authorizations, but a Link card authorized at a checkout cannot be updated (the API returns 409): delete it and -create a new one. An uncertain update enters `recovery_required` and must not be retried. +create a new one. An uncertain update enters `recovery_required`; delete the card and create a +new one rather than retrying the update. Identical card creation returns its existing state without polling, reauthorizing, or resetting recovery. Link fill is limited to the origin of the checkout the card was authorized at; AgentCard checkout domains remain provider-assigned; Kernel fill is locked to the exact origin @@ -535,10 +536,14 @@ If `restart_verification` is advertised in `available_operations`, invoke with e user approval each time to get a fresh `spend_approval` link. The cardholder must verify and approve spending again; restart grants no spend consent. If the response is unclear, re-get and share any new action; invoke once more only if still advertised with renewed -user approval. Otherwise do not retry payment, delete or replace the item. Contact -support@kernel.sh privately with project, vault, key, status_reason and relevant event -timestamps (never card data, API keys or capability URLs) to reconcile the original -operation and establish whether a new purchase is safe; never assume a new card is safe. +user approval. Otherwise do not retry payment or the uncertain update. A Link or Kernel +card in recovery for issuance, or an AgentCard card whose checkout create returned no +authorization ID, may be deleted after explicit user confirmation and replaced by one new +card under a new key. The cardholder may see an approval request for the original card and +should approve only the new one. If the API refuses deletion (for example, Kernel recovery +while reporting a merchant transaction), contact support@kernel.sh privately with project, +vault, key, status_reason and relevant event timestamps (never card data, API keys or +capability URLs) to reconcile the original operation; never assume a new card is safe. Unresolved child cards can block wallet and vault deletion. Time passing or deletion is not evidence of non-execution. @@ -583,8 +588,10 @@ kernel vaults items invoke checkout order-1 restart_verification --open ``` This rotates the approval link locally without a provider call, only after a classified -initial attestation failure with proof no intent was dispatched. Otherwise do not retry, -delete, or replace the item. Always check `available_operations` rather than assuming +initial attestation failure with proof no intent was dispatched. Otherwise do not retry; +with user confirmation, delete the card and create a new purchase under a new key (the +cardholder approves it separately). Recovery while reporting a merchant transaction cannot +be deleted and must be reconciled with support. Always check `available_operations` rather than assuming the API supports this operation. Human output, JSON output and execution expose only advertised restart_verification during recovery; other operations and stale action URLs are suppressed. @@ -791,9 +798,9 @@ through this endpoint. advertised. In `recovery_required`, the CLI hides and refuses every operation except an advertised `restart_verification`, invoked with explicit user approval each time and new cardholder spend consent. Human and JSON output use the same recovery filtering. -Other operations remain blocked; otherwise contact support@kernel.sh with project, +Other operations remain blocked. Where the API permits it, delete the card after user +confirmation and create a new one; otherwise contact support@kernel.sh with project, vault, key, status_reason and event timestamps (no card data, API keys or capability URLs). -Do not delete or replace unresolved items. Stale action URLs in recovery are not opened; a successful restart returns a fresh approval action. @@ -956,8 +963,9 @@ does not merge the new JSON with the existing spec. Waits are single bounded observations, not readiness guarantees or payment retries. Pending state is returned as-is. Requests are not automatically retried by the vault commands. **Never automatically retry failed, timed-out, rejected, or indeterminate payments.** Inspect -state/events and reconcile the outcome instead. If an AgentCard recovery state explicitly -permits user-confirmed abandonment, delete that card before creating a replacement; deletion +state/events and reconcile the outcome instead. If a recovery state permits user-confirmed +deletion (Link or Kernel issuance, or AgentCard without an authorization ID), delete that +card before creating a replacement; deletion does not prove the original attempt failed. Do not pass card data, OAuth codes/tokens, ciphertext, provider secrets, or sensitive provider responses to the CLI. Complete collection, OAuth, and approval actions through the provider's returned URL/UI; no callback-code command diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index 930c2073..3c5b23b3 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -430,11 +430,12 @@ func newVaultCardCommand(update bool) *cobra.Command { kernel cards cannot be updated. other requested cards accept a replacement spec. pending issuance updates preserve omitted optional fields; explicit empty lists clear them. the api restricts fields after authorization starts; wallet/provider bindings cannot change. an uncertain update -enters recovery_required and must not be retried. checkout cards can be edited +enters recovery_required; delete the card and create a new one rather than retrying +the update. checkout cards can be edited between authorizations, but a Link card authorized at a checkout cannot be updated: delete it and create a new one. identical creates return existing state without resetting it. never reconfigure the same item to retry a failed, timed-out, rejected, or indeterminate payment. -an AgentCard recovery item that permits abandonment must be deleted after explicit user confirmation before creating a replacement. +a recovery item that permits deletion (Link or kernel issuance, or AgentCard without an authorization id) must be deleted after explicit user confirmation before creating a replacement. ` + linkCardFlowHelp + "\n" + vaultSpecHelp + vaultCardSpecHelp, Example: " kernel vaults cards " + use + ` checkout order-1 \ diff --git a/cmd/vaults_kernel_errors.go b/cmd/vaults_kernel_errors.go index fe9d12d2..07a4896c 100644 --- a/cmd/vaults_kernel_errors.go +++ b/cmd/vaults_kernel_errors.go @@ -108,7 +108,7 @@ func retryAfterWait(apiErr *kernel.Error) string { return "wait for the rate limit to clear" } -const kernelSupportGuidance = "do not retry payment, delete, or replace the item. contact support@kernel.sh privately with project, vault, item key, status_reason and relevant event timestamps (never card data, api keys or capability urls) to reconcile the original operation and establish whether a new purchase is safe; do not assume a new card is safe or promise a resulting state" +const kernelSupportGuidance = "do not retry payment or the uncertain update. a Link or kernel card in recovery for issuance, or an AgentCard card whose checkout create returned no authorization id, may be deleted after explicit user confirmation and replaced by one new card under a new key; the cardholder may see an approval request for the original card and should approve only the new one. deletion is not proof the original operation did not occur. if the api refuses deletion (for example, kernel recovery while reporting a merchant transaction), contact support@kernel.sh privately with project, vault, item key, status_reason and relevant event timestamps (never card data, api keys or capability urls) to reconcile the original operation and establish whether a new purchase is safe; do not assume a new card is safe or promise a resulting state" const kernelTransactionHelp = "report one observed merchant outcome only when confirm_transaction is advertised. PENDING means the merchant explicitly confirms an accepted transaction is pending; ERROR means an explicit merchant transaction error, not transport loss or an ambiguous processing page. these are final reports, not polling states. unknown means no reliable merchant outcome: ask the user to check order confirmation, merchant account or statement, then report the confirmed outcome even after card expiry. never infer success from fill. if reporting fails or the response is lost, re-get and send the identical observed report once more only if confirm_transaction is freshly advertised; otherwise observe state/events (recorded, in flight or recovery). never repeat merchant checkout" diff --git a/go.mod b/go.mod index 35d9a46c..2d6037ef 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.126.1-0.20261015220752-4ba1a16b77cf + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015225154-c29cc0a2acc9 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 ff24bc7a..dcfadb45 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015220752-4ba1a16b77cf h1:rQRCTzQSNJ4jlOMSLIjfKUshEszmHftLEp8wdI77VTo= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015220752-4ba1a16b77cf/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015225154-c29cc0a2acc9 h1:+RtabqOGnTm1+5yck3+hX5OaS0uFB4S3GqvKrAHu2ps= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015225154-c29cc0a2acc9/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 334365071d11ae571fd4cfd3d0c62315e6d4befa Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:26:51 +0000 Subject: [PATCH 14/16] CLI: Update Go SDK to a7500dba2721 Bumps github.com/kernel/kernel-go-sdk to a7500dba27215b13133ab42299f42720aaefd952. The SDK change only touches browser telemetry response types (new captcha 'media' field, 'binance' captcha provider value); no new methods or params, so no new CLI commands/flags are needed. Full enumeration of api.md methods vs. CLI commands found no coverage gaps (config-registry endpoints are x-cli-skip). Tested: go build ./... (version bump only; no new commands to smoke test) Co-Authored-By: Claude Opus 5.5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2d6037ef..3b2ad7f1 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.126.1-0.20261015225154-c29cc0a2acc9 + github.com/kernel/kernel-go-sdk v0.126.1-0.20261015232057-a7500dba2721 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 dcfadb45..8138a4cc 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015225154-c29cc0a2acc9 h1:+RtabqOGnTm1+5yck3+hX5OaS0uFB4S3GqvKrAHu2ps= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015225154-c29cc0a2acc9/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015232057-a7500dba2721 h1:daF2LqZxsweqxsfGXJe0QzDMkBOT04srwzm+P2zJY9Y= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261015232057-a7500dba2721/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 07462c3ec2d62c57119793de386f8082d02b746b Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:45:23 +0000 Subject: [PATCH 15/16] Update Go SDK to 126a86281d291ffa25bf281fd172b0663eff09b0 and CLI coverage --- cmd/browsers.go | 8 +++++++- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/browsers.go b/cmd/browsers.go index 47f2e98d..7ef069ac 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -3160,7 +3160,13 @@ func init() { // extensions extensionsRoot := &cobra.Command{Use: "extensions", Short: "Add browser extensions to a running instance"} - extensionsUpload := &cobra.Command{Use: "upload ...", Short: "Upload one or more unpacked extensions and restart Chromium", Args: cobra.MinimumNArgs(2), RunE: runBrowsersExtensionsUpload} + extensionsUpload := &cobra.Command{ + Use: "upload ...", + Short: "Load one or more unpacked extensions into a running browser", + Long: "Load one or more unpacked extensions into a running browser. Eligible extensions are activated live; Chromium restarts only when enterprise policy requires it or live activation fails.", + Args: cobra.MinimumNArgs(2), + RunE: runBrowsersExtensionsUpload, + } extensionsRoot.AddCommand(extensionsUpload) browsersCmd.AddCommand(extensionsRoot) diff --git a/go.mod b/go.mod index 3b2ad7f1..5e98c204 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.126.1-0.20261015232057-a7500dba2721 + github.com/kernel/kernel-go-sdk v0.126.1-0.20261016123619-126a86281d29 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 8138a4cc..7656586e 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015232057-a7500dba2721 h1:daF2LqZxsweqxsfGXJe0QzDMkBOT04srwzm+P2zJY9Y= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261015232057-a7500dba2721/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261016123619-126a86281d29 h1:H7nRfD2Rtcn1aLK/mvMZoHjlPFQtavKxONieV0IXdME= +github.com/kernel/kernel-go-sdk v0.126.1-0.20261016123619-126a86281d29/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 909dccf12824e9377d43474e3e7aadbd7e43da0e Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:17:24 +0000 Subject: [PATCH 16/16] Update Go SDK to 721b8dc53e92d9b7326f30174d71fe5f15c0f21a and CLI coverage --- cmd/browser_pools.go | 4 ++-- cmd/browser_pools_test.go | 38 +++++++++++++++++++++++++++++++++ cmd/browsers.go | 12 +++++------ cmd/browsers_test.go | 24 +++++++++++++++------ cmd/vaults_sdk_contract_test.go | 17 +++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- 7 files changed, 84 insertions(+), 17 deletions(-) diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 8ffa1841..e77da019 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -902,7 +902,7 @@ func init() { browserPoolsCreateCmd.Flags().Bool("stealth", false, "Enable stealth mode") browserPoolsCreateCmd.Flags().Bool("headless", false, "Enable headless mode") browserPoolsCreateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode") - browserPoolsCreateCmd.Flags().String("memory", "", "Memory for headful browsers in the pool: '8GiB' (default) or '16GiB'") + browserPoolsCreateCmd.Flags().String("memory", "", "Memory for non-GPU browsers in the pool: headless supports '1GiB' (default), '2GiB', or '4GiB'; headful supports '8GiB' (default) or '16GiB'") browserPoolsCreateCmd.Flags().Bool("refresh-on-profile-update", false, "Flush idle browsers when the pool's profile is updated") browserPoolsCreateCmd.Flags().String("profile-id", "", "Profile ID") browserPoolsCreateCmd.Flags().String("profile-name", "", "Profile name") @@ -929,7 +929,7 @@ func init() { browserPoolsUpdateCmd.Flags().Bool("stealth", false, "Enable stealth mode") browserPoolsUpdateCmd.Flags().Bool("headless", false, "Enable headless mode") browserPoolsUpdateCmd.Flags().Bool("kiosk", false, "Enable kiosk mode") - browserPoolsUpdateCmd.Flags().String("memory", "", "Memory for newly-warmed headful browsers in the pool: '8GiB' or '16GiB'. Existing browsers keep their allocation; use --discard-all-idle to replace idle browsers") + browserPoolsUpdateCmd.Flags().String("memory", "", "Memory for newly-warmed non-GPU browsers: headless supports '1GiB', '2GiB', or '4GiB'; headful supports '8GiB' or '16GiB'. Existing browsers keep their allocation; use --discard-all-idle to replace idle browsers") browserPoolsUpdateCmd.Flags().Bool("refresh-on-profile-update", false, "Flush idle browsers when the pool's profile is updated") browserPoolsUpdateCmd.Flags().String("profile-id", "", "Profile ID") browserPoolsUpdateCmd.Flags().String("profile-name", "", "Profile name") diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index afcd981f..26c186b9 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -189,6 +189,44 @@ func TestBrowserPoolsCreate_WithRegion(t *testing.T) { assert.Error(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Region: "emea"})) } +func TestBrowserPoolsMemorySizes(t *testing.T) { + setupStdoutCapture(t) + + var created kernel.BrowserPoolNewParams + var updated kernel.BrowserPoolUpdateParams + fake := &FakeBrowserPoolsService{ + NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + created = body + return &kernel.BrowserPool{ID: "pool-memory"}, nil + }, + UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + updated = body + return &kernel.BrowserPool{ID: id}, nil + }, + } + c := BrowserPoolsCmd{client: fake} + + for _, tc := range []struct { + input string + want kernel.BrowserMemoryRequest + }{ + {input: "1gib", want: kernel.BrowserMemoryRequest1GiB}, + {input: "2GiB", want: kernel.BrowserMemoryRequest2GiB}, + {input: "4GIB", want: kernel.BrowserMemoryRequest4GiB}, + {input: "8GiB", want: kernel.BrowserMemoryRequest8GiB}, + {input: "16GiB", want: kernel.BrowserMemoryRequest16GiB}, + } { + require.NoError(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Memory: tc.input})) + assert.Equal(t, tc.want, created.Memory) + + require.NoError(t, c.Update(context.Background(), BrowserPoolsUpdateInput{IDOrName: "pool-memory", Memory: tc.input})) + assert.Equal(t, kernel.BrowserPoolUpdateParamsMemory(tc.want), updated.Memory) + } + + assert.Error(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Memory: "6GiB"})) + assert.Error(t, c.Update(context.Background(), BrowserPoolsUpdateInput{IDOrName: "pool-memory", Memory: "6GiB"})) +} + func TestBrowserPoolsGet_ShowsRegion(t *testing.T) { setupStdoutCapture(t) diff --git a/cmd/browsers.go b/cmd/browsers.go index 7ef069ac..82d42068 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -180,15 +180,15 @@ func parseRegionFlag(region string) (string, error) { return "", fmt.Errorf("invalid --region value: %s (must be one of %s)", region, strings.Join(availableRegions(), ", ")) } -// availableMemorySizes returns the memory sizes the API accepts when creating a -// browser session. Only headful, non-GPU sessions can request memory; every -// other configuration gets a fixed allocation. +// availableMemorySizes returns the memory sizes the API accepts for non-GPU +// browser sessions. Headless and headful sessions support different subsets. func availableMemorySizes() []string { - return []string{"8GiB", "16GiB"} + return []string{"1GiB", "2GiB", "4GiB", "8GiB", "16GiB"} } // parseMemoryFlag validates a --memory value. An empty value means the flag was -// not set, which lets the API apply its default (8GiB). +// not set, which lets the API apply the image default (1GiB for headless or 8GiB +// for headful). func parseMemoryFlag(memory string) (kernel.BrowserMemoryRequest, error) { if memory == "" { return "", nil @@ -3292,7 +3292,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`, browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection") browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access") browsersCreateCmd.Flags().Bool("gpu", false, "Launch browser with hardware-accelerated GPU rendering") - browsersCreateCmd.Flags().String("memory", "", "Memory for a headful, non-GPU browser session: '8GiB' (default) or '16GiB'") + browsersCreateCmd.Flags().String("memory", "", "Memory for a non-GPU browser: headless supports '1GiB' (default), '2GiB', or '4GiB'; headful supports '8GiB' (default) or '16GiB'") browsersCreateCmd.Flags().String("video-memory", "", "Video memory (VRAM) for a GPU browser session (requires --gpu): '2GiB' (default, 4 vCPU/6GiB memory) or '4GiB' (8 vCPU/12GiB memory)") browsersCreateCmd.Flags().String("invocation-id", "", "Associate the browser session with an invocation") browsersCreateCmd.Flags().Bool("kiosk", false, "Launch browser in kiosk mode") diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index a3def713..28da45c0 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -937,12 +937,24 @@ func TestBrowsersCreate_WithMemory(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(raw), `"memory":"16GiB"`) - // Values are normalized to the casing the API expects. - err = b.Create(context.Background(), BrowsersCreateInput{Memory: "8gib"}) - require.NoError(t, err) - assert.Equal(t, kernel.BrowserMemoryRequest8GiB, captured.Memory) + // Values are normalized to the casing the API expects, including the smaller + // allocations available to headless browsers. + for _, tc := range []struct { + input string + want kernel.BrowserMemoryRequest + }{ + {input: "1gib", want: kernel.BrowserMemoryRequest1GiB}, + {input: "2GiB", want: kernel.BrowserMemoryRequest2GiB}, + {input: "4GIB", want: kernel.BrowserMemoryRequest4GiB}, + {input: "8gib", want: kernel.BrowserMemoryRequest8GiB}, + } { + err = b.Create(context.Background(), BrowsersCreateInput{Memory: tc.input}) + require.NoError(t, err) + assert.Equal(t, tc.want, captured.Memory) + } - // Omitting the flag sends nothing; the server defaults to 8GiB. + // Omitting the flag sends nothing; the server selects the image default (1GiB + // for headless or 8GiB for headful). err = b.Create(context.Background(), BrowsersCreateInput{}) require.NoError(t, err) assert.Empty(t, captured.Memory) @@ -952,7 +964,7 @@ func TestBrowsersCreate_WithMemory(t *testing.T) { assert.NotContains(t, string(raw), "memory") // An unsupported size is rejected before the request is made. - assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"})) + assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "6GiB"})) } func TestBrowsersCreate_WithVideoMemory(t *testing.T) { diff --git a/cmd/vaults_sdk_contract_test.go b/cmd/vaults_sdk_contract_test.go index cee81edf..7230f6c3 100644 --- a/cmd/vaults_sdk_contract_test.go +++ b/cmd/vaults_sdk_contract_test.go @@ -64,6 +64,23 @@ func TestCredentialUpdateIdentityPrecondition(t *testing.T) { } } +func TestCredentialUpdateFieldDefinitionsWithGeneratedSDK(t *testing.T) { + t.Setenv("KERNEL_PROJECT", "") + spec := `{"add_fields":[{"name":"site_passkey","type":"passkey","required":true,"sensitive":true}],"fields":{"site_passkey":{"value":"synthetic-cxf-passkey"}},"remove_fields":["password"]}` + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PATCH", r.Method) + var body map[string]json.RawMessage + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.JSONEq(t, spec, string(body["spec"])) + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, credentialFixture) + }) + + out, _, err := executeVaultCommand(t, client, "vaults", "credentials", "update", "user-123", "login", "--version", "7", "--spec-file", credentialSpecFile(t, spec), "-o", "json") + require.NoError(t, err) + assert.NotContains(t, out, "synthetic-cxf-passkey") +} + func TestCredentialInitialValuesWithGeneratedSDK(t *testing.T) { t.Setenv("KERNEL_PROJECT", "") spec := `{"description":"Example","fields":[{"name":"username","label":"Membership Number or Username","type":"text","sensitive":false,"value":"synthetic-user"},{"name":"email","type":"email","sensitive":false,"value":"test@example.com"},{"name":"password","type":"password","sensitive":true,"value":"synthetic-password"},{"name":"otp","type":"totp","sensitive":true,"value":"JBSWY3DPEHPK3PXP"}]}` diff --git a/go.mod b/go.mod index 5e98c204..59945477 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.126.1-0.20261016123619-126a86281d29 + github.com/kernel/kernel-go-sdk v0.127.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 7656586e..59fd599c 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261016123619-126a86281d29 h1:H7nRfD2Rtcn1aLK/mvMZoHjlPFQtavKxONieV0IXdME= -github.com/kernel/kernel-go-sdk v0.126.1-0.20261016123619-126a86281d29/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.127.0 h1:q/lOm5JcuG7jhJpDq37DS5IhcDJBnV35btgqUb8LThM= +github.com/kernel/kernel-go-sdk v0.127.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=