Skip to content

Commit 9f01e1e

Browse files
authored
Add 1Password brokered credential vault commands (#271)
Depends on kernel/kernel#4256; built against kernel-go-sdk v0.114.0, which includes that API. ## Summary - `vaults` and `vaults credentials` help give agents a decision order before any credential is created: 1. Reuse an existing credential found with `items list <vault> -o json`. 2. Otherwise ask the user where the login lives and wait for the answer. The options are Kernel-hosted collection or 1Password brokered approval. The 1Password option only works for the user's own non-shared vault; shared-vault items and passkeys are not supported. 3. If 1Password is unavailable, can't be connected, or is declined, offer Kernel-hosted collection instead of switching silently. The help also says never to ask for passwords, OAuth codes, tokens, or keys in the terminal or chat, and that a submitted form is not proof of sign-in. - `vaults credentials connect <vault> <account-key> --provider 1password` creates or reuses a `credential_account` item with the Kernel-managed OAuth client and shows the 1Password authorization URL. Running it again on a failed or declined link starts a new authorization. If the item advertises `1pw_recover`, invoke that first, then connect again. - `vaults credentials create` sends `provider`. A spec without one defaults to `kernel`: the generated SDK requires the field, and the API still accepts an omitted provider as legacy Kernel input. - A `1password` spec takes `account` (a `credential_account` key in the same vault) and v2 `requests` with 1-5 login entries. - It can instead take stored-token fields: `access_token` + `integration_key`. Supply either `account` or both secrets, never both. The CLI rejects invalid combinations before any API call. - `vaults items invoke` supports `1pw_create_access_request`, `1pw_access_request_status`, `1pw_fill` (with optional `entry_id` to pick one of several approved entries on the same origin), `1pw_update_access_token`, and `1pw_recover`, each only when the item advertises it. - Parameters are strict: unknown or duplicate keys are rejected, and the CLI adds `type` itself. - `1pw_update_access_token` takes only `access_token`, and accepts it only through `--spec-file`/stdin; `--params` is rejected so the token never appears in command-line arguments. - There is no reset or reconcile operation. After an uncertain outcome, the help and errors say to stop and tell the user, not retry or delete and recreate the item. Only after a confirmed `failed` status, and with the user's agreement, may the agent recreate the credential for one new request. - Error guidance for 1Password operations: - 400/403/404: rejected. - 409: not currently available; do not reset. - 503, or 500 `provider_unavailable` on connect: 1Password is unavailable here; offer Kernel-hosted collection. - Any other 5xx or a transport failure: outcome unknown; stop. - Response bodies are never echoed. - Output stays display-safe: - It shows `account`; `access_token` and `integration_key` are never shown, and stored-token credentials display as "stored token (developer-supplied)". - Text output lists request entry IDs for choosing `entry_id`. - `items list` gains a Site column: the description for Kernel credentials, the requested websites for 1Password credentials. - The native `onepassword://grant-brokered-access?access_request_reference=…` link is shown only on a `1password_access_approval` action with exactly that shape. ## Not included - The CLI still sends only the Kernel-managed OAuth client for credential accounts; there is no 1Password vault-provider-config command. - `credentials update` covers Kernel-hosted credentials only. ## SDK `go.mod` requires `github.com/kernel/kernel-go-sdk v0.114.0` (commit `b307cc10fdfab032069c951663312e04d0203b5b`); the temporary private-preview `replace` is gone. Its `vaultitem.go` is identical to the #4256 preview this PR was developed against. ## Validation - `go vet ./...` and `go test ./...` against v0.114.0: all pass. - `cmd/vaults_onepassword_test.go` covers: - exact request bodies for connect, account-backed and stored-token create, and each operation, including `entry_id` and token update - validation with no API call on invalid input: account/secret exclusivity, 1-5 entries, `--params` rejected for token updates, and extra keys on token updates - operation and connect error guidance, including the not-advertised hint - fill outcomes and exit codes - redaction of secrets, response bodies, and forged approval links - the `items list` Site column - the help decision guidance - Not run: `golangci-lint`, and any call against a live API. --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
1 parent 643c26b commit 9f01e1e

12 files changed

Lines changed: 1034 additions & 40 deletions

‎cmd/vaults.go‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,42 @@ func (c VaultsCmd) ListItems(ctx context.Context, vault, output string) error {
137137
pterm.Info.Println("No vault items found")
138138
return nil
139139
}
140-
rows := pterm.TableData{{"Key", "Type", "Provider", "Status", "Action"}}
141-
for _, item := range *items {
140+
filtered, err := vaultSafeJSONSlice(*items, vaultItemFields)
141+
if err != nil {
142+
return err
143+
}
144+
data, err := json.Marshal(filtered)
145+
var safe []kernel.VaultItemUnion
146+
if err != nil || json.Unmarshal(data, &safe) != nil {
147+
return fmt.Errorf("invalid vault item response")
148+
}
149+
rows := pterm.TableData{{"Key", "Type", "Provider", "Status", "Site", "Action"}}
150+
for _, item := range safe {
142151
actions, err := effectiveVaultItemActions(&item)
143152
if err != nil {
144153
return err
145154
}
146-
rows = append(rows, []string{item.Key, item.Type, item.Spec.Provider, item.State.Status, util.OrDash(actions.RequiredAction)})
155+
rows = append(rows, []string{item.Key, item.Type, item.Spec.Provider, item.State.Status, util.OrDash(vaultItemSite(item)), util.OrDash(actions.RequiredAction)})
147156
}
148157
PrintTableNoPad(rows, true)
149158
return nil
150159
}
151160

161+
// vaultItemSite names the site a credential signs in to so agents can reuse it.
162+
func vaultItemSite(item kernel.VaultItemUnion) string {
163+
if item.Type != "credential" {
164+
return ""
165+
}
166+
if item.Spec.Provider != "1password" {
167+
return item.Spec.Description
168+
}
169+
websites := make([]string, 0, len(item.Spec.Requests.Entries))
170+
for _, entry := range item.Spec.Requests.Entries {
171+
websites = append(websites, entry.Parameters.Website)
172+
}
173+
return strings.Join(websites, ", ")
174+
}
175+
152176
func validateVaultWait(wait int64) error {
153177
if wait < 0 || wait > 60 {
154178
return fmt.Errorf("--wait must be between 0 and 60 seconds")
@@ -212,10 +236,13 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
212236
if operation == "prepare_checkout" && (params == nil || params.Checkout == nil) {
213237
return fmt.Errorf("prepare_checkout requires checkout parameters")
214238
}
239+
if isOnePasswordOperation(operation) && (params == nil || params.OnePassword == nil) {
240+
return fmt.Errorf("%s requires its documented parameters", operation)
241+
}
215242
item, err := c.vaults.Items.Get(ctx, key, kernel.VaultItemGetParams{IDOrName: vault}, option.WithMaxRetries(0))
216243
if err != nil {
217-
if operation == "fill" {
218-
return fmt.Errorf("could not retrieve vault item; fill was not invoked")
244+
if operation == "fill" || operation == "1pw_fill" {
245+
return vaultFillLookupError(err, operation)
219246
}
220247
return util.CleanedUpSdkError{Err: err}
221248
}
@@ -240,13 +267,22 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
240267
}
241268
}
242269
if !available {
270+
if isOnePasswordOperation(operation) {
271+
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item with items get -o json. For 1Password credentials, check that the linked credential_account is connected and the credential state; do not delete or recreate the item to reset it", operation)
272+
}
243273
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item", operation)
244274
}
245275
if operation == "fill" {
246276
return c.fill(ctx, vault, key, params.Fill, output)
247277
}
278+
if operation == "1pw_fill" {
279+
return c.onePasswordFill(ctx, vault, key, params.OnePassword, output)
280+
}
248281
request := kernel.VaultItemPerformOperationParams{IDOrName: vault}
249-
if operation == "prepare_checkout" {
282+
if params != nil && params.OnePassword != nil {
283+
request = *params.OnePassword
284+
request.IDOrName = vault
285+
} else if operation == "prepare_checkout" {
250286
if item.Type != "card" || item.Spec.Provider != "agentcard" {
251287
return fmt.Errorf("prepare_checkout requires an AgentCard card")
252288
}
@@ -259,12 +295,15 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
259295
}
260296
response, err := c.vaults.Items.PerformOperation(ctx, key, request, option.WithMaxRetries(0))
261297
if err != nil {
262-
if item.Type == "credential" {
298+
if isOnePasswordOperation(operation) {
299+
return onePasswordOperationError(err, operation)
300+
}
301+
if item.Type == "credential" || item.Type == "credential_account" {
263302
return vaultCredentialError(err)
264303
}
265304
return util.CleanedUpSdkError{Err: err}
266305
}
267-
if response == nil || (response.Type != "card" && response.Type != "wallet" && response.Type != "credential") {
306+
if response == nil || (response.Type != "card" && response.Type != "wallet" && response.Type != "credential" && response.Type != "credential_account") {
268307
return fmt.Errorf("unexpected vault operation response; inspect the item and do not retry")
269308
}
270309
var updated kernel.VaultItemUnion

‎cmd/vaults_commands.go‎

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,14 @@ func vaultPreRun(cmd *cobra.Command, args []string) error {
5252
func newVaultsCommand() *cobra.Command {
5353
cmd := &cobra.Command{
5454
Use: "vaults", Aliases: []string{"vault"}, Short: "Collect user credentials and manage payment credentials",
55-
Long: `Collect user credentials and manage payment credentials; fill never submits website forms.
55+
Long: `Collect user credentials and manage payment credentials.
5656
5757
Do not use credential items to store, collect, or fill credit card data.
5858
Use wallet and card item types for credit cards and payment checkout instead.
5959
60-
User credential flow:
60+
` + vaultCredentialPathsHelp + `
61+
62+
Kernel-hosted credential flow (fill never submits website forms):
6163
1. Create a vault per end user and create a browser with --vault <id-or-name>.
6264
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.
6365
3. Present the returned collection URL to the user. Poll items get --wait 60 for ready.
@@ -66,6 +68,10 @@ Use credentials update --version for edits, or items invoke collect to reopen th
6668
Credential values belong in protected files/stdin, never command-line arguments.
6769
See credentials --help and items invoke --help for examples.
6870
71+
1Password credential flow: reuse or connect the owner's account with credentials connect,
72+
create a 1password credential for the site's login entries, then invoke only the
73+
advertised 1pw_* operations. See credentials --help.
74+
6975
Payment credential flow:
7076
7177
Optionally select a project with --project <id-or-name> or KERNEL_PROJECT.
@@ -122,7 +128,7 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
122128
addVaultJSONOutputFlag(get)
123129
cmd.AddCommand(create, list, get, newVaultDeleteCommand(false))
124130

125-
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.\nCreate and edit credentials with vaults credentials; payment items use wallets/cards."}
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."}
126132
itemList := &cobra.Command{Use: "list <vault>", Short: "List items by vault ID or name", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun,
127133
RunE: func(cmd *cobra.Command, args []string) error {
128134
return getVaultsHandler(cmd).ListItems(cmd.Context(), args[0], vaultOutput(cmd))
@@ -185,20 +191,47 @@ establish test mode; merchant credentials determine it.
185191
Use only when advertised for an AgentCard card. Keep the returned approval page open,
186192
poll until ready_to_submit, then submit native Pay before preparation.expires_at.
187193
Preparations are single-use, including after failure or expiry; never retry automatically.
188-
collect/authorize/prepare_checkout may use --open. Fill returns value-free per-field outcomes;
189-
completed exits 0, failed/unknown exit nonzero with valid JSON retained on stdout in -o json.`,
194+
collect/authorize/prepare_checkout/1pw_recover may use --open. Fill returns value-free per-field outcomes;
195+
completed exits 0, failed/unknown exit nonzero with valid JSON retained on stdout in -o json.
196+
197+
1Password credentials (see credentials --help) use --params or --spec-file without type:
198+
1pw_create_access_request: browser_id (vault-bound session ID); optional goal (<=140),
199+
reason (<=100), keywords (1-5 strings); reason and keywords only for single-entry
200+
credentials. Present the returned onepassword:// approval link and instructions to
201+
the account owner unchanged; invoke it once per credential.
202+
1pw_access_request_status: browser_id; optional timeout_seconds 0-120 (default 10).
203+
Check status after the account owner has the approval link.
204+
1pw_fill: browser_id and the exact page_url of one open login page on a requested
205+
origin; entry_id when several approved entries share that origin; optional
206+
timeout_ms 1-30000. The extension selects fields and submits. fill_submitted exits 0
207+
and does not confirm sign-in; inspect the page. fill_failed and fill_unknown exit
208+
nonzero. After fill_unknown, do not retry in the same browser.
209+
1pw_update_access_token (stored-token credentials only): --spec-file, never --params,
210+
with access_token only.
211+
1pw_recover (credential accounts, no parameters): returns a new 1Password link that
212+
recovers a failed account connection. Share it with the account owner, then run
213+
credentials connect again with the same key; never delete the item to recover.
214+
Never retry 1Password operations after failures or uncertain outcomes, and never delete
215+
and recreate an item to reset an uncertain outcome; stop and tell the user instead.`,
190216
Example: ` kernel vaults items invoke user-vault login collect
191217
kernel vaults items invoke user-vault login fill --spec-file - <<'JSON'
192218
{"browser_id":"<browser-id>","fields":[{"field":"username","selector":"#username"},{"field":"password","selector":"#password"}]}
193219
JSON
220+
kernel vaults items invoke user-vault github 1pw_create_access_request --params '{"browser_id":"<browser-id>","reason":"Sign in to GitHub"}'
221+
kernel vaults items invoke user-vault github 1pw_access_request_status --params '{"browser_id":"<browser-id>","timeout_seconds":60}'
222+
kernel vaults items invoke user-vault github 1pw_fill --params '{"browser_id":"<browser-id>","page_url":"https://github.com/login"}'
223+
kernel vaults items invoke user-vault github 1pw_fill --params '{"browser_id":"<browser-id>","page_url":"https://github.com/login","entry_id":"<entry-id>"}'
194224
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"}]}' -o json`,
195225
RunE: func(cmd *cobra.Command, args []string) error {
196226
open, _ := cmd.Flags().GetBool("open")
197227
raw, _ := cmd.Flags().GetString("params")
198228
paramsSet := cmd.Flags().Changed("params")
229+
if args[2] == "1pw_update_access_token" && paramsSet {
230+
return fmt.Errorf("1pw_update_access_token accepts the access token only through --spec-file <path|->, never command-line arguments")
231+
}
199232
if cmd.Flags().Changed("spec-file") {
200-
if args[2] != "fill" && args[2] != "prepare_checkout" {
201-
return fmt.Errorf("--spec-file is only supported for fill and prepare_checkout")
233+
if !vaultOperationTakesParams(args[2]) {
234+
return fmt.Errorf("--spec-file is only supported for fill, prepare_checkout, and 1Password operations with parameters")
202235
}
203236
data, err := readVaultSpecFile(cmd)
204237
if err != nil {
@@ -212,8 +245,8 @@ JSON
212245
}
213246
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], params, vaultOutput(cmd), open)
214247
}}
215-
invoke.Flags().String("params", "", "Fill or prepare_checkout parameters JSON (maximum 128 KiB); omit type and credential values")
216-
invoke.Flags().String("spec-file", "", "Fill or prepare_checkout parameters JSON file (use '-' for stdin; maximum 128 KiB)")
248+
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")
249+
invoke.Flags().String("spec-file", "", "Operation parameters JSON file (use '-' for stdin; maximum 128 KiB)")
217250
invoke.MarkFlagsMutuallyExclusive("params", "spec-file")
218251
invoke.Flags().Bool("open", false, "Open a returned HTTPS action URL in your browser")
219252
addVaultJSONOutputFlag(invoke)

0 commit comments

Comments
 (0)