Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/preview.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Preview CLI binaries

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: false
- name: Preview SDK read token
id: sdk-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.ADMIN_APP_ID }}
private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
repositories: kernel-go-sdk-staging
permission-contents: read
- name: Download dependencies
env:
GOPRIVATE: github.com/kernel/kernel-go-sdk-staging
GIT_CONFIG_COUNT: '1'
GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.sdk-token.outputs.token }}@github.com/.insteadOf
GIT_CONFIG_VALUE_0: https://github.com/
run: go mod download
- name: Build preview archives
run: bash scripts/build-preview.sh
- uses: actions/upload-artifact@v4
with:
name: kernel-preview-${{ github.event.pull_request.head.sha || github.sha }}
path: dist/preview/*
if-no-files-found: error
retention-days: 14
7 changes: 7 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ jobs:
with:
fetch-depth: 0

- name: Reject preview SDK dependencies
run: |
if grep -q 'kernel-go-sdk-staging' go.mod; then
echo 'Replace the preview SDK pin with a released SDK before publishing.' >&2
exit 1
fi

- name: Set up Go
uses: actions/setup-go@v6
with:
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

jobs:
test:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -21,7 +22,24 @@ jobs:
uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
cache: true
cache: false

- name: Preview SDK read token
id: sdk-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.ADMIN_APP_ID }}
private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }}
repositories: kernel-go-sdk-staging
permission-contents: read

- name: Download dependencies
env:
GOPRIVATE: github.com/kernel/kernel-go-sdk-staging
GIT_CONFIG_COUNT: '1'
GIT_CONFIG_KEY_0: url.https://x-access-token:${{ steps.sdk-token.outputs.token }}@github.com/.insteadOf
GIT_CONFIG_VALUE_0: https://github.com/
run: go mod download

- name: Run tests
run: make test
52 changes: 52 additions & 0 deletions PREVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Preview binaries

The `Preview CLI binaries` workflow builds same-repository pull requests automatically.
It also supports `workflow_dispatch` after the workflow exists on the default branch.
It builds the PR head, not GitHub's synthetic merge commit. Archives cover Linux,
macOS, and Windows on amd64 and arm64, with `SHA256SUMS`. The embedded version is
`0.0.0-preview.g<commit>`; the full commit is embedded as well.

Download a run's artifact (GitHub authentication required):

```sh
gh run list --repo kernel/cli --workflow preview.yaml --branch <branch>
gh run download <run-id> --repo kernel/cli --name kernel-preview-<full-commit> --dir preview
cd preview
sha256sum -c SHA256SUMS
# macOS: shasum -a 256 -c SHA256SUMS
# Extract the archive matching your operating system and architecture.
tar -xzf kernel_0.0.0-preview.g<commit>_linux_amd64.tar.gz
./kernel --version
./kernel vaults credentials --help
./kernel vaults items invoke --help
```

Use the extracted binary explicitly rather than replacing the stable installation.
Set `KERNEL_BASE_URL` and `KERNEL_API_KEY` for your local/test environment before API
calls; do not assume the production API supports preview features.

Artifacts expire after 14 days. No GitHub release, stable tag, npm package, Homebrew
formula, or production deployment is created. macOS binaries are unsigned; Windows
archives contain `kernel.exe`. Download on trusted machines and verify checksums.

## Temporary SDK pin

`go.mod` replaces the normal Go SDK with an immutable STLC preview revision.
The staging SDK repository requires GitHub authentication.
Tests and preview builds use the existing GitHub App credentials to obtain a
contents-read token scoped to `kernel-go-sdk-staging`, solely for `go mod download`.
No token is passed to compilation/tests, no credential config is persisted, and Go
caching is disabled so private module sources are not exported into Actions caches.
Fork PR jobs are skipped while this private dependency is required. No
`pull_request_target` workflow executes untrusted PR code.

Replace the preview pin with the released `github.com/kernel/kernel-go-sdk` version
before merging this preview branch, then remove the temporary SDK-auth steps and restore fork
testing/cache behavior. The stable release workflow rejects the staging SDK pin.

For a local cross-platform build, authenticate Git for the SDK repository, then run:

```sh
GOPRIVATE=github.com/kernel/kernel-go-sdk-staging go mod download
bash scripts/build-preview.sh
```
66 changes: 54 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Commands with JSON output support:
- **Proxies**: `create`, `list`, `get`, `update`, `check`
- **API Keys**: `create`, `list`, `get`, `update`, `rotate`
- **Auth Connections**: `timeline`
- **Vaults**: `create`, `list`, `get`, `items list/get/events/invoke`, `wallets create/payment-methods`, `cards create/update` (display-safe public fields only)
- **Vaults**: `create`, `list`, `get`, `credentials create/update`, `items list/get/events/invoke` (including `collect` and `fill`), `wallets create/payment-methods`, `cards create/update` (display-safe public fields only)
- **Projects**: `update`
- **Org**: `limits get/set`
- **Apps**: `list`, `history`
Expand Down Expand Up @@ -270,7 +270,44 @@ Commands with JSON output support:

### Vaults

Vault commands **prepare and observe payment credentials; they do not submit merchant payments**.
Vault commands **collect user credentials and manage payment credentials; fill does not submit website forms**.

#### User credentials

Create a vault for the end user, attach it when creating a browser, then navigate to the
sensitive form. Define the observed fields without supplying values:

```sh
kernel vaults create --name user-vault
kernel browsers create --vault user-vault
kernel vaults credentials create user-vault login --spec-file - <<'JSON'
{"fields":{"username":{"type":"email","required":true},"password":{"type":"password","required":true}}}
JSON
kernel vaults items get user-vault login --wait 60 -o json
kernel vaults items invoke user-vault login fill --spec-file - <<'JSON'
{"browser_id":"<browser-id>","fields":[{"field":"username","selector":"#username"},{"field":"password","selector":"#password"}]}
JSON
```

Present the returned collection URL to the user before waiting for `ready`. It is a
bearer credential: share it only with that user. Readiness means required values are
populated, not that login succeeded. `fill` requires an already-open page and never
navigates or submits it. Optional `page_url` selects the exact page; cards require it.
Do not automatically retry failed/unknown fills or fall back to aliases.

Use `credentials update <vault> <key> --version <version> --spec-file changes.json`
with a spec such as `{"fields":{"password":{"value":"replacement"}}}`. Keep actual
secrets in protected files or stdin, never shell arguments. Omission preserves values;
null clears supported fields. Field definitions cannot change. Stale versions fail,
without retries. `items invoke <vault> <key> collect` reopens the full form without
clearing values; compare versions to observe edits to already-ready items.

Types are `text`, `email`, `password`, and `totp`. TOTP seeds must be provided through
create/update, never the form; only generated codes enter the browser. Unrestricted
browser access can read filled values. CLI output omits all stored credential values,
including non-sensitive values, and retains definitions, version, and `has_value`.
Credential spec input is capped at 128 KiB; write errors are redacted.

Vault names, item keys, and project ownership are immutable. Optionally select a project with
`--project <id-or-name>` or `KERNEL_PROJECT`; otherwise, the API resolves the project from your
credentials and its defaults (the default project for org-wide credentials, not all projects).
Expand Down Expand Up @@ -485,17 +522,22 @@ advertised. The API controls availability. The CLI additionally refuses invocati
actions in `recovery_required`, even if a stale action or operation was returned.

`authorize` sends `{"type":"authorize"}` without `--params` and returns the updated item,
possibly with a required user action. `--open` is supported only for authorize.
possibly with a required user action. `collect` is also parameterless and returns a credential
collection URL. `--open` is supported for authorize and collect.
The [API spec](https://api.onkernel.com/spec.yaml) also accepts `fill`, with its inputs in
`--params`. The positional operation supplies `type`; including `type` in params is rejected.
`--params` or `--spec-file <path|->` (mutually exclusive, maximum 128 KiB). The positional
operation supplies `type`; including `type` in either input is rejected.
Parameters must be a JSON object without unknown or duplicate properties. There is no
operation `--spec` flag; wallet/card `--spec` flags remain unchanged. New parameterless
operations can still be invoked by name when advertised.

##### Fill checkout fields
##### Fill browser fields

Fill is supported only when advertised by a ready Link card, not AgentCard. It writes stored
card data without returning the values or submitting checkout:
Fill supports credential items and ready Link cards when advertised by the API, not AgentCard.
Both use the same execution and outcome handling. Credential bindings use declared field names,
including TOTP fields, and must omit `format`. Credentials may omit `page_url` only when the API
can resolve a unique page. Card bindings require an exact HTTPS `page_url` and the card fields
listed below. It writes stored values without returning them or submitting the website form:

```bash
kernel vaults items get checkout order-1
Expand All @@ -504,16 +546,16 @@ kernel vaults items invoke checkout order-1 fill --params '{"browser_id":"browse

- `browser_id` is a browser **session ID**, not a reusable browser name. It is sent unchanged;
the CLI does not resolve names.
- `page_url` is the exact current top-level HTTPS URL, including path, query, and fragment,
- For cards, `page_url` is the exact current top-level HTTPS URL, including path, query, and fragment,
without embedded credentials. It must match exactly one open page; no prefix/glob matching.
- `fields` contains 1-32 bindings in write order. Each has `field` and a nonempty CSS
`selector` targeting an editable input/select or its container. The API searches the selected
page and descendants, including payment iframes. Do not supply frame IDs or literal values.
- Stored fields: `number`, `cvc`, `exp_month` (MM), `exp_year` (YYYY), `billing_name`,
- Stored card fields: `number`, `cvc`, `exp_month` (MM), `exp_year` (YYYY), `billing_name`,
`billing_line1`, `billing_line2`, `billing_city`, `billing_state`, `billing_postal_code`,
`billing_country`. Billing fields use the stored address without reformatting; request only
needed fields. Missing requested billing data fails validation before browser writes.
- Combined `expiration` requires `format: "MM/YY"` or `"MM/YYYY"`. Other fields reject `format`.
- Combined card `expiration` requires `format: "MM/YY"` or `"MM/YYYY"`. Other fields reject `format`.
- Optional `timeout_ms` is an integer from 1 to 30000 (default 10000), for the whole operation.

Fill returns an execution result, **not an updated item**. Normal output shows zero-based
Expand All @@ -530,8 +572,8 @@ errors are printed in fill results.

Fill is non-atomic: execution stops at the first failed/unknown field and earlier writes are
not rolled back. `filled` does not mean the site retained or accepted the value; `completed`
does not mean paid. Transport errors do not prove no writes occurred. Inspect the browser
before deciding what to do next. The CLI never retries, submits checkout, or falls back to
does not mean logged in or paid. Transport errors do not prove no writes occurred. Inspect the browser
before deciding what to do next. The CLI never retries, submits website forms, or falls back to
aliases. Returned `state.aliases` remain an alternative for explicitly chosen egress-substitution
integrations, not a recovery path after a failed or indeterminate fill.

Expand Down
2 changes: 1 addition & 1 deletion cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2974,7 +2974,7 @@ func init() {
browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)")
browsersCreateCmd.Flags().String("viewport", "", "Browser viewport size (e.g., 1920x1080@25). Supported: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60, 1280x800@60")
browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list")
browsersCreateCmd.Flags().StringArray("vault", nil, "Project-owned vault ID or name to attach at creation (repeatable, max 20; incompatible with pools)")
browsersCreateCmd.Flags().StringArray("vault", nil, "Vault ID or name to attach for credential/card fill at creation (repeatable, max 20; incompatible with pools; see vaults --help)")
browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)")
browsersCreateCmd.Flags().String("pool-name", "", "Browser pool name to acquire from (mutually exclusive with --pool-id)")
browsersCreateCmd.Flags().String("telemetry", "", "Configure telemetry (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
Expand Down
21 changes: 16 additions & 5 deletions cmd/vaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func (c VaultsCmd) SaveCard(ctx context.Context, vault, key string, spec kernel.
var item *kernel.VaultItemUnion
var err error
if update {
item, err = c.vaults.Items.Update(ctx, key, kernel.VaultItemUpdateParams{IDOrName: vault, Spec: spec}, option.WithMaxRetries(0))
item, err = c.vaults.Items.Update(ctx, key, kernel.VaultItemUpdateParams{IDOrName: vault, OfCard: &kernel.VaultItemUpdateParamsBodyCard{Spec: spec}}, option.WithMaxRetries(0))
} else {
item, err = c.vaults.Items.Upsert(ctx, key, kernel.VaultItemUpsertParams{IDOrName: vault, OfCard: &kernel.VaultItemUpsertParamsBodyCard{Spec: spec}}, option.WithMaxRetries(0))
}
Expand Down Expand Up @@ -241,15 +241,26 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item", operation)
}
if operation == "fill" {
if err := validateVaultFillItem(params, item); err != nil {
return err
}
return c.fill(ctx, vault, key, params, output)
}
// Preserve support for other advertised parameterless operations.
authorize := kernel.VaultItemPerformOperationParamsBodyAuthorize{Type: constant.Authorize(operation)}
response, err := c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, OfAuthorize: &authorize}, option.WithMaxRetries(0))
request := kernel.VaultItemPerformOperationParams{IDOrName: vault}
if operation == "collect" {
request.OfCollect = &kernel.CollectVaultItemOperationRequestParam{Type: "collect"}
} else {
// Preserve support for other advertised parameterless operations.
request.OfAuthorize = &kernel.VaultItemPerformOperationParamsBodyAuthorize{Type: constant.Authorize(operation)}
}
response, err := c.vaults.Items.PerformOperation(ctx, key, request, option.WithMaxRetries(0))
if err != nil {
if item.Type == "credential" {
return vaultCredentialError(err)
}
return util.CleanedUpSdkError{Err: err}
}
if response == nil || (response.Type != "card" && response.Type != "wallet") {
if response == nil || (response.Type != "card" && response.Type != "wallet" && response.Type != "credential") {
return fmt.Errorf("unexpected vault operation response; inspect the item and do not retry")
}
var updated kernel.VaultItemUnion
Expand Down
Loading
Loading