Skip to content

Commit 4aff0a9

Browse files
authored
Add vault item WebMCP invoke command (#279)
## Summary Adds CLI support for the vault `webmcp_invoke` operation, which invokes a live WebMCP tool with values from a credential item or ready Link card substituted into null input slots. - Bumps `github.com/kernel/kernel-go-sdk` from v0.116.0 to v0.117.0, which adds `WebmcpInvokeVaultItemOperationRequestParam` and the `webmcp_invoke` result. - New flag-based command: ```sh kernel vaults items webmcp invoke <vault> <key> \ --browser-id <session-id> --tool-ref <tool-ref> --page-url <source.page_url> \ --input '{"email":null,"password":null}' \ --bind email=/email --bind password=/password [--timeout-sec 1..120] [-o json] ``` `--input-file <path|->` replaces `--input`. Card expiration uses `--bind expiration:MM/YY=/pointer`. - `kernel vaults items invoke <vault> <key> webmcp_invoke --params/--spec-file` accepts the same request as JSON (`browser_id`, `tool_ref`, `page_url`, `input`, `bindings[{field,input_path,format?}]`, `timeout_sec`). Both forms go through the same `Invoke` path: the item is fetched first, and the operation must appear in `available_operations`. Before this change, an advertised `webmcp_invoke` fell through to the parameterless-operation branch. - Local validation runs before any request and never echoes payloads. It mirrors the API's binding rules: 1-32 bindings; unique fields and paths; each `input_path` is an RFC 6901 pointer to an existing `null` (no root, no `-` append, canonical in-range indices, valid `~0`/`~1` escapes). It also checks: input is a JSON object of at most 64 KiB; `tool_ref` is non-empty and at most 128 bytes; `page_url` is absolute with no fragment; `timeout_sec` is 1-120. Top-level input members are sent as raw JSON so numbers are not re-encoded. The SDK encodes `json.Number` as a string, which would change them. - Output: `-o json` prints `type`, `status`, `invocation_id`, `output` (raw JSON preserved) and `error_text` as returned. Human output labels `output`/`error_text` as untrusted page data that may contain supplied values. Both are JSON-encoded so page-supplied terminal control characters are escaped. - Outcomes follow existing fill/1pw_fill conventions: - `completed` and `awaiting_submission` exit 0. `awaiting_submission` directs the user to submit the populated form instead of re-invoking, matching `browsers webmcp invoke`. - `canceled`, `error` and `unknown` exit nonzero with no extra diagnostic; the JSON result stays on stdout. - Requests use `WithMaxRetries(0)`. - 400/403/404/409 errors keep the recognized code plus guidance, and state that the tool was not invoked. - 5xx errors, transport loss, and malformed results report that the tool may have run, and say not to retry. - Help text (`vaults`, `items`, `items invoke`, `items webmcp invoke`) and README updated. Existing fill semantics are unchanged. ## Tests `make test` (go vet + go test ./...) passes. New `cmd/vaults_webmcp_test.go` covers: - synthetic Resy login (`email`/`password` null slots) through both the flag command and `items invoke ... webmcp_invoke --spec-file`, asserting the exact request body and the JSON result, including a large integer preserved in output - card expiration format, nested/array/escaped pointers, and large-number preservation in input - 35 validation cases that make no HTTP request and don't leak payloads - each result status in JSON and human modes, including exit codes and escaping of control characters in `error_text` - 400/403/409/500/502 and malformed responses, each sent exactly once (no retry), with correct invoked-vs-uncertain guidance - an unadvertised operation and item lookup failure, neither of which POSTs Not run against the live API. --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
1 parent 53119d1 commit 4aff0a9

14 files changed

Lines changed: 905 additions & 71 deletions

‎README.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ bearer credential: share it only with that user. Readiness means required values
348348
populated, not that login succeeded. `fill` requires an already-open page and never
349349
navigates or submits it. Optional `page_url` selects the exact page; cards require it.
350350
Do not automatically retry failed/unknown fills or fall back to aliases.
351+
Once ready, use `fill` for ordinary web forms. When the item advertises `webmcp_invoke`, use
352+
`items webmcp invoke` instead to bind credential fields to existing `null` inputs of a live
353+
WebMCP tool; the tool may submit or have other side effects, so never retry an uncertain
354+
outcome (see [Invoke WebMCP tools with vault fields](#invoke-webmcp-tools-with-vault-fields)).
355+
1Password credentials use their own advertised `1pw_*` operations instead.
351356

352357
Create specs list `fields` as an ordered array. Each entry carries a stable `name`
353358
(letters, digits, and underscores, starting with a letter) that keys values, updates,
@@ -399,6 +404,7 @@ cannot switch projects.
399404
| `kernel vaults items list <vault>` | List item keys, types, providers, status, and required actions |
400405
| `kernel vaults items get <vault> <key>` | Inspect state/actions/returned AgentCard aliases and copyable operation commands; `--wait 0..60`, `--expand payment_methods`, `--open` |
401406
| `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 |
407+
| `kernel vaults items webmcp invoke <vault> <key>` | Invoke a live WebMCP tool with item fields bound to null input slots; `--browser-id`, `--tool-ref`, `--page-url`, `--input`/`--input-file`, repeatable `--bind <field>=<json-pointer>`, `--timeout-sec 1..120`. Same as `items invoke <vault> <key> webmcp_invoke --params '<json>'` |
402408
| `kernel vaults items events <vault> <key>` | Read ordered audit events; `--after <event-id>`, `--wait 0..60` |
403409
| `kernel vaults items delete <vault> <key>` | Invalidate an item; `--yes` skips confirmation |
404410

@@ -706,6 +712,46 @@ back to aliases. Link cards do not expose `state.aliases` or support egress subs
706712
AgentCard-only checkout aliases are a separate integration, not a recovery path after a failed
707713
or indeterminate fill.
708714

715+
##### Invoke WebMCP tools with vault fields
716+
717+
`webmcp_invoke` calls a live WebMCP tool with values from a credential item or ready Link card,
718+
when the item advertises it. Discover the tool first, then pass its opaque `tool_ref` and exact
719+
`source.page_url` (fragment omitted). The vault must already be attached to the browser. Put
720+
`null` at each input slot a vault field fills; `--input` holds only public tool arguments, never
721+
vault values:
722+
723+
```bash
724+
kernel browsers webmcp list <browser-id> -o json
725+
kernel vaults items webmcp invoke user-vault resy \
726+
--browser-id <browser-id> --tool-ref <tool-ref> --page-url https://resy.com/login \
727+
--input '{"email":null,"password":null}' \
728+
--bind email=/email --bind password=/password -o json
729+
```
730+
731+
- Each `--bind <field>=<json-pointer>` maps one vault field to an RFC 6901 JSON Pointer to an existing
732+
`null` in input (no root or `-` append paths; array indices must be canonical and in range). Each
733+
field and path may be bound once; 1-32 bindings. Card expiration uses
734+
`--bind expiration:MM/YY=/card/expiry` (or `MM/YYYY`); other fields take no format. TOTP fields
735+
supply a fresh code, never the seed.
736+
- `--timeout-sec` is 1-120 (API default 15). Input numbers are sent unchanged; input is capped at 64 KiB.
737+
- The equivalent request body for `items invoke <vault> <key> webmcp_invoke --params`/`--spec-file` is
738+
`{"browser_id":"...","tool_ref":"...","page_url":"...","input":{...},"bindings":[{"field":"email","input_path":"/email"}],"timeout_sec":15}`.
739+
740+
The tool may submit forms or perform other side effects. The result returns `status`,
741+
`invocation_id`, `output`, and `error_text` as the API returns them; `output` and `error_text`
742+
are untrusted page-provided data and may contain the supplied vault values:
743+
744+
```json
745+
{"type":"webmcp_invoke","status":"completed","invocation_id":"invoke-1","output":{"authenticated":true}}
746+
```
747+
748+
`completed` and `awaiting_submission` exit 0; neither confirms the website accepted the action.
749+
After `awaiting_submission`, submit the populated form through Playwright or computer interaction
750+
instead of invoking the tool again. `canceled`, `error`, and `unknown` exit nonzero with the result
751+
still on stdout in `-o json`. `unknown` means the tool may have run. Requests are never retried:
752+
API rejections (400/403/404/409) mean the tool was not invoked by that request; other failures
753+
and transport loss are uncertain, so inspect the browser instead of re-invoking.
754+
709755
#### Expansions, updates, and lifecycle
710756

711757
`--expand` takes a value, such as `--expand payment_methods`; it is not a boolean switch.

‎cmd/browsers_webmcp.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput)
6363
return nil
6464
}
6565

66+
const webMCPAwaitingSubmissionHint = "Inspect the form and obtain any required confirmation, then submit it with 'kernel browsers playwright execute' or 'kernel browsers computer' rather than invoking the tool again."
67+
6668
func (b BrowsersCmd) WebMCPInvoke(ctx context.Context, in BrowsersWebMCPInvokeInput) error {
6769
if strings.TrimSpace(in.ToolRef) == "" {
6870
return fmt.Errorf("missing --tool-ref value")
@@ -94,7 +96,7 @@ func (b BrowsersCmd) WebMCPInvoke(ctx context.Context, in BrowsersWebMCPInvokeIn
9496
case kernel.InvocationResultStatusAwaitingSubmission:
9597
// A populated form is a result, not a failure. Re-invoking would refill the
9698
// same fields, so point the caller at submitting the form they already have.
97-
pterm.Warning.Printfln("WebMCP invocation %s: awaiting_submission — populated a form without submitting it. Inspect the form and obtain any required confirmation, then submit it with 'kernel browsers playwright execute' or 'kernel browsers computer' rather than invoking the tool again.", res.InvocationID)
99+
pterm.Warning.Printfln("WebMCP invocation %s: awaiting_submission — populated a form without submitting it. %s", res.InvocationID, webMCPAwaitingSubmissionHint)
98100
default:
99101
return fmt.Errorf("WebMCP invocation %s: %s: %s", res.InvocationID, res.Status, res.ErrorText)
100102
}

‎cmd/vaults.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
233233
if operation == "fill" && (params == nil || params.Fill == nil || open) {
234234
return fmt.Errorf("fill requires --params and does not support --open")
235235
}
236+
if operation == "webmcp_invoke" && (params == nil || params.WebMCP == nil || open) {
237+
return fmt.Errorf("webmcp_invoke requires --params and does not support --open")
238+
}
236239
if operation == "prepare_checkout" && (params == nil || params.Checkout == nil) {
237240
return fmt.Errorf("prepare_checkout requires checkout parameters")
238241
}
@@ -241,7 +244,7 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
241244
}
242245
item, err := c.vaults.Items.Get(ctx, key, kernel.VaultItemGetParams{IDOrName: vault}, option.WithMaxRetries(0))
243246
if err != nil {
244-
if operation == "fill" || operation == "1pw_fill" {
247+
if operation == "fill" || operation == "1pw_fill" || operation == "webmcp_invoke" {
245248
return vaultFillLookupError(err, operation)
246249
}
247250
return util.CleanedUpSdkError{Err: err}
@@ -278,6 +281,9 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
278281
if operation == "1pw_fill" {
279282
return c.onePasswordFill(ctx, vault, key, params.OnePassword, output)
280283
}
284+
if operation == "webmcp_invoke" {
285+
return c.webMCPInvoke(ctx, vault, key, params.WebMCP, output)
286+
}
281287
request := kernel.VaultItemPerformOperationParams{IDOrName: vault}
282288
if params != nil && params.OnePassword != nil {
283289
request = *params.OnePassword

‎cmd/vaults_commands.go‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,18 @@ Use wallet and card item types for credit cards and payment checkout instead.
5959
6060
` + vaultCredentialPathsHelp + `
6161
62-
Kernel-hosted credential flow (fill never submits website forms):
62+
Kernel-hosted credential flow (fill never submits website forms; WebMCP tools may):
6363
1. Create a vault per end user and create a browser with --vault <id-or-name>.
6464
2. Navigate to a sensitive form and define its fields in natural top-to-bottom order with credentials create --spec-file; that array order controls the user-facing collection form.
6565
3. Present the returned collection URL to the user. Poll items get --wait 60 for ready.
66-
4. Use items invoke <vault> <key> fill --spec-file with browser_id and field selectors.
66+
4. Once ready, for ordinary web forms use items invoke <vault> <key> fill --spec-file with
67+
browser_id and field selectors (writes fields without submitting). When the item
68+
advertises webmcp_invoke, items webmcp invoke instead binds credential fields to existing
69+
null inputs of a live WebMCP tool (the tool may submit or have side effects).
70+
Never automatically retry either; inspect the browser after an uncertain outcome.
6771
Use credentials update --version for edits, or items invoke collect to reopen the form.
6872
Credential values belong in protected files/stdin, never command-line arguments.
69-
See credentials --help and items invoke --help for examples.
73+
See credentials --help, items invoke --help, and items webmcp invoke --help for examples.
7074
7175
1Password credential flow: reuse or connect the owner's account with credentials connect,
7276
create a 1password credential for the site's login entries, then invoke only the
@@ -128,7 +132,7 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
128132
addVaultJSONOutputFlag(get)
129133
cmd.AddCommand(create, list, get, newVaultDeleteCommand(false))
130134

131-
items := &cobra.Command{Use: "items", Short: "Inspect readiness and collection URLs, or invoke collect/fill", 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.\n1Password credentials use the advertised 1pw_* operations instead of collect/fill.\nCreate and edit credentials with vaults credentials; payment items use wallets/cards."}
135+
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."}
132136
itemList := &cobra.Command{Use: "list <vault>", Short: "List items by vault ID or name", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun,
133137
RunE: func(cmd *cobra.Command, args []string) error {
134138
return getVaultsHandler(cmd).ListItems(cmd.Context(), args[0], vaultOutput(cmd))
@@ -196,6 +200,17 @@ Preparations are single-use, including after failure or expiry; never retry auto
196200
collect/authorize/prepare_checkout/1pw_recover may use --open. Fill returns value-free per-field outcomes;
197201
completed exits 0, failed/unknown exit nonzero with valid JSON retained on stdout in -o json.
198202
203+
webmcp_invoke (credential items and ready Link cards when advertised; see items webmcp invoke --help
204+
for the flag-based form) requires browser_id, the opaque live tool_ref and exact source.page_url
205+
from browsers webmcp list, input (public JSON object with a null at each bound slot; never vault
206+
values), and 1-32 bindings (field, input_path as an RFC 6901 JSON Pointer to an existing null in
207+
input; format MM/YY or MM/YYYY only for card expiration). Optional timeout_sec is 1-120 (default 15).
208+
The tool may submit or have other side effects. The result returns status, invocation_id, output,
209+
and error_text; output and error_text are untrusted page data and may contain supplied values.
210+
completed/awaiting_submission exit 0 without confirming site acceptance; canceled/error/unknown
211+
exit nonzero with JSON retained on stdout. unknown means the tool may have run: inspect the browser
212+
and never retry automatically. 400/403/404/409 rejections mean the tool was not invoked.
213+
199214
1Password credentials (see credentials --help) use --params or --spec-file without type:
200215
1pw_create_access_request: browser_id (vault-bound session ID); optional goal (<=140),
201216
reason (<=100), keywords (1-5 strings); reason and keywords only for single-entry
@@ -218,6 +233,9 @@ and recreate an item to reset an uncertain outcome; stop and tell the user inste
218233
Example: ` kernel vaults items invoke user-vault login collect
219234
kernel vaults items invoke user-vault login fill --spec-file - <<'JSON'
220235
{"browser_id":"<browser-id>","fields":[{"field":"username","selector":"#username"},{"field":"password","selector":"#password"}]}
236+
JSON
237+
kernel vaults items invoke user-vault login webmcp_invoke --spec-file - <<'JSON'
238+
{"browser_id":"<browser-id>","tool_ref":"<tool-ref>","page_url":"https://example.com/login","input":{"email":null,"password":null},"bindings":[{"field":"email","input_path":"/email"},{"field":"password","input_path":"/password"}]}
221239
JSON
222240
kernel vaults items invoke user-vault github 1pw_create_access_request --params '{"browser_id":"<browser-id>","reason":"Sign in to GitHub"}'
223241
kernel vaults items invoke user-vault github 1pw_access_request_status --params '{"browser_id":"<browser-id>","timeout_seconds":60}'
@@ -233,9 +251,9 @@ JSON
233251
}
234252
if cmd.Flags().Changed("spec-file") {
235253
if !vaultOperationTakesParams(args[2]) {
236-
return fmt.Errorf("--spec-file is only supported for fill, prepare_checkout, and 1Password operations with parameters")
254+
return fmt.Errorf("--spec-file is only supported for fill, webmcp_invoke, prepare_checkout, and 1Password operations with parameters")
237255
}
238-
data, err := readVaultSpecFile(cmd)
256+
data, err := readVaultJSONFile(cmd, "spec-file")
239257
if err != nil {
240258
return err
241259
}
@@ -247,12 +265,12 @@ JSON
247265
}
248266
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], params, vaultOutput(cmd), open)
249267
}}
250-
invoke.Flags().String("params", "", "Operation parameters JSON for fill, prepare_checkout, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file")
268+
invoke.Flags().String("params", "", "Operation parameters JSON for fill, webmcp_invoke, prepare_checkout, or 1pw_* (maximum 128 KiB); omit type and credential values; 1pw_update_access_token requires --spec-file")
251269
invoke.Flags().String("spec-file", "", "Operation parameters JSON file (use '-' for stdin; maximum 128 KiB)")
252270
invoke.MarkFlagsMutuallyExclusive("params", "spec-file")
253271
invoke.Flags().Bool("open", false, "Open a returned HTTPS action URL in your browser")
254272
addVaultJSONOutputFlag(invoke)
255-
items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultDeleteCommand(true))
273+
items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultWebMCPCommand(), newVaultDeleteCommand(true))
256274

257275
wallets := &cobra.Command{Use: "wallets", Short: "Connect provider wallets and inspect funding methods"}
258276
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,

‎cmd/vaults_credential_steering_test.go‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,40 @@ func TestCredentialHelpSteering(t *testing.T) {
4040
assert.Contains(t, cmd.Example, `"label":"Username"`)
4141
assert.Contains(t, cmd.Example, `"sensitive":false`)
4242
}
43+
44+
func TestCredentialHelpReadyOperations(t *testing.T) {
45+
create, _, err := newVaultsCommand().Find([]string{"credentials", "create"})
46+
require.NoError(t, err)
47+
group, _, err := newVaultsCommand().Find([]string{"credentials"})
48+
require.NoError(t, err)
49+
items, _, err := newVaultsCommand().Find([]string{"items"})
50+
require.NoError(t, err)
51+
flat := func(s string) string { return strings.Join(strings.Fields(s), " ") }
52+
for name, long := range map[string]string{"vaults": newVaultsCommand().Long, "credentials": group.Long, "credentials create": create.Long} {
53+
t.Run(name, func(t *testing.T) {
54+
text := flat(long)
55+
paths := flat(vaultCredentialPathsHelp)
56+
assert.Contains(t, text, paths)
57+
assert.Contains(t, paths, "Once ready, items invoke fill writes them into ordinary web form fields in a vault-bound browser without submitting")
58+
assert.Contains(t, paths, "When the item advertises webmcp_invoke, items webmcp invoke instead binds credential fields to existing null inputs of a live WebMCP tool; the tool may submit or have other side effects")
59+
assert.Contains(t, paths, "1pw_fill fills and submits through the 1Password extension")
60+
assert.Contains(t, text, "Never automatically retry")
61+
})
62+
}
63+
text := flat(create.Long)
64+
assert.Contains(t, text, "Ordinary web forms: items invoke fill writes field values without submitting")
65+
assert.Contains(t, text, "Live WebMCP tool: when webmcp_invoke is advertised, run browsers webmcp list, then items webmcp invoke")
66+
assert.Contains(t, text, "Never automatically retry fill or webmcp_invoke")
67+
assert.Contains(t, text, "approves each access request in the 1Password app")
68+
69+
vaults := flat(newVaultsCommand().Long)
70+
assert.Contains(t, vaults, "for ordinary web forms use items invoke <vault> <key> fill --spec-file")
71+
assert.Contains(t, vaults, "When the item advertises webmcp_invoke, items webmcp invoke instead binds credential fields to existing null inputs of a live WebMCP tool (the tool may submit or have side effects)")
72+
assert.Contains(t, vaults, "items webmcp invoke --help")
73+
assert.Contains(t, items.Long, "1Password credentials use the advertised 1pw_* operations instead of collect/fill/webmcp_invoke")
74+
75+
// The 1Password path keeps its own operations and does not advertise webmcp_invoke.
76+
onePassword := flat(vaultOnePasswordCredentialHelp)
77+
assert.NotContains(t, onePassword, "webmcp")
78+
assert.Contains(t, onePassword, "Never automatically retry an access request, fill, or recovery")
79+
}

0 commit comments

Comments
 (0)