Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ cannot switch projects.
| `kernel vaults create --name <name>` | Create or retrieve the vault with that immutable name |
| `kernel vaults list` | `--limit 1..100` (default 20), `--offset`, `--query` (name substring or exact ID); JSON includes `vaults` and optional `next_offset` |
| `kernel vaults get <vault>` | Get by ID or name |
| `kernel vaults get-encryption-key <vault>` | Get the public key custom collection apps use to send `encrypted_value` |
| `kernel vaults delete <vault>` | Invalidate the vault and all its items; `--yes` skips confirmation |
| `kernel vaults wallets create <vault> <key> --provider link\|agentcard --spec '<json>'` | Connect/enroll a wallet using its provider's spec; `--open` opens a returned HTTPS action URL |
| `kernel vaults wallets payment-methods <vault> <key>` | Fetch advertised live payment methods; JSON is the item with `expanded.payment_methods` |
Expand Down
34 changes: 34 additions & 0 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ func parseMemoryFlag(memory string) (kernel.BrowserMemoryRequest, error) {
return "", fmt.Errorf("invalid --memory value: %s (must be one of %s)", memory, strings.Join(availableMemorySizes(), ", "))
}

// availableVideoMemorySizes returns the video memory (VRAM) sizes the API
// accepts for a GPU browser session.
func availableVideoMemorySizes() []string {
return []string{"2GiB", "4GiB"}
}

// parseVideoMemoryFlag validates a --video-memory value. An empty value means the
// flag was not set, which lets the API apply its default (2GiB).
func parseVideoMemoryFlag(videoMemory string) (kernel.BrowserVideoMemory, error) {
if videoMemory == "" {
return "", nil
}
for _, m := range availableVideoMemorySizes() {
if strings.EqualFold(videoMemory, m) {
return kernel.BrowserVideoMemory(m), nil
}
}
return "", fmt.Errorf("invalid --video-memory value: %s (must be one of %s)", videoMemory, strings.Join(availableVideoMemorySizes(), ", "))
}

// maxPrivateHosts mirrors the API's cap on network.private_hosts entries.
const maxPrivateHosts = 32

Expand Down Expand Up @@ -468,6 +488,7 @@ type BrowsersCreateInput struct {
Headless BoolFlag
GPU BoolFlag
Memory string
VideoMemory string
InvocationID string
Kiosk BoolFlag
ProfileID string
Expand Down Expand Up @@ -705,6 +726,13 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
if memory != "" {
params.Memory = memory
}
videoMemory, err := parseVideoMemoryFlag(in.VideoMemory)
if err != nil {
return err
}
if videoMemory != "" {
params.VideoMemory = videoMemory
}
if in.InvocationID != "" {
params.InvocationID = kernel.Opt(in.InvocationID)
}
Expand Down Expand Up @@ -972,6 +1000,9 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
tableData = append(tableData, []string{"Stealth", fmt.Sprintf("%t", browser.Stealth)})
tableData = append(tableData, []string{"GPU", fmt.Sprintf("%t", browser.GPU)})
tableData = append(tableData, []string{"Memory", util.OrDash(string(browser.Memory))})
if browser.VideoMemory != "" {
tableData = append(tableData, []string{"Video Memory", string(browser.VideoMemory)})
}
tableData = append(tableData, []string{"Kiosk Mode", fmt.Sprintf("%t", browser.KioskMode)})
if browser.Viewport.Width > 0 && browser.Viewport.Height > 0 {
viewportStr := fmt.Sprintf("%dx%d", browser.Viewport.Width, browser.Viewport.Height)
Expand Down Expand Up @@ -3247,6 +3278,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`,
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("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")
browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session")
Expand Down Expand Up @@ -3387,6 +3419,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
headlessVal, _ := cmd.Flags().GetBool("headless")
gpuVal, _ := cmd.Flags().GetBool("gpu")
memory, _ := cmd.Flags().GetString("memory")
videoMemory, _ := cmd.Flags().GetString("video-memory")
invocationID, _ := cmd.Flags().GetString("invocation-id")
kioskVal, _ := cmd.Flags().GetBool("kiosk")
timeout, _ := cmd.Flags().GetInt("timeout")
Expand Down Expand Up @@ -3544,6 +3577,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal},
GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal},
Memory: memory,
VideoMemory: videoMemory,
InvocationID: invocationID,
Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal},
ProfileID: profileID,
Expand Down
29 changes: 29 additions & 0 deletions cmd/browsers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,35 @@ func TestBrowsersCreate_WithMemory(t *testing.T) {
assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"}))
}

func TestBrowsersCreate_WithVideoMemory(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserNewParams
fake := &FakeBrowsersService{
NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
captured = body
return &kernel.BrowserNewResponse{SessionID: "sess-video-memory"}, nil
},
}
b := BrowsersCmd{browsers: fake}

err := b.Create(context.Background(), BrowsersCreateInput{GPU: BoolFlag{Set: true, Value: true}, VideoMemory: "4gib"})
require.NoError(t, err)
assert.Equal(t, kernel.BrowserVideoMemory4GiB, captured.VideoMemory)

raw, err := captured.MarshalJSON()
require.NoError(t, err)
assert.Contains(t, string(raw), `"video_memory":"4GiB"`)

// Omitting the flag sends nothing; the server defaults to 2GiB.
err = b.Create(context.Background(), BrowsersCreateInput{GPU: BoolFlag{Set: true, Value: true}})
require.NoError(t, err)
assert.Empty(t, captured.VideoMemory)

// An unsupported size is rejected before the request is made.
assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{VideoMemory: "8GiB"}))
}

func TestBrowsersCreate_WithChromePolicy(t *testing.T) {
setupStdoutCapture(t)

Expand Down
8 changes: 8 additions & 0 deletions cmd/vaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ func (c VaultsCmd) Get(ctx context.Context, vault, output string) error {
return printVault(v, output)
}

func (c VaultsCmd) GetEncryptionKey(ctx context.Context, vault, output string) error {
k, err := c.vaults.GetEncryptionKey(ctx, vault, option.WithMaxRetries(0))
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
return printVaultEncryptionKey(k, output)
}

func (c VaultsCmd) List(ctx context.Context, limit, offset int64, query, project, output string) error {
if limit < 1 || limit > 100 || offset < 0 {
return fmt.Errorf("--limit must be between 1 and 100; --offset must be non-negative")
Expand Down
18 changes: 17 additions & 1 deletion cmd/vaults_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,23 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
return getVaultsHandler(cmd).Get(cmd.Context(), args[0], vaultOutput(cmd))
}}
addVaultJSONOutputFlag(get)
cmd.AddCommand(create, list, get, newVaultDeleteCommand(false))

encryptionKey := &cobra.Command{Use: "get-encryption-key <vault>", Short: "Get a vault's public key for browser-encrypted credential values", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun,
Long: `Get the public key that custom credential collection web apps use to encrypt values in the browser.

Use this only if you run your own credential collection web app and want values
encrypted in the browser, sent to your backend still encrypted, and forwarded to
Kernel's API still encrypted. In every other case, including server-side code that
already holds the plaintext, use value. The page encrypts each value to this key as a
compact JWE (alg ECDH-ES, enc A256GCM, kid in the protected header), and your backend
forwards the ciphertext unchanged as encrypted_value in credentials create/update specs.
Each vault has its own key; it is created on first request and may be cached.`,
Example: " kernel vaults get-encryption-key user-vault -o json",
RunE: func(cmd *cobra.Command, args []string) error {
return getVaultsHandler(cmd).GetEncryptionKey(cmd.Context(), args[0], vaultOutput(cmd))
}}
addJSONOutputFlag(encryptionKey)
cmd.AddCommand(create, list, get, encryptionKey, newVaultDeleteCommand(false))

items := &cobra.Command{Use: "items", Short: "Inspect readiness and collection URLs, or invoke collect/fill/webmcp_invoke", Long: "Use get --wait 60 to observe readiness and get -o json for schema/version/presence.\nUse invoke collect to obtain a collection URL, or invoke fill --spec-file to fill a browser.\nUse webmcp invoke to call a live WebMCP tool with item fields bound to null input slots.\n1Password credentials use the advertised 1pw_* operations instead of collect/fill/webmcp_invoke.\nCreate and edit credentials with vaults credentials; payment items use wallets/cards."}
itemList := &cobra.Command{Use: "list <vault>", Short: "List items by vault ID or name", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun,
Expand Down
4 changes: 3 additions & 1 deletion cmd/vaults_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ user-facing collection form renders that order unchanged. Definitions accept a s
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.
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.
Expand Down Expand Up @@ -155,7 +157,7 @@ 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, 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.\nField definitions are immutable. 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")
Expand Down
12 changes: 12 additions & 0 deletions cmd/vaults_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@ func printVault(v *kernel.Vault, output string) error {
return nil
}

// The encryption key is public, so JSON output passes the API response through unchanged.
func printVaultEncryptionKey(k *kernel.VaultEncryptionKey, output string) error {
if output == "json" {
return printVaultJSON(json.RawMessage(k.RawJSON()))
}
PrintTableNoPad(pterm.TableData{
{"Property", "Value"}, {"Key ID (kid)", k.Kid}, {"Algorithm (alg)", string(k.Alg)}, {"Encryption (enc)", string(k.Enc)},
{"JWK kty", k.Jwk.Kty}, {"JWK crv", k.Jwk.Crv}, {"JWK x", k.Jwk.X}, {"JWK y", k.Jwk.Y},
}, true)
return nil
}

func vaultDisplayURL(address string) bool {
u, err := url.Parse(address)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Hostname() == "" || u.User != nil {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.121.1-0.20261009121157-454206ab83bc
github.com/kernel/kernel-go-sdk v0.122.1-0.20261009181557-f737b63ffd16
github.com/klauspost/compress v1.18.5
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pterm/pterm v0.12.80
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.121.1-0.20261009121157-454206ab83bc h1:iIVpTV5HZMbbDp74IB/0z1Qjz9uzL9wpKJRZXoiqPEk=
github.com/kernel/kernel-go-sdk v0.121.1-0.20261009121157-454206ab83bc/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.122.1-0.20261009181557-f737b63ffd16 h1:/cyzSOIuZRlBmYJyXFBE0A62g+BZ+R+UNxA7VmZOJqE=
github.com/kernel/kernel-go-sdk v0.122.1-0.20261009181557-f737b63ffd16/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=
Expand Down
Loading