Skip to content

Commit 709d98b

Browse files
authored
Add vault item fill parameters and safe operation outcomes (#251)
## Summary - Extend `vaults items invoke <vault> <key> <operation>` with `--params` for fill. Validate exact property names, duplicate keys, required browser/page inputs, field bindings, expiration formats, billing fields, and timeout bounds without echoing inputs. The positional operation supplies `type`; parameters cannot override it. - Keep authorize parameterless and preserve `authorize --open`. Re-fetch advertised availability, retain recovery-required guards and project scoping, and disable retries for both requests. - Render fill's value-free result separately from item responses. Completed exits 0; failed/unknown exit 1 after printing the result, including intact JSON on stdout. Reuse the root command's silent-error convention. Sanitize request/response failures and warn that browser writes may have occurred. - Update help, operation hints, and README examples. No checkout submission, alias fallback, or extra prompts. ## SDK approach Upgrade the Go SDK from v0.101.0 to released v0.102.0. Both authorize and fill use typed `PerformOperation` requests and its operation-response union, with `WithMaxRetries(0)`. Fill bindings use the SDK's stored-field/expiration union constructors. No generic `Client.Post` compatibility path remains. Keep strict CLI input validation and display-safe response projections so unknown response properties cannot expose values or selectors; authorize item responses retain the existing output/open behavior. Requires an API that advertises fill; older APIs remain gated by `available_operations`. Browser session IDs are sent unchanged, never resolved as names. No SDK or API release/merge is part of this change. ## Validation - `make test` — passes (`go vet ./...` and full `go test ./...`). - `go test -race ./cmd -run 'TestVault' -count=1` — passes. - `make build` — passes. - Command-level and real CLI subprocess tests cover malformed/incompatible params, operation type protection, 1/32-field boundaries, all stored bindings and expiration formats, authorization compatibility, availability/recovery guards, scoped authenticated serialization, output redaction, response union validation, exit codes, HTTP errors, dropped/truncated connections, and no retries. - The automated HTTP tests use synthetic fixtures and local mock servers. Separate production verification is recorded below. - Full diff reviewed for scope, secret-safe output, dependency changes, and generated-file changes. Only the Go SDK dependency and its checksums changed; no unrelated dependencies or generated files changed. ## Production verification Tested commit `b157880` with SDK v0.102.0 against the production API using a new disposable vault, an explicitly connected Link wallet, and a user-approved card with a $1 spending limit. This was a fill-only test, not a purchase. The headless browser used controlled HTML fulfilled by Playwright, with a cross-origin iframe, password inputs, restrictive CSP, no third-party page scripts, and no form or submit controls. No merchant checkout was submitted. Assertions returned booleans only; card and billing values were not returned, logged, or screenshotted. | Scenario | Observed result | | --- | --- | | Vault/wallet creation, connection, card creation and authorization | Card reached ready and advertised fill | | Fill unavailable on the pending wallet | Exit 1, empty stdout | | Number, expiration MM/YY, and CVC inside cross-origin iframe | Completed; exit 0; exactly three filled outcomes in JSON; boolean format/population checks passed | | exp_month, exp_year, expiration MM/YYYY, billing_name, billing_line1, billing_city, billing_state, billing_postal_code, billing_country | Completed; exit 0; nine value-free rows in normal output; format consistency and population checks passed | | Target removed by an input event after the first write | Failed; exit 1; intact JSON containing filled, failed/target_changed, not_attempted; empty stderr | | Partial-failure browser assertions | First write retained, second target absent, third field untouched; no retry or fallback performed | | Item events | Exactly two completed fill events and one failed fill event | | Cleanup | Disposable browser, vault, wallet, and card deleted | Limitations: this verifies production orchestration and browser writes on a controlled page, not merchant acceptance or payment settlement. Unknown outcomes/transport failures remain covered by mocked tests rather than deliberately induced against a live approved credential. Optional billing_line2 was not requested in production. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Touches payment credential orchestration and browser field writes with partial-failure semantics; mistakes could leave checkout partially filled or mishandle sensitive flows despite redaction guards. > > **Overview** > Adds **`fill`** to `kernel vaults items invoke` so ready Link cards can write vaulted checkout fields into an attached browser via **`--params`** JSON (`browser_id`, exact `page_url`, CSS field bindings, optional `timeout_ms`). **`authorize`** stays parameterless with **`--open`** only; the positional operation sets `type` and cannot be overridden in params. > > **Invoke** now routes through SDK **v0.102.0** typed `PerformOperation` unions: authorize returns a filtered item; fill returns a **value-free** per-field result with exit **0** on `completed` and nonzero on `failed`/`unknown` while still printing JSON on stdout. Strict client-side param validation, display-safe result filtering, sanitized transport/API errors, and no retries or alias fallback. README, cobra help, and `items get` hints document the flow; broad tests cover validation, redaction, authorize compatibility, and CLI subprocess outcomes. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b157880. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 10ebf4a commit 709d98b

10 files changed

Lines changed: 794 additions & 29 deletions

‎README.md‎

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ cannot switch projects.
291291
| `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 |
292292
| `kernel vaults items list <vault>` | List item keys, types, providers, status, and required actions |
293293
| `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 |
294+
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; `authorize --open` opens a returned HTTPS action; `fill --params '<json>'` fills checkout fields |
295295
| `kernel vaults items events <vault> <key>` | Read ordered audit events; `--after <event-id>`, `--wait 0..60` |
296296
| `kernel vaults items delete <vault> <key>` | Invalidate an item; `--yes` skips confirmation |
297297

@@ -429,8 +429,8 @@ wallet and vault deletion. Time passing or deletion is not evidence of non-execu
429429
kernel vaults items get checkout order-1 --wait 60
430430
```
431431

432-
4. When ready, attach the same vault to a new browser. Use only the returned
433-
`state.aliases` values in that browser's checkout and respect returned permitted domains:
432+
4. When ready, attach the same vault to a new browser, navigate to checkout, and use
433+
the advertised `fill` operation below. Respect returned permitted domains:
434434

435435
```bash
436436
kernel browsers create --vault checkout
@@ -473,21 +473,67 @@ card spec. Otherwise, the cardholder selects a card at approval. A reusable card
473473

474474
#### Invoking item operations
475475

476-
`items get` displays every `available_operations` entry's type and description, plus a
477-
copyable `items invoke` command retaining the selected project. Read the description and
478-
follow its approval requirements before invoking. Required user actions (OAuth, enrollment,
479-
MFA, spend approval) appear separately; they are not operations to invoke through this endpoint.
476+
`items get` displays every `available_operations` entry's type and description, plus
477+
an `items invoke` command retaining the selected project (replace `<json>` for fill).
478+
Read the description and follow its approval requirements before invoking. Required user actions
479+
(OAuth, enrollment, MFA, spend approval) appear separately; they are not operations to invoke
480+
through this endpoint.
480481

481482
`items invoke` fetches the item again and calls
482483
`POST /vaults/{id_or_name}/items/{key}/operations` only if the requested operation is still
483484
advertised. The API controls availability. The CLI additionally refuses invocation and opening
484-
actions in `recovery_required`, even if a stale action or operation was returned. The response
485-
is the updated item, possibly with a required user action.
485+
actions in `recovery_required`, even if a stale action or operation was returned.
486486

487-
The current [API spec](https://api.onkernel.com/spec.yaml) accepts only
488-
`{"type":"authorize"}` and forbids extra fields. There is no operation `--spec` flag;
489-
wallet/card `--spec` flags remain unchanged. New parameterless operations can be invoked by
490-
name when the API advertises them, without adding CLI subcommands.
487+
`authorize` sends `{"type":"authorize"}` without `--params` and returns the updated item,
488+
possibly with a required user action. `--open` is supported only for authorize.
489+
The [API spec](https://api.onkernel.com/spec.yaml) also accepts `fill`, with its inputs in
490+
`--params`. The positional operation supplies `type`; including `type` in params is rejected.
491+
Parameters must be a JSON object without unknown or duplicate properties. There is no
492+
operation `--spec` flag; wallet/card `--spec` flags remain unchanged. New parameterless
493+
operations can still be invoked by name when advertised.
494+
495+
##### Fill checkout fields
496+
497+
Fill is supported only when advertised by a ready Link card, not AgentCard. It writes stored
498+
card data without returning the values or submitting checkout:
499+
500+
```bash
501+
kernel vaults items get checkout order-1
502+
kernel vaults items invoke checkout order-1 fill --params '{"browser_id":"browser-session-id","page_url":"https://shop.example/checkout","fields":[{"field":"number","selector":"#card-number"},{"field":"expiration","format":"MM/YY","selector":"#expiry"},{"field":"cvc","selector":"#security-code"}],"timeout_ms":10000}' -o json
503+
```
504+
505+
- `browser_id` is a browser **session ID**, not a reusable browser name. It is sent unchanged;
506+
the CLI does not resolve names.
507+
- `page_url` is the exact current top-level HTTPS URL, including path, query, and fragment,
508+
without embedded credentials. It must match exactly one open page; no prefix/glob matching.
509+
- `fields` contains 1-32 bindings in write order. Each has `field` and a nonempty CSS
510+
`selector` targeting an editable input/select or its container. The API searches the selected
511+
page and descendants, including payment iframes. Do not supply frame IDs or literal values.
512+
- Stored fields: `number`, `cvc`, `exp_month` (MM), `exp_year` (YYYY), `billing_name`,
513+
`billing_line1`, `billing_line2`, `billing_city`, `billing_state`, `billing_postal_code`,
514+
`billing_country`. Billing fields use the stored address without reformatting; request only
515+
needed fields. Missing requested billing data fails validation before browser writes.
516+
- Combined `expiration` requires `format: "MM/YY"` or `"MM/YYYY"`. Other fields reject `format`.
517+
- Optional `timeout_ms` is an integer from 1 to 30000 (default 10000), for the whole operation.
518+
519+
Fill returns an execution result, **not an updated item**. Normal output shows zero-based
520+
field indices, statuses, and error codes. `-o json` preserves the display-safe result shape:
521+
522+
```json
523+
{"type":"fill","status":"unknown","fields":[{"index":0,"status":"filled"},{"index":1,"status":"unknown","error_code":"timeout"},{"index":2,"status":"not_attempted"}]}
524+
```
525+
526+
`completed` exits 0; `failed` and `unknown` exit nonzero **with the result still on stdout**,
527+
without appended error text. API/transport errors exit nonzero with a sanitized diagnostic on
528+
stderr, not a fabricated execution result. No values, selectors, DOM content, or raw browser
529+
errors are printed in fill results.
530+
531+
Fill is non-atomic: execution stops at the first failed/unknown field and earlier writes are
532+
not rolled back. `filled` does not mean the site retained or accepted the value; `completed`
533+
does not mean paid. Transport errors do not prove no writes occurred. Inspect the browser
534+
before deciding what to do next. The CLI never retries, submits checkout, or falls back to
535+
aliases. Returned `state.aliases` remain an alternative for explicitly chosen egress-substitution
536+
integrations, not a recovery path after a failed or indeterminate fill.
491537

492538
#### Expansions, updates, and lifecycle
493539

‎cmd/vaults.go‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"net/http"
78
"net/url"
@@ -13,6 +14,7 @@ import (
1314
"github.com/kernel/cli/pkg/util"
1415
kernel "github.com/kernel/kernel-go-sdk"
1516
"github.com/kernel/kernel-go-sdk/option"
17+
"github.com/kernel/kernel-go-sdk/shared/constant"
1618
"github.com/pterm/pterm"
1719
)
1820

@@ -201,17 +203,26 @@ func (c VaultsCmd) SaveCard(ctx context.Context, vault, key string, spec kernel.
201203
return c.showItem(item, output, false)
202204
}
203205

204-
func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation, output string, open bool) error {
206+
func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, params *vaultFillParams, output string, open bool) error {
205207
if strings.TrimSpace(operation) == "" {
206208
return fmt.Errorf("operation must not be empty")
207209
}
210+
if operation == "fill" && (params == nil || open) {
211+
return fmt.Errorf("fill requires --params and does not support --open")
212+
}
208213
item, err := c.vaults.Items.Get(ctx, key, kernel.VaultItemGetParams{IDOrName: vault}, option.WithMaxRetries(0))
209214
if err != nil {
215+
if operation == "fill" {
216+
return fmt.Errorf("could not retrieve vault item; fill was not invoked")
217+
}
210218
return util.CleanedUpSdkError{Err: err}
211219
}
220+
if item == nil {
221+
return fmt.Errorf("empty vault item response; operation was not invoked")
222+
}
212223
actions, err := effectiveVaultItemActions(item)
213224
if err != nil {
214-
return err
225+
return fmt.Errorf("invalid vault item operations; operation was not invoked")
215226
}
216227
if actions.RecoveryRequired {
217228
return fmt.Errorf("recovery_required: reconcile the original operation with the provider or support; do not retry, delete, or replace it")
@@ -220,7 +231,7 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation, output str
220231
for _, op := range actions.Operations {
221232
if op.Type == operation {
222233
available = true
223-
if output != "json" {
234+
if output != "json" && operation != "fill" {
224235
pterm.Info.Println(op.Description)
225236
}
226237
break
@@ -229,11 +240,23 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation, output str
229240
if !available {
230241
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item", operation)
231242
}
232-
item, err = c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, Type: kernel.VaultItemPerformOperationParamsType(operation)}, option.WithMaxRetries(0))
243+
if operation == "fill" {
244+
return c.fill(ctx, vault, key, params, output)
245+
}
246+
// Preserve support for other advertised parameterless operations.
247+
authorize := kernel.VaultItemPerformOperationParamsBodyAuthorize{Type: constant.Authorize(operation)}
248+
response, err := c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, OfAuthorize: &authorize}, option.WithMaxRetries(0))
233249
if err != nil {
234250
return util.CleanedUpSdkError{Err: err}
235251
}
236-
return c.showItem(item, output, open)
252+
if response == nil || (response.Type != "card" && response.Type != "wallet") {
253+
return fmt.Errorf("unexpected vault operation response; inspect the item and do not retry")
254+
}
255+
var updated kernel.VaultItemUnion
256+
if err := json.Unmarshal([]byte(response.RawJSON()), &updated); err != nil {
257+
return fmt.Errorf("invalid vault item response; inspect the item and do not retry")
258+
}
259+
return c.showItem(&updated, output, open)
237260
}
238261

239262
func (c VaultsCmd) Events(ctx context.Context, vault, key, after string, wait int64, output string) error {

‎cmd/vaults_commands.go‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ Vault names, item keys, and project ownership are immutable.
6363
3. Create a card request with --provider and --spec JSON.
6464
4. Inspect items get, then use items invoke <vault> <key> <operation> only when advertised.
6565
Follow the operation description and any returned provider action.
66-
5. Attach the vault with browsers create --vault <id-or-name>. Use only returned
67-
non-secret aliases in that browser. Inspect items get/events for the outcome.
66+
5. Attach the vault with browsers create --vault <id-or-name>. For ready Link cards,
67+
use advertised fill with --params to bind checkout fields. Returned non-secret
68+
aliases are an alternative for explicitly chosen egress-substitution integrations,
69+
not a fallback after fill. Inspect items get/events for payment outcomes.
6870
6971
Permitted checkout domains are provider-assigned and displayed when returned;
7072
there is no domain-setting API.
@@ -132,13 +134,37 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
132134
itemEvents.Flags().Int64("wait", 0, "Long-poll once for new events (0-60 seconds)")
133135
addVaultJSONOutputFlag(itemEvents)
134136
invoke := &cobra.Command{Use: "invoke <vault> <key> <operation>", Short: "Invoke an operation advertised by an item", Args: cobra.ExactArgs(3), PreRunE: vaultPreRun,
135-
Long: "Retrieve the item and invoke only an operation listed in available_operations.\nRead its description with items get before invoking; follow any approval requirements.\nThe API determines availability regardless of item type, provider, or state.\nRequests are not automatically retried. The updated item may contain a required user action.\nThe current API accepts only {\"type\":\"authorize\"}; there are no operation parameters or --spec flag.",
136-
Example: " kernel vaults items get checkout order-1\n kernel vaults items invoke checkout order-1 authorize",
137+
Long: `Retrieve the item and invoke only an operation listed in available_operations.
138+
Read its description with items get before invoking; follow any approval requirements.
139+
Authorize sends {"type":"authorize"} without --params and returns an updated item;
140+
--open opens its returned HTTPS action URL.
141+
Fill requires --params JSON with browser_id (session ID, not name), exact HTTPS
142+
page_url, and 1-32 fields. Each binding has field and selector; expiration also
143+
requires format MM/YY or MM/YYYY. Stored fields: number, cvc, exp_month (MM),
144+
exp_year (YYYY), billing_name, billing_line1, billing_line2, billing_city,
145+
billing_state, billing_postal_code, billing_country. Optional timeout_ms is 1-30000
146+
(default 10000). Do not include type, values, or frame IDs in --params.
147+
Fill is available only when advertised by a ready Link card, not AgentCard.
148+
The API searches the selected page and descendant frames, including payment iframes.
149+
Fill returns value-free per-field outcomes, not an updated item. Completed exits 0;
150+
failed/unknown exit nonzero while preserving the result in -o json.
151+
Fill is not atomic: earlier writes are not rolled back. Transport errors do not
152+
prove no writes occurred. No automatic retries, alias fallback, or form submission.
153+
Inspect the browser before deciding what to do next; completed does not mean paid.`,
154+
Example: ` kernel vaults items get checkout order-1
155+
kernel vaults items invoke checkout order-1 authorize --open
156+
kernel vaults items invoke checkout order-1 fill --params '{"browser_id":"browser-session-id","page_url":"https://shop.example/checkout","fields":[{"field":"number","selector":"#card-number"},{"field":"expiration","format":"MM/YY","selector":"#expiry"},{"field":"cvc","selector":"#security-code"}],"timeout_ms":10000}' -o json`,
137157
RunE: func(cmd *cobra.Command, args []string) error {
138158
open, _ := cmd.Flags().GetBool("open")
139-
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], vaultOutput(cmd), open)
159+
raw, _ := cmd.Flags().GetString("params")
160+
params, err := parseVaultOperationParams(args[2], raw, cmd.Flags().Changed("params"), cmd.Flags().Changed("open"))
161+
if err != nil {
162+
return err
163+
}
164+
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], params, vaultOutput(cmd), open)
140165
}}
141-
invoke.Flags().Bool("open", false, "Open a returned HTTPS action URL in your browser")
166+
invoke.Flags().String("params", "", "Operation-specific JSON object for fill; omit type (supplied by <operation>)")
167+
invoke.Flags().Bool("open", false, "Open a returned HTTPS action URL for authorize")
142168
addVaultJSONOutputFlag(invoke)
143169
items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultDeleteCommand(true))
144170

‎cmd/vaults_fill.go‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"strconv"
9+
10+
kernel "github.com/kernel/kernel-go-sdk"
11+
"github.com/kernel/kernel-go-sdk/option"
12+
"github.com/pterm/pterm"
13+
)
14+
15+
type vaultFillResult struct {
16+
Type string `json:"type"`
17+
Status string `json:"status"`
18+
Fields []vaultFillFieldResult `json:"fields"`
19+
}
20+
21+
type vaultFillFieldResult struct {
22+
Index *int `json:"index"`
23+
Status string `json:"status"`
24+
ErrorCode string `json:"error_code,omitempty"`
25+
}
26+
27+
var vaultFillResultFields = vaultOutputFields{
28+
"type": nil, "status": nil,
29+
"fields": vaultFieldsOf("index status error_code"),
30+
}
31+
32+
const vaultFillUncertain = "browser fields may have been written; inspect the browser and do not retry or fall back to aliases"
33+
34+
func vaultFillRequestError(err error) error {
35+
var apiErr *kernel.Error
36+
if errors.As(err, &apiErr) {
37+
return fmt.Errorf("fill request failed (HTTP %d); %s", apiErr.StatusCode, vaultFillUncertain)
38+
}
39+
// Do not wrap SDK/transport errors: they can contain request or response data,
40+
// and the root error handler extracts raw SDK error messages through Unwrap.
41+
return fmt.Errorf("fill result unavailable; %s", vaultFillUncertain)
42+
}
43+
44+
func (c VaultsCmd) fill(ctx context.Context, vault, key string, params *vaultFillParams, output string) error {
45+
request := kernel.FillVaultItemOperationRequestParam{
46+
BrowserID: params.BrowserID,
47+
PageURL: params.PageURL,
48+
Type: kernel.FillVaultItemOperationRequestTypeFill,
49+
Fields: make([]kernel.VaultCardFillFieldUnionParam, 0, len(params.Fields)),
50+
}
51+
if params.TimeoutMS != nil {
52+
request.TimeoutMs = kernel.Opt(int64(*params.TimeoutMS))
53+
}
54+
for _, field := range params.Fields {
55+
binding := kernel.VaultCardFillFieldParamOfVaultCardFillFieldVaultCardStoredFillField(field.Field, field.Selector)
56+
if field.Field == "expiration" {
57+
binding = kernel.VaultCardFillFieldParamOfVaultCardFillFieldVaultCardExpirationFillField(field.Field, field.Format, field.Selector)
58+
}
59+
request.Fields = append(request.Fields, binding)
60+
}
61+
response, err := c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, OfFill: &request}, option.WithMaxRetries(0))
62+
if err != nil {
63+
return vaultFillRequestError(err)
64+
}
65+
if response == nil {
66+
return fmt.Errorf("empty fill result; %s", vaultFillUncertain)
67+
}
68+
result, err := parseVaultFillResult(json.RawMessage(response.RawJSON()), len(params.Fields))
69+
if err != nil {
70+
return err
71+
}
72+
if output == "json" {
73+
if err := printVaultJSON(result); err != nil {
74+
return err
75+
}
76+
} else {
77+
pterm.Printf("Fill: %s\n", result.Status)
78+
rows := pterm.TableData{{"Field index", "Status", "Error code"}}
79+
for _, field := range result.Fields {
80+
rows = append(rows, []string{strconv.Itoa(*field.Index), field.Status, field.ErrorCode})
81+
}
82+
PrintTableNoPad(rows, true)
83+
if result.Status == "completed" {
84+
pterm.Println("Fields filled; this does not confirm payment or merchant acceptance.")
85+
} else {
86+
pterm.Println(vaultFillUncertain)
87+
}
88+
}
89+
if result.Status != "completed" {
90+
return vaultFillOutcomeError{status: result.Status}
91+
}
92+
return nil
93+
}
94+
95+
// The result has already been printed; retain a nonzero exit without diagnostics.
96+
type vaultFillOutcomeError struct{ status string }
97+
98+
func (e vaultFillOutcomeError) Error() string { return "fill " + e.status }
99+
func (e vaultFillOutcomeError) Silent() bool { return true }
100+
101+
func parseVaultFillResult(raw json.RawMessage, count int) (*vaultFillResult, error) {
102+
invalid := fmt.Errorf("invalid fill result; %s", vaultFillUncertain)
103+
safe, err := filterVaultJSON(raw, vaultFillResultFields)
104+
if err != nil {
105+
return nil, invalid
106+
}
107+
var result vaultFillResult
108+
if json.Unmarshal(safe, &result) != nil || result.Type != "fill" || len(result.Fields) != count {
109+
return nil, invalid
110+
}
111+
status := "completed"
112+
stopped := false
113+
for i, field := range result.Fields {
114+
if field.Index == nil || *field.Index != i {
115+
return nil, invalid
116+
}
117+
if stopped {
118+
if field.Status != "not_attempted" {
119+
return nil, invalid
120+
}
121+
} else {
122+
switch field.Status {
123+
case "filled":
124+
case "failed", "unknown":
125+
status, stopped = field.Status, true
126+
default:
127+
return nil, invalid
128+
}
129+
}
130+
if field.ErrorCode != "" {
131+
if field.Status != "failed" && field.Status != "unknown" {
132+
return nil, invalid
133+
}
134+
switch field.ErrorCode {
135+
case "target_changed", "element_not_found", "ambiguous_selector", "element_not_editable", "option_not_found", "timeout", "execution_failed":
136+
default:
137+
return nil, invalid
138+
}
139+
}
140+
}
141+
if result.Status != status {
142+
return nil, invalid
143+
}
144+
return &result, nil
145+
}

0 commit comments

Comments
 (0)