Skip to content
Open
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
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Commands with JSON output support:
- **Proxies**: `create`, `list`, `get`, `update`, `check`
- **API Keys**: `create`, `list`, `get`, `update`, `rotate`
- **Auth Connections**: `timeline`
- **Vaults**: `create`, `list`, `get`, `credentials create/update`, `items list/get/events/invoke` (including `collect`, `fill`, and `prepare_checkout`), `wallets create/payment-methods`, `cards create/update` (display-safe public fields only)
- **Vaults**: `create`, `list`, `get`, `credentials create/update`, `items list/get/events/invoke` (including `collect`, `fill`, `webmcp_invoke`, and `prepare_checkout`), `wallets create/payment-methods`, `cards create/update` (display-safe public fields only)
- **Projects**: `update`
- **Org**: `limits get/set`
- **Apps**: `list`, `history`
Expand Down Expand Up @@ -383,6 +383,17 @@ text/email values, definitions, version, and `has_value`. Sensitive values and T
seeds 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
provider `managed_auth` and the connection ID from `kernel auth connections list`.
The item stores no values and reads the connection's credential at fill time; it is
created `ready`, `state.fields` lists fill binding names, and `update` returns 409:

```sh
kernel vaults credentials create user-vault amazon --spec-file - <<'JSON'
{"provider":"managed_auth","connection_id":"<connection-id>","description":"Amazon"}
JSON
```

Vault names, item keys, and project ownership are immutable. Optionally select a project with
`--project <id-or-name>` or `KERNEL_PROJECT`; otherwise, the API resolves the project from your
credentials and its defaults (the default project for org-wide credentials, not all projects).
Expand All @@ -394,7 +405,7 @@ cannot switch projects.
| Command | Purpose / flags |
| --- | --- |
| `kernel vaults create --name <name>` | Create or retrieve the vault with that immutable name |
| `kernel vaults list` | `--limit 1..100` (default 20), `--offset`; JSON includes `vaults` and optional `next_offset` |
| `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 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 |
Expand Down
41 changes: 39 additions & 2 deletions cmd/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,13 @@ func (c CredentialsCmd) Get(ctx context.Context, in CredentialsGetInput) error {
{"Name", cred.Name},
{"Domain", cred.Domain},
{"Has TOTP Secret", hasTOTP},
}
tableData = append(tableData, credentialTotpRows(cred)...)
tableData = append(tableData, pterm.TableData{
{"SSO Provider", ssoProvider},
{"Created At", util.FormatLocal(cred.CreatedAt)},
{"Updated At", util.FormatLocal(cred.UpdatedAt)},
}
}...)

PrintTableNoPad(tableData, true)
return nil
Expand Down Expand Up @@ -276,8 +279,9 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e
{"Name", cred.Name},
{"Domain", cred.Domain},
{"Has TOTP Secret", hasTOTP},
{"SSO Provider", ssoProvider},
Comment thread
cursor[bot] marked this conversation as resolved.
}
tableData = append(tableData, credentialTotpRows(cred)...)
tableData = append(tableData, []string{"SSO Provider", ssoProvider})

PrintTableNoPad(tableData, true)

Expand All @@ -289,6 +293,36 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e
return nil
}

// normalizeTotpAlgorithm validates a TOTP HMAC algorithm and returns its
// canonical upper-case form (SHA1, SHA256, or SHA512).
func normalizeTotpAlgorithm(algorithm string) (string, error) {
normalized := strings.ToUpper(strings.TrimSpace(algorithm))
switch normalized {
case "SHA1", "SHA256", "SHA512":
return normalized, nil
default:
return "", fmt.Errorf("invalid --totp-algorithm %q (must be one of SHA1, SHA256, SHA512)", algorithm)
}
}

// credentialTotpRows returns TOTP metadata rows for credentials with a TOTP secret.
func credentialTotpRows(cred *kernel.Credential) pterm.TableData {
if !cred.HasTotpSecret {
return nil
}
rows := pterm.TableData{}
if cred.TotpAlgorithm != "" {
rows = append(rows, []string{"TOTP Algorithm", string(cred.TotpAlgorithm)})
}
if cred.TotpDigits > 0 {
rows = append(rows, []string{"TOTP Digits", fmt.Sprintf("%d", cred.TotpDigits)})
}
if cred.TotpPeriod > 0 {
rows = append(rows, []string{"TOTP Period", fmt.Sprintf("%ds", cred.TotpPeriod)})
}
return rows
}

Comment thread
cursor[bot] marked this conversation as resolved.
func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) error {
if err := validateJSONOutput(in.Output); err != nil {
return err
Expand Down Expand Up @@ -427,6 +461,9 @@ Examples:
# Create a credential with TOTP for 2FA
kernel credentials create --name "my-2fa-site" --domain "example.com" --value "username=myuser" --value "password=mypass" --totp-secret "JBSWY3DPEHPK3PXP"

# Create a credential with custom TOTP parameters
kernel credentials create --name "my-8digit-site" --domain "example.com" --value "username=myuser" --totp-secret "JBSWY3DPEHPK3PXP" --totp-algorithm SHA256 --totp-digits 8 --totp-period 60

Comment thread
cursor[bot] marked this conversation as resolved.
# Create a credential with SSO provider
kernel credentials create --name "google-sso" --domain "example.com" --value "email=user@gmail.com" --value "password=mypass" --sso-provider google`,
Args: cobra.NoArgs,
Expand Down
19 changes: 19 additions & 0 deletions cmd/credentials_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNormalizeTotpAlgorithm(t *testing.T) {
for input, want := range map[string]string{"SHA1": "SHA1", "sha256": "SHA256", " Sha512 ": "SHA512"} {
got, err := normalizeTotpAlgorithm(input)
require.NoError(t, err)
assert.Equal(t, want, got)
}

_, err := normalizeTotpAlgorithm("md5")
assert.Error(t, err)
}
7 changes: 6 additions & 1 deletion cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ func runLogs(cmd *cobra.Command, args []string) error {
pterm.Info.Println("Showing recent logs (timeout after 3s with no events)")
}

stream := client.Invocations.FollowStreaming(cmd.Context(), inv.ID, kernel.InvocationFollowParams{}, option.WithMaxRetries(0))
// Only forward --since when explicitly set so older invocations still show their full logs
invParams := kernel.InvocationFollowParams{}
if cmd.Flags().Changed("since") {
invParams.Since = kernel.Opt(since)
}
stream := client.Invocations.FollowStreaming(cmd.Context(), inv.ID, invParams, option.WithMaxRetries(0))
if stream.Err() != nil {
return fmt.Errorf("failed to follow streaming: %w", stream.Err())
}
Expand Down
15 changes: 14 additions & 1 deletion cmd/offset_pagination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestOffsetPaginationListCommands(t *testing.T) {
case "projects":
err = (ProjectsCmd{projects: &client.Projects}).List(context.Background(), ProjectsListInput{Limit: 20, Offset: 20, Output: "json"})
case "vaults":
err = (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 20, "", "json")
err = (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 20, "", "", "json")
case "vault-provider-configs":
err = (VaultProviderConfigsCmd{configs: &client.VaultProviderConfigs}).List(context.Background(), 20, 20, "json")
}
Expand All @@ -87,3 +87,16 @@ func TestOffsetPaginationListCommands(t *testing.T) {
}
}
}

func TestVaultsListQuery(t *testing.T) {
client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "my vault", r.URL.Query().Get("query"))
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Has-More", "true")
w.Header().Set("X-Next-Offset", "20")
_, _ = io.WriteString(w, "[]")
})
setupStdoutCapture(t)
require.NoError(t, (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 0, "my vault", "", "table"))
assert.Contains(t, outBuf.String(), `--query "my vault"`)
}
14 changes: 11 additions & 3 deletions cmd/vaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ func (c VaultsCmd) Get(ctx context.Context, vault, output string) error {
return printVault(v, output)
}

func (c VaultsCmd) List(ctx context.Context, limit, offset int64, project, output string) error {
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")
}
var response *http.Response
page, err := c.vaults.List(ctx, kernel.VaultListParams{Limit: kernel.Opt(limit), Offset: kernel.Opt(offset)}, option.WithMaxRetries(0), option.WithResponseInto(&response))
params := kernel.VaultListParams{Limit: kernel.Opt(limit), Offset: kernel.Opt(offset)}
if query != "" {
params.Query = kernel.Opt(query)
}
page, err := c.vaults.List(ctx, params, option.WithMaxRetries(0), option.WithResponseInto(&response))
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
Expand Down Expand Up @@ -88,7 +92,11 @@ func (c VaultsCmd) List(ctx context.Context, limit, offset int64, project, outpu
if project != "" {
projectFlag = fmt.Sprintf(" --project %q", project)
}
pterm.Printf("Next: kernel%s vaults list --limit %d --offset %d\n", projectFlag, limit, pagination.NextOffset)
queryFlag := ""
if query != "" {
queryFlag = fmt.Sprintf(" --query %q", query)
}
pterm.Printf("Next: kernel%s vaults list --limit %d --offset %d%s\n", projectFlag, limit, pagination.NextOffset, queryFlag)
}
return nil
}
Expand Down
27 changes: 18 additions & 9 deletions cmd/vaults_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
RunE: func(cmd *cobra.Command, args []string) error {
limit, _ := cmd.Flags().GetInt64("limit")
offset, _ := cmd.Flags().GetInt64("offset")
query, _ := cmd.Flags().GetString("query")
project, _ := cmd.Flags().GetString("project")
return getVaultsHandler(cmd).List(cmd.Context(), limit, offset, resolveProjectSelection(project), vaultOutput(cmd))
return getVaultsHandler(cmd).List(cmd.Context(), limit, offset, query, resolveProjectSelection(project), vaultOutput(cmd))
}}
list.Flags().Int64("limit", 20, "Maximum vaults to return (1-100)")
list.Flags().Int64("offset", 0, "Number of vaults to skip")
list.Flags().String("query", "", "Case-insensitive substring match against vault name; IDs match by exact value")
addVaultJSONOutputFlag(list)

get := &cobra.Command{Use: "get <vault>", Short: "Get a vault by ID or name", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun,
Expand Down Expand Up @@ -273,7 +275,7 @@ JSON
items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultWebMCPCommand(), newVaultDeleteCommand(true))

wallets := &cobra.Command{Use: "wallets", Short: "Connect provider wallets and inspect funding methods"}
walletCreate := &cobra.Command{Use: "create <vault> <key> --provider <link|agentcard> --spec '<json>'", Short: "Create a wallet and display its connection or enrollment action", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
walletCreate := &cobra.Command{Use: "create <vault> <key> --provider <link|agentcard|kernel> --spec '<json>'", Short: "Create a wallet and display its connection or enrollment action", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: "Create a wallet at an immutable key and follow the returned provider action.\n" + vaultSpecHelp + vaultWalletSpecHelp,
Example: ` kernel vaults wallets create checkout wallet-1 \
--provider link --spec '{
Expand All @@ -284,7 +286,10 @@ JSON
}' --open

kernel vaults wallets create checkout wallet-1 \
--provider agentcard --spec '{}'`,
--provider agentcard --spec '{}'

kernel vaults wallets create checkout wallet-2 \
--provider kernel --spec '{}' --open`,
RunE: func(cmd *cobra.Command, args []string) error {
spec, err := vaultWalletSpecFromFlags(cmd)
if err != nil {
Expand Down Expand Up @@ -338,13 +343,14 @@ func newVaultCardCommand(update bool) *cobra.Command {
if update {
use, short = "update", "Update a card spec when the API permits configuration"
}
cmd := &cobra.Command{Use: use + " <vault> <key> --provider <link|agentcard> --spec '<json>'", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: short + `. Neither create nor update authorizes a Link card.
cmd := &cobra.Command{Use: use + " <vault> <key> --provider <link|agentcard|kernel> --spec '<json>'", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: short + `. Neither create nor update authorizes a Link or Kernel card.
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
between authorizations. Identical creates return existing state without resetting it.
between authorizations. Kernel cards cannot be updated; delete and create a new item.
Identical creates return existing state without resetting it.
Never reconfigure the same item to retry a failed, timed-out, rejected, or indeterminate payment.
A recovery item that permits abandonment must be deleted after explicit user confirmation before creating a replacement.
` + vaultSpecHelp + vaultCardSpecHelp,
Expand All @@ -360,6 +366,9 @@ A recovery item that permits abandonment must be deleted after explicit user con
if err != nil {
return err
}
if provider, _ := cmd.Flags().GetString("provider"); update && provider == "kernel" {
return fmt.Errorf("Kernel cards cannot be updated; delete the item and create a new one")
}
return getVaultsHandler(cmd).SaveCard(cmd.Context(), args[0], args[1], param.Override[kernel.CardVaultItemSpecUnionParam](spec), update, vaultOutput(cmd))
}}
addVaultSpecFlags(cmd)
Expand All @@ -368,16 +377,16 @@ A recovery item that permits abandonment must be deleted after explicit user con
}

func addVaultSpecFlags(cmd *cobra.Command) {
cmd.Flags().String("provider", "", "Provider: link or agentcard (required)")
cmd.Flags().String("provider", "", "Provider: link, agentcard, or kernel (required)")
cmd.Flags().String("spec", "", "Raw JSON specification object (required); see types and examples above")
_ = cmd.MarkFlagRequired("provider")
_ = cmd.MarkFlagRequired("spec")
}

func vaultSpecFromFlags(cmd *cobra.Command) (map[string]json.RawMessage, error) {
provider, _ := cmd.Flags().GetString("provider")
if provider != "link" && provider != "agentcard" {
return nil, fmt.Errorf("--provider must be link or agentcard")
if provider != "link" && provider != "agentcard" && provider != "kernel" {
return nil, fmt.Errorf("--provider must be link, agentcard, or kernel")
}
raw, _ := cmd.Flags().GetString("spec")
var spec map[string]json.RawMessage
Expand Down
26 changes: 24 additions & 2 deletions cmd/vaults_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ and requests instead of account. Supply either account or both secrets, never bo
or stdin; they are write-only and never displayed. Never ask an end user for them.
Replace the token with items invoke 1pw_update_access_token --spec-file.`

const vaultManagedAuthCredentialHelp = `Managed auth credentials (spec provider "managed_auth"): reference a managed auth
connection in the vault's project that already has a saved Kernel credential, with
connection_id (from auth connections list) and an optional description. The item
stores no values; fill reads the connection's saved credential at fill time, so
managed auth updates apply immediately. Items are created ready; state.fields lists
fill binding names without values. No collection form is offered and update returns
409. Deleting the item leaves the connection and its credential unchanged.`

const vaultCredentialHelp = `Create credentials for a website.

` + vaultCredentialPathsHelp + `
Expand Down Expand Up @@ -119,7 +127,9 @@ Collection URLs are bearer credentials: share only with the intended user.

` + vaultOnePasswordCredentialHelp + `

` + vaultOnePasswordStoredTokenHelp
` + vaultOnePasswordStoredTokenHelp + `

` + vaultManagedAuthCredentialHelp

func newVaultCredentialsCommand() *cobra.Command {
group := &cobra.Command{Use: "credentials", Short: "Collect, update, and fill user credentials", Long: vaultCredentialHelp}
Expand Down Expand Up @@ -158,6 +168,11 @@ JSON
# 1Password brokered approval (account is the connected credential_account key)
kernel vaults credentials create user-vault github --spec-file - <<'JSON'
{"provider":"1password","account":"onepassword","requests":{"version":2,"entries":[{"type":"login","parameters":{"website":"https://github.com"}}]}}
JSON

# Managed auth connection with a saved credential
kernel vaults credentials create user-vault amazon --spec-file - <<'JSON'
{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"}
JSON`
}
cmd.Flags().String("spec-file", "", "Credential spec JSON file (use '-' for stdin; maximum 128 KiB)")
Expand Down Expand Up @@ -293,8 +308,15 @@ func credentialSpecInput(data []byte) (kernel.CredentialVaultItemSpecInputUnionP
return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("1Password credential spec requires requests with 1-5 login entries")
}
return kernel.CredentialVaultItemSpecInputUnionParam{Of1password: &spec}, nil
case "managed_auth":
var spec kernel.ManagedAuthCredentialVaultItemSpecInputParam
if json.Unmarshal(data, &spec) != nil || strings.TrimSpace(spec.ConnectionID) == "" {
return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("managed auth credential spec requires connection_id")
}
spec.Provider = kernel.ManagedAuthCredentialVaultItemSpecInputProviderManagedAuth
return kernel.CredentialVaultItemSpecInputUnionParam{OfManagedAuth: &spec}, nil
default:
return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("credential spec provider must be kernel or 1password")
return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("credential spec provider must be kernel, 1password, or managed_auth")
}
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/vaults_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,5 +218,5 @@ func TestCredentialSpecInputProvider(t *testing.T) {
assert.EqualValues(t, "kernel", spec.OfKernel.Provider)

_, err = credentialSpecInput([]byte(`{"provider":"bitwarden","fields":[{"name":"password","type":"password"}]}`))
assert.EqualError(t, err, "credential spec provider must be kernel or 1password")
assert.EqualError(t, err, "credential spec provider must be kernel, 1password, or managed_auth")
}
19 changes: 19 additions & 0 deletions cmd/vaults_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ type AgentCardWalletSpec = {
provider_config?: ProviderConfigReference; // omit for Kernel-managed credentials
user_id?: string; // usr_...; enrolled in this organization under the SAME config
};

// Kernel-managed Visa/Mastercard agentic network token enrollment. Creation returns a
// card_enrollment action: the cardholder enters the card on a Kernel-hosted page.
// The card number never reaches Kernel or the CLI. No provider config or tokens.
type KernelWalletSpec = {
provider: "kernel";
};
`

const vaultCardSpecHelp = `
Expand Down Expand Up @@ -73,6 +80,18 @@ 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. Visa purchases are
// not yet supported (authorize returns 400). Updates are not supported.
type KernelCardSpec = {
provider: "kernel";
wallet: string; // Kernel wallet item key
amount: number; // integer minor units; 1..50000
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
};

type LinkLineItem = {
name: string;
quantity?: number; // integer >= 1
Expand Down
Loading
Loading