Skip to content

Commit 512d979

Browse files
authored
Add vault payment commands (#244)
## Summary Add vault payment commands for Link and AgentCard. The CLI prepares payment credentials, displays provider actions and payment-method choices, attaches vaults to new browsers, and observes item state/events. It does not submit merchant payments. ### Current interface - Vaults: `create --name`, `list`, `get <vault>`, `delete <vault>`. - Wallets: `wallets create <vault> <key> --provider link|agentcard --spec '<json>' [--open]`; `wallets payment-methods <vault> <key>`. - Cards: `cards create|update <vault> <key> --provider link|agentcard --spec '<json>'`. - Items: `items list`, `items get [--wait 0..60] [--expand payment_methods] [--open]`, `items invoke <vault> <key> <operation> [--open]`, `items events [--after <event-id>] [--wait 0..60]`, and `items delete`. - Browser attachment: `browsers create --vault <id-or-name>`; repeatable, up to 20 vaults, incompatible with pools. The `wallets`, `cards`, and `items` commands are under `kernel vaults`. Vault selectors accept an ID or name; item selectors currently use the immutable key. `--project`/`KERNEL_PROJECT` is optional: omitted scope is resolved by the API, using the default project for org-wide credentials rather than listing all projects. ### Specifications and behavior `--spec` is a JSON specification object, not a `{type, spec}` envelope. The command supplies the item type and injects `provider`; an embedded provider must match the flag. Provider-specific flags have been removed. JSON values are passed through the SDK without defaults, normalization, or integer precision loss; the API validates required and optional fields. This includes Link purchase details, metadata, and expiry. Card updates replace the entire spec, including removing omitted optional fields. Help includes provider-specific types and examples, with a sync comment linking to the [published API spec](https://api.onkernel.com/spec.yaml). The README covers both provider flows, expansion syntax, updates, and lifecycle behavior. - `items get` shows operation types/descriptions and shell-quoted copyable commands retaining the selected project. `items invoke` GETs the item again, checks the requested type against `available_operations`, then POSTs `{"type":"<operation>"}` to `/operations`. Availability is API-controlled, without local item/provider/state rules or an operation registry. It prints the updated item and supports `--open` and `-o json`. Requests do not automatically retry; waits are bounded observations, not readiness/payment guarantees. - The current OpenAPI operation schema accepts only `{"type":"authorize"}` and forbids extra fields; no operation `--spec` flag is added. Future parameterless operations can be invoked by name when advertised. The dedicated `cards authorize` command is removed without an alias. - Action and approval URLs print in full outside truncating tables. Non-delete commands support `-o json`; output preserves public fields and aliases while filtering unknown/opaque provider data and credential-bearing action URLs. - API errors retain the standard code/message. Vault and item deletes treat any HTTP 404 as success with `Deleted or not found`; other errors fail. Existing non-vault delete commands are unchanged. - Vault bindings are creation-only. Vault/item rename and provider-domain configuration are not exposed. ## SDK Use published `github.com/kernel/kernel-go-sdk v0.100.0`. The preview replacement and checksums are removed. CI no longer needs a private SDK token or GOPRIVATE configuration, and Go caching is restored. Nine existing browser SDK call sites use the released SDK's `IDOrName` path-field spelling. No SDK repository files were changed. ## Verification - `make test` and `make build` pass locally; vault tests also pass with `-race`. All four README `--spec` examples parse as JSON objects. - `golangci-lint run --new-from-rev=origin/main`: 0 issues. - SDK v0.100.0 downloads into an empty module cache with private-module configuration disabled; `go mod verify` passes. README and repository Markdown docs contain no card-mode references. - HTTP fixtures cover both providers, raw JSON preservation/validation, request envelopes, optional scope, API errors, deletion semantics, bounded waits, advertised-operation gating across types/providers/states, future operation names, operation GET/POST errors without retries, explicit action opening, shell-safe/project-aware invocation hints, expansions, output, and browser attachment/pool conflicts. - Local CLI checks cover both providers' wallet/card specs, full URLs in 40/80/120-column terminals, delete/list behavior for invalid-project responses, and executing a copied GET operation hint against a fixture (project shell quoting preserved; exactly GET/GET/POST). No live payment credentials or payments were created for testing. --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
1 parent 2c121e3 commit 512d979

15 files changed

Lines changed: 2302 additions & 14 deletions

‎DEVELOPMENT.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ brew install onkernel/tap/kernel
1212

1313
Install the following tools:
1414

15-
- Go 1.22+ ( https://go.dev/doc/install )
15+
- Go 1.25+ ( https://go.dev/doc/install ); the required version is recorded in `go.mod`
1616
- [Goreleaser Pro](https://goreleaser.com/install/#pro) - **IMPORTANT: You must install goreleaser-pro, not the standard version, as this is required for our release process**
1717
- [chglog](https://github.com/goreleaser/chglog)
1818

‎README.md‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Commands with JSON output support:
128128
- **Proxies**: `create`, `list`, `get`, `update`, `check`
129129
- **API Keys**: `create`, `list`, `get`, `update`, `rotate`
130130
- **Auth Connections**: `timeline`
131+
- **Vaults**: `create`, `list`, `get`, `items list/get/events/invoke`, `wallets create/payment-methods`, `cards create/update` (display-safe public fields only)
131132
- **Projects**: `update`
132133
- **Org**: `limits get/set`
133134
- **Apps**: `list`, `history`
@@ -223,6 +224,7 @@ Commands with JSON output support:
223224
- `--proxy-mode direct|default` - Egress mode instead of a selected proxy: `direct` for no proxy regardless of stealth, `default` for the stealth-derived default (Kernel's stealth proxy with `--stealth`, direct egress otherwise). Omit all proxy flags to get the default.
224225
- `--name <name>` - Optional unique name for the session (used to find it later by name; can be changed with `browsers update --name`)
225226
- `--tag <KEY=VALUE>` - Set a tag on the session, repeatable; up to 50 pairs
227+
- `--vault <id-or-name>` - Attach a project-owned vault at creation (repeatable, max 20). Uses the API's effective project unless `--project` or `KERNEL_PROJECT` selects one. Cannot be combined with pool flags, even with `--yes`; vault bindings cannot be added to existing sessions.
226228
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags). `--name`/`--tag` still apply to the acquired session.
227229
- `--pool-name <name>` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags)
228230
- `--telemetry=all` - Enable telemetry for all categories
@@ -266,6 +268,174 @@ Commands with JSON output support:
266268
- `-s, --silent` - Suppress progress output
267269
- _Note: redirects are followed automatically by Chromium._
268270

271+
### Vaults
272+
273+
Vault commands **prepare and observe payment credentials; they do not submit merchant payments**.
274+
Vault names, item keys, and project ownership are immutable. Optionally select a project with
275+
`--project <id-or-name>` or `KERNEL_PROJECT`; otherwise, the API resolves the project from your
276+
credentials and its defaults (the default project for org-wide credentials, not all projects).
277+
Ownership is assigned from that scope, not a `project_id` body field. Project-scoped credentials
278+
cannot switch projects.
279+
280+
#### Command reference
281+
282+
| Command | Purpose / flags |
283+
| --- | --- |
284+
| `kernel vaults create --name <name>` | Create or retrieve the vault with that immutable name |
285+
| `kernel vaults list` | `--limit 1..100` (default 20), `--offset`; JSON includes `vaults` and optional `next_offset` |
286+
| `kernel vaults get <vault>` | Get by ID or name |
287+
| `kernel vaults delete <vault>` | Invalidate the vault and all its items; `--yes` skips confirmation |
288+
| `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 |
289+
| `kernel vaults wallets payment-methods <vault> <key>` | Fetch advertised live payment methods; JSON is the item with `expanded.payment_methods` |
290+
| `kernel vaults cards create <vault> <key> --provider link\|agentcard --spec '<json>'` | Create a card request; never implicitly authorize Link |
291+
| `kernel vaults cards update <vault> <key> --provider link\|agentcard --spec '<json>'` | Replace the full card spec; the API enforces state/provider constraints |
292+
| `kernel vaults items list <vault>` | List item keys, types, providers, status, and required actions |
293+
| `kernel vaults items get <vault> <key>` | Inspect state/actions/returned aliases and copyable operation commands; `--wait 0..60`, `--expand payment_methods`, `--open` |
294+
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; optional `--open` opens a returned HTTPS action |
295+
| `kernel vaults items events <vault> <key>` | Read ordered audit events; `--after <event-id>`, `--wait 0..60` |
296+
| `kernel vaults items delete <vault> <key>` | Invalidate an item; `--yes` skips confirmation |
297+
298+
`<vault>` accepts an ID or name. `<key>` is the immutable item key within that vault, not
299+
its generated item ID. Names and keys use letters, digits, dots, underscores, and hyphens
300+
(1–255 characters; not `.` or `..`). All commands except delete support `-o json`.
301+
JSON preserves field presence and API-returned aliases, while omitting unknown fields,
302+
opaque metadata, and unrecognized event data. Human output labels aliases as non-secret
303+
checkout values and distinguishes card readiness from checkout authorization/payment outcomes.
304+
Action and approval URLs print in full on separate lines, without table truncation.
305+
API failures use the CLI's standard error formatter, preserving the API's code and message.
306+
`vaults delete` and `vaults items delete` treat HTTP 404 as success and print
307+
`Deleted or not found`, whether the missing object is the project, vault, or item.
308+
Other API errors still return a nonzero exit status.
309+
310+
**Provider specifications:** wallet creation and card creation/update require `--provider`
311+
and `--spec '<json>'`. Supply only the spec object, not a `{type, spec}` envelope. The command
312+
sets the item type and injects `provider`; if JSON also contains `provider`, it must match.
313+
Other values are forwarded unchanged, including optional fields, without defaults or normalization.
314+
The API validates the provider-specific schema. Each command's `--help` includes its raw
315+
TypeScript-style types, which must stay in sync with the [API spec](https://api.onkernel.com/spec.yaml).
316+
317+
- **Link wallet:** supply `authorization: {method: "oauth", client: {type: "kernel_managed"}}`.
318+
- **AgentCard wallet:** use `{}` to enroll, or supply `user_id` for an already enrolled user.
319+
- **Link card:** include the required fields shown in help. Optional `line_items`, `totals`,
320+
`metadata`, and `expires_at` are supported through JSON.
321+
- **AgentCard card:** uses `merchant`, not Link's `merchant_name`. Its optional `card_id` selects
322+
a vaulted card; otherwise the cardholder selects one at approval.
323+
324+
`cards update` replaces the entire spec, so omitted optional details are removed. Permitted
325+
checkout domains remain provider-assigned. Neither command submits a merchant payment.
326+
327+
#### Link checkout preparation
328+
329+
1. Create/select a vault in the effective project. Connect the wallet in the provider's UI:
330+
331+
```bash
332+
kernel vaults create --name checkout
333+
kernel vaults wallets create checkout wallet-1 --provider link \
334+
--spec '{"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}' --open
335+
kernel vaults items get checkout wallet-1 --wait 60
336+
```
337+
338+
2. Once connected, list methods and explicitly choose a returned ID:
339+
340+
```bash
341+
kernel vaults wallets payment-methods checkout wallet-1
342+
# Equivalent: kernel vaults items get checkout wallet-1 --expand payment_methods
343+
kernel vaults cards create checkout order-1 --provider link --spec '{
344+
"wallet": "wallet-1",
345+
"payment_method_id": "<returned-id>",
346+
"amount": 1234,
347+
"currency": "usd",
348+
"merchant_name": "Example Shop",
349+
"merchant_url": "https://shop.example",
350+
"context": "Purchase the selected office supplies from Example Shop for the approved order, with a total spending limit of 1234 minor currency units."
351+
}'
352+
```
353+
354+
3. After explicit user approval, authorize **only if the item advertises it**. Follow the
355+
returned approval action, then observe:
356+
357+
```bash
358+
kernel vaults items get checkout order-1
359+
kernel vaults items invoke checkout order-1 authorize --open
360+
kernel vaults items get checkout order-1 --wait 60
361+
```
362+
363+
4. When ready, attach the same vault to a new browser. Use only the returned
364+
`state.aliases` values in that browser's checkout and respect returned permitted domains:
365+
366+
```bash
367+
kernel browsers create --vault checkout
368+
```
369+
370+
5. Observe outcomes independently of merchant checkout submission:
371+
372+
```bash
373+
kernel vaults items get checkout order-1
374+
kernel vaults items events checkout order-1
375+
kernel vaults items events checkout order-1 --after <last-event-id> --wait 60
376+
```
377+
378+
#### AgentCard checkout preparation
379+
380+
For a separate AgentCard flow, create a vault and complete the wallet enrollment action:
381+
382+
```bash
383+
kernel vaults create --name agentcard-checkout
384+
kernel vaults wallets create agentcard-checkout wallet-1 --provider agentcard --spec '{}' --open
385+
kernel vaults items get agentcard-checkout wallet-1 --wait 60
386+
```
387+
388+
Once the wallet is connected, create the card request:
389+
390+
```bash
391+
kernel vaults cards create agentcard-checkout order-1 --provider agentcard --spec '{
392+
"wallet": "wallet-1",
393+
"merchant": "Example Shop",
394+
"amount": 1234,
395+
"currency": "usd"
396+
}'
397+
kernel browsers create --vault agentcard-checkout
398+
```
399+
400+
AgentCard authorizes at checkout and does not currently advertise `authorize`. To select a
401+
vaulted card in advance, inspect `wallets payment-methods` and include its ID as `card_id` in the
402+
card spec. Otherwise, the cardholder selects a card at approval. A reusable card being
403+
`ready` does not mean the last payment succeeded.
404+
405+
#### Invoking item operations
406+
407+
`items get` displays every `available_operations` entry's type and description, plus a
408+
copyable `items invoke` command retaining the selected project. Read the description and
409+
follow its approval requirements before invoking. Required user actions (OAuth, enrollment,
410+
MFA, spend approval) appear separately; they are not operations to invoke through this endpoint.
411+
412+
`items invoke` fetches the item again and calls
413+
`POST /vaults/{id_or_name}/items/{key}/operations` only if the requested operation is still
414+
advertised. The API controls availability; the CLI has no provider/type/state-specific
415+
operation checks. The response is the updated item, possibly with a required user action.
416+
417+
The current [API spec](https://api.onkernel.com/spec.yaml) accepts only
418+
`{"type":"authorize"}` and forbids extra fields. There is no operation `--spec` flag;
419+
wallet/card `--spec` flags remain unchanged. New parameterless operations can be invoked by
420+
name when the API advertises them, without adding CLI subcommands.
421+
422+
#### Expansions, updates, and lifecycle
423+
424+
`--expand` takes a value, such as `--expand payment_methods`; it is not a boolean switch.
425+
Request only expansions advertised in `available_expansions`. Add `-o json` to read the
426+
returned `expanded.payment_methods` directly. Unavailable expansions return an API error.
427+
428+
Use `cards update <vault> <key> --provider <provider> --spec '<json>'` to replace the entire
429+
card spec when the API permits it. Include optional fields you want to retain; the CLI
430+
does not merge the new JSON with the existing spec.
431+
432+
Waits are single bounded observations, not readiness guarantees or payment retries. Pending
433+
state is returned as-is. Requests are not automatically retried by the vault commands.
434+
**Never retry failed, timed-out, rejected, or indeterminate payments.** Inspect state/events
435+
and reconcile the outcome instead. Do not pass card data, OAuth codes/tokens, ciphertext,
436+
provider secrets, or sensitive provider responses to the CLI. Complete collection, OAuth,
437+
and approval actions through the provider's returned URL/UI; no callback-code command exists.
438+
269439
### Browser Pools
270440

271441
- `kernel browser-pools list` - List browser pools

‎cmd/browser_vaults.go‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
kernel "github.com/kernel/kernel-go-sdk"
7+
)
8+
9+
func buildBrowserVaults(values []string) ([]kernel.VaultReferenceParam, error) {
10+
if len(values) > 20 {
11+
return nil, fmt.Errorf("at most 20 --vault references may be attached")
12+
}
13+
var refs []kernel.VaultReferenceParam
14+
seen := make(map[string]bool, len(values))
15+
for _, value := range values {
16+
if err := validateVaultName(value, "--vault"); err != nil {
17+
return nil, err
18+
}
19+
if seen[value] {
20+
return nil, fmt.Errorf("duplicate --vault reference")
21+
}
22+
seen[value] = true
23+
ref := kernel.VaultReferenceParam{}
24+
if cuidRegex.MatchString(value) {
25+
ref.ID = kernel.Opt(value)
26+
} else {
27+
ref.Name = kernel.Opt(value)
28+
}
29+
refs = append(refs, ref)
30+
}
31+
return refs, nil
32+
}

‎cmd/browser_vaults_test.go‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
11+
"github.com/kernel/cli/pkg/util"
12+
kernel "github.com/kernel/kernel-go-sdk"
13+
"github.com/kernel/kernel-go-sdk/option"
14+
"github.com/spf13/cobra"
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
func TestBuildBrowserVaults(t *testing.T) {
20+
const id = "abcdefghijklmnopqrstuvwx"
21+
refs, err := buildBrowserVaults([]string{id, "checkout"})
22+
require.NoError(t, err)
23+
require.Len(t, refs, 2)
24+
assert.Equal(t, id, refs[0].ID.Value)
25+
assert.False(t, refs[0].Name.Valid())
26+
assert.Equal(t, "checkout", refs[1].Name.Value)
27+
assert.False(t, refs[1].ID.Valid())
28+
for _, values := range [][]string{{""}, {" "}, {"../checkout"}, {".."}, {"checkout", "checkout"}, make([]string, 21)} {
29+
_, err := buildBrowserVaults(values)
30+
require.Error(t, err)
31+
}
32+
refs, err = buildBrowserVaults(nil)
33+
require.NoError(t, err)
34+
body, err := json.Marshal(kernel.BrowserNewParams{Vaults: refs})
35+
require.NoError(t, err)
36+
assert.NotContains(t, string(body), "vaults")
37+
assert.NotNil(t, browsersCreateCmd.Flags().Lookup("vault"))
38+
assert.Nil(t, browsersUpdateCmd.Flags().Lookup("vault"))
39+
assert.False(t, poolLeaseAllowedFlags()["vault"])
40+
}
41+
42+
func browserVaultTestCommand(client kernel.Client) *cobra.Command {
43+
cmd := &cobra.Command{Use: "create"}
44+
cmd.Flags().String("project", "", "")
45+
cmd.Flags().StringArray("vault", nil, "")
46+
cmd.Flags().String("pool-id", "", "")
47+
cmd.Flags().String("pool-name", "", "")
48+
cmd.Flags().Bool("yes", false, "")
49+
addJSONOutputFlag(cmd)
50+
cmd.SetContext(context.WithValue(context.Background(), util.KernelClientKey, client))
51+
return cmd
52+
}
53+
54+
func TestBrowserVaultPoolAndReferenceValidation(t *testing.T) {
55+
t.Setenv("KERNEL_PROJECT", "")
56+
client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { t.Error("invalid attachment reached API") })
57+
for _, flags := range [][]string{
58+
{"--vault", "checkout", "--pool-id", "pool-1", "--yes"},
59+
{"--vault", "checkout", "--pool-name", "pool", "--yes"},
60+
{"--vault="},
61+
} {
62+
cmd := browserVaultTestCommand(client)
63+
require.NoError(t, cmd.ParseFlags(flags))
64+
err := runBrowsersCreate(cmd, nil)
65+
require.Error(t, err)
66+
}
67+
}
68+
69+
func TestBrowserCreateVaultRequestAndReturnedAttachments(t *testing.T) {
70+
t.Setenv("KERNEL_PROJECT", "")
71+
const body = `{"session_id":"browser-1","cdp_ws_url":"ws://example.test/cdp","vaults":[{"id":"vault-1","name":"checkout"}]}`
72+
client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) {
73+
assert.Equal(t, http.MethodPost, r.Method)
74+
assert.Equal(t, "/browsers", r.URL.Path)
75+
assert.Empty(t, r.Header.Get("X-Kernel-Project"))
76+
payload, _ := io.ReadAll(r.Body)
77+
assert.JSONEq(t, `{"vaults":[{"name":"checkout"}]}`, string(payload))
78+
w.Header().Set("Content-Type", "application/json")
79+
_, _ = io.WriteString(w, body)
80+
})
81+
for _, output := range []string{"", "json"} {
82+
cmd := browserVaultTestCommand(client)
83+
require.NoError(t, cmd.Flags().Set("vault", "checkout"))
84+
require.NoError(t, cmd.Flags().Set("output", output))
85+
buf := capturePtermOutput(t)
86+
out := captureStdout(t, func() { require.NoError(t, runBrowsersCreate(cmd, nil)) })
87+
if output == "json" {
88+
assert.JSONEq(t, body, out)
89+
} else {
90+
assert.Contains(t, buf.String(), "Attached vault ID")
91+
assert.Contains(t, buf.String(), "vault-1")
92+
assert.Contains(t, buf.String(), "checkout")
93+
}
94+
}
95+
}
96+
97+
func TestBrowserCreateInvalidVaultNeverCallsSDK(t *testing.T) {
98+
b := BrowsersCmd{browsers: &FakeBrowsersService{NewFunc: func(ctx context.Context, body kernel.BrowserNewParams, opts ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
99+
t.Fatal("invalid vault reference should not reach SDK")
100+
return nil, nil
101+
}}}
102+
require.Error(t, b.Create(context.Background(), BrowsersCreateInput{Vaults: []string{strings.Repeat("x", 256)}}))
103+
}

0 commit comments

Comments
 (0)