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
17 changes: 14 additions & 3 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 @@ -378,6 +378,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 @@ -389,7 +400,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 All @@ -398,7 +409,7 @@ cannot switch projects.
| `kernel vaults cards update <vault> <key> --provider link\|agentcard --spec '<json>'` | Update a card spec; pending issuance preserves omitted optional fields, and the API enforces state/provider constraints |
| `kernel vaults items list <vault>` | List item keys, types, providers, status, and required actions |
| `kernel vaults items get <vault> <key>` | Inspect state/actions/returned AgentCard aliases and copyable operation commands; `--wait 0..60`, `--expand payment_methods`, `--open` |
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; `authorize --open` opens a returned HTTPS action; `prepare_checkout --params '<json>'` prepares an unused AgentCard card for Square Pay; `fill --params '<json>'` fills checkout or login fields; `collect --open` opens a credential item's hosted form |
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; `authorize --open` opens a returned HTTPS action; `prepare_checkout --params '<json>'` prepares an unused AgentCard card for Square Pay; `fill --params '<json>'` fills checkout or login fields; `webmcp_invoke --params '<json>'` invokes a WebMCP tool (from `browsers webmcp list`) with vaulted values bound to null input slots by JSON Pointer; `collect --open` opens a credential item's hosted form |
| `kernel vaults items events <vault> <key>` | Read ordered audit events; `--after <event-id>`, `--wait 0..60` |
| `kernel vaults items delete <vault> <key>` | Invalidate an item; `--yes` skips confirmation |

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"`)
}
24 changes: 19 additions & 5 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 Expand Up @@ -236,12 +244,15 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
if operation == "prepare_checkout" && (params == nil || params.Checkout == nil) {
return fmt.Errorf("prepare_checkout requires checkout parameters")
}
if operation == "webmcp_invoke" && (params == nil || params.WebMCP == nil || open) {
return fmt.Errorf("webmcp_invoke requires --params and does not support --open")
}
if isOnePasswordOperation(operation) && (params == nil || params.OnePassword == nil) {
return fmt.Errorf("%s requires its documented parameters", operation)
}
item, err := c.vaults.Items.Get(ctx, key, kernel.VaultItemGetParams{IDOrName: vault}, option.WithMaxRetries(0))
if err != nil {
if operation == "fill" || operation == "1pw_fill" {
if operation == "fill" || operation == "1pw_fill" || operation == "webmcp_invoke" {
return vaultFillLookupError(err, operation)
}
return util.CleanedUpSdkError{Err: err}
Expand All @@ -260,7 +271,7 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
for _, op := range actions.Operations {
if op.Type == operation {
available = true
if output != "json" && operation != "fill" {
if output != "json" && operation != "fill" && operation != "webmcp_invoke" {
pterm.Info.Println(op.Description)
}
break
Expand All @@ -275,6 +286,9 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
if operation == "fill" {
return c.fill(ctx, vault, key, params.Fill, output)
}
if operation == "webmcp_invoke" {
return c.webmcpInvoke(ctx, vault, key, params.WebMCP, output)
}
if operation == "1pw_fill" {
return c.onePasswordFill(ctx, vault, key, params.OnePassword, output)
}
Expand Down
Loading
Loading