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
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Commands with JSON output support:
- `--telemetry=all` - Enable telemetry for all categories
- `--telemetry=off` - Disable telemetry
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
- `--telemetry-export-otlp <id-or-name>` - Export captured telemetry over OTLP to one of the org's configured destinations. Implies `--telemetry=all` when `--telemetry` is not set, since export requires capture. Use `--telemetry-export-otlp=off` to disable export.
- `--chrome-policy <json>` - Custom Chrome enterprise policy as a JSON object. Kernel-managed policies (extensions, proxy, automation) are rejected server-side.
- `--chrome-policy-file <path>` - Read the Chrome enterprise policy from a file (use `-` for stdin). Mutually exclusive with `--chrome-policy`.
- `--output json`, `-o json` - Output raw JSON object
Expand Down Expand Up @@ -320,6 +321,16 @@ Telemetry config is a sub-field of the browser session. Use `browsers create` or

Per-category updates are partial — only categories you name are changed; others retain their current state. `--telemetry=all` and `--telemetry=off` reset the entire config.

#### Exporting telemetry

Captured telemetry can be exported over OTLP to one of the org's configured destinations with `--telemetry-export-otlp <id-or-name>`. A value that looks like an ID is sent as one; anything else is resolved as a destination name, which must match exactly one destination in the org.

- Capture and export: `kernel browsers create --telemetry-export-otlp my-collector`
- Capture without exporting: `kernel browsers create --telemetry=all`
- Stop exporting: `--telemetry-export-otlp=off`

Export is bound at session creation, so it is available on `browsers create` and on the managed-auth commands that create a browser (`auth connections create`, `update`, and `login`). A browser session keeps the destination it was created with — `browsers update` cannot change it — and browser pools do not support export.

- `kernel browsers telemetry stream <id>` - Stream live telemetry events (NDJSON with `-o json`)
- `--categories <list>` - Filter by event category (`console`, `network`, `page`, `interaction`, `control`, `connection`, `system`, `screenshot`, `captcha`, `monitor`)
- `--types <list>` - Filter by event type (e.g. `network_response`, `console_error`)
Expand Down Expand Up @@ -536,12 +547,15 @@ Managed auth connections (`kernel auth connections`). The commands below are new
- `--page <n>` - Page number (1-based, default: 1)
- `--per-page <n>` - Items per page (default: 20)
- `--output json`, `-o json` - Output raw JSON array
- `kernel auth connections create` - New flag:
- `kernel auth connections create` - New flags:
- `--telemetry=all` / `--telemetry=off` / `--telemetry=<categories>` - Default telemetry for this connection's browser sessions. Same semantics as `kernel browsers create`
- `kernel auth connections update <id>` - New flag:
- `--telemetry-export-otlp <id-or-name>` - Export this connection's captured telemetry over OTLP to one of the org's configured destinations. Implies `--telemetry=all` when `--telemetry` is not set. Use `=off` to disable export.
- `kernel auth connections update <id>` - New flags:
- `--telemetry=all` / `--telemetry=off` / `--telemetry=<categories>` - Update telemetry for future browser sessions
- `kernel auth connections login <id>` - New flag:
- `--telemetry-export-otlp <id-or-name>` - Update where future sessions export captured telemetry. Naming a destination requires passing `--telemetry` in the same command, since the API validates capture and export together and enabling capture here would replace the connection's current category selection. Use `=off` to disable export.
- `kernel auth connections login <id>` - New flags:
- `--telemetry=all` / `--telemetry=off` / `--telemetry=<categories>` - Telemetry override for this login only, merged onto the connection's config
- `--telemetry-export-otlp <id-or-name>` - Export override for this login only. Naming a destination requires passing `--telemetry` in the same command. Use `=off` to disable export.
- `kernel auth connections submit <id>` - New flags:
- `--field-value <id=value>` - Canonical field-id=value pair from the connection's `fields` list (repeatable); preferred over the legacy `--field`
- `--choice-id <id>` - Canonical choice ID from the connection's `choices` list
Expand Down
96 changes: 87 additions & 9 deletions cmd/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ type APIKeysCreateInput struct {
}

type APIKeysListInput struct {
Limit int
Offset int
Output string
Limit int
Offset int
Name string
Query string
Status string
IncludeDeleted bool
SortBy string
SortDirection string
Output string
}

type APIKeysGetInput struct {
Expand Down Expand Up @@ -116,6 +122,50 @@ func (c APIKeysCmd) List(ctx context.Context, in APIKeysListInput) error {
if in.Offset > 0 {
params.Offset = kernel.Int(int64(in.Offset))
}
if in.Name != "" {
params.Name = kernel.String(in.Name)
}
if in.Query != "" {
params.Query = kernel.String(in.Query)
}
// Prefer the newer --status filter; fall back to the deprecated
// --include-deleted so existing scripts keep working.
if in.Status != "" {
switch in.Status {
case "active":
params.Status = kernel.APIKeyListParamsStatusActive
case "deleted":
params.Status = kernel.APIKeyListParamsStatusDeleted
case "all":
params.Status = kernel.APIKeyListParamsStatusAll
default:
return fmt.Errorf("invalid --status value: %s (must be 'active', 'deleted', or 'all')", in.Status)
}
} else if in.IncludeDeleted {
params.IncludeDeleted = kernel.Opt(true)
}
if in.SortBy != "" {
switch in.SortBy {
case "created_at":
params.SortBy = kernel.APIKeyListParamsSortByCreatedAt
case "name":
params.SortBy = kernel.APIKeyListParamsSortByName
case "expires_at":
params.SortBy = kernel.APIKeyListParamsSortByExpiresAt
default:
return fmt.Errorf("invalid --sort-by value: %s (must be 'created_at', 'name', or 'expires_at')", in.SortBy)
}
}
if in.SortDirection != "" {
switch in.SortDirection {
case "asc":
params.SortDirection = kernel.APIKeyListParamsSortDirectionAsc
case "desc":
params.SortDirection = kernel.APIKeyListParamsSortDirectionDesc
default:
return fmt.Errorf("invalid --sort-direction value: %s (must be 'asc' or 'desc')", in.SortDirection)
}
}

page, err := c.apiKeys.List(ctx, params)
if err != nil {
Expand All @@ -136,17 +186,27 @@ func (c APIKeysCmd) List(ctx context.Context, in APIKeysListInput) error {
return nil
}

table := pterm.TableData{{"ID", "Name", "Scope", "Project", "Masked Key", "Expires At", "Created At"}}
// Only surface Deleted At when the filter can actually return deleted keys.
showDeletedAt := in.IncludeDeleted || in.Status == "deleted" || in.Status == "all"
header := []string{"ID", "Name", "Scope", "Project", "Masked Key", "Expires At", "Created At"}
if showDeletedAt {
header = append(header, "Deleted At")
}
table := pterm.TableData{header}
for _, key := range keys {
table = append(table, []string{
row := []string{
key.ID,
key.Name,
formatAPIKeyScope(key),
formatAPIKeyProject(key),
key.MaskedKey,
formatAPIKeyExpiresAt(key),
util.FormatLocal(key.CreatedAt),
})
}
if showDeletedAt {
row = append(row, util.FormatLocal(key.DeletedAt))
}
table = append(table, row)
}
PrintTableNoPad(table, true)
return nil
Expand Down Expand Up @@ -362,11 +422,23 @@ func runAPIKeysList(cmd *cobra.Command, args []string) error {
c := getAPIKeysHandler(cmd)
limit, _ := cmd.Flags().GetInt("limit")
offset, _ := cmd.Flags().GetInt("offset")
name, _ := cmd.Flags().GetString("name")
query, _ := cmd.Flags().GetString("query")
status, _ := cmd.Flags().GetString("status")
includeDeleted, _ := cmd.Flags().GetBool("include-deleted")
sortBy, _ := cmd.Flags().GetString("sort-by")
sortDirection, _ := cmd.Flags().GetString("sort-direction")
output, _ := cmd.Flags().GetString("output")
return c.List(cmd.Context(), APIKeysListInput{
Limit: limit,
Offset: offset,
Output: output,
Limit: limit,
Offset: offset,
Name: name,
Query: query,
Status: status,
IncludeDeleted: includeDeleted,
SortBy: sortBy,
SortDirection: sortDirection,
Output: output,
})
}

Expand Down Expand Up @@ -468,6 +540,12 @@ func init() {
addJSONOutputFlag(apiKeysListCmd)
apiKeysListCmd.Flags().Int("limit", 0, "Maximum number of results to return")
apiKeysListCmd.Flags().Int("offset", 0, "Number of results to skip")
apiKeysListCmd.Flags().String("name", "", "Exact-match filter on API key name (names are not unique, so several keys may match)")
apiKeysListCmd.Flags().String("query", "", "Search API keys by name, creator, or project (identifiers and masked keys match by exact value or prefix)")
apiKeysListCmd.Flags().String("status", "", "Filter by status: 'active' (default), 'deleted', or 'all'")
apiKeysListCmd.Flags().Bool("include-deleted", false, "Deprecated: Use --status all instead. Include soft-deleted API keys in the results")
apiKeysListCmd.Flags().String("sort-by", "", "Sort by: created_at, name, or expires_at")
apiKeysListCmd.Flags().String("sort-direction", "", "Sort direction: asc or desc")

addJSONOutputFlag(apiKeysGetCmd)
apiKeysGetCmd.Flags().Bool("include-deleted", false, "Include soft-deleted API keys in the lookup")
Expand Down
28 changes: 20 additions & 8 deletions cmd/audit_logs_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type AuditLogsDownloadInput struct {
Service string
AuthStrategy string
UserIDs []string
Format string
To string
Force bool
}
Expand All @@ -50,7 +51,7 @@ func (c AuditLogsCmd) Download(ctx context.Context, in AuditLogsDownloadInput) e

outPath := in.To
if outPath == "" {
outPath = defaultAuditLogsDownloadPath(params.Start, params.End)
outPath = defaultAuditLogsDownloadPath(params.Start, params.End, params.Format)
}
partialPath := outPath + ".partial"
out, err := openAuditLogsDownloadOutput(partialPath, outPath, in.Force)
Expand Down Expand Up @@ -202,7 +203,14 @@ func buildAuditLogsDownloadParams(in AuditLogsDownloadInput) (kernel.AuditLogExp

params.Start = start
params.End = end
params.Format = kernel.AuditLogExportChunkParamsFormatJSONLGz
switch in.Format {
case "", "jsonl.gz":
params.Format = kernel.AuditLogExportChunkParamsFormatJSONLGz
case "jsonl":
params.Format = kernel.AuditLogExportChunkParamsFormatJSONL
default:
return params, fmt.Errorf("invalid --format value: %s (must be 'jsonl.gz' or 'jsonl')", in.Format)
}
if in.Search != "" {
params.Search = kernel.String(in.Search)
}
Expand All @@ -222,9 +230,9 @@ func buildAuditLogsDownloadParams(in AuditLogsDownloadInput) (kernel.AuditLogExp
return params, nil
}

func defaultAuditLogsDownloadPath(start, end time.Time) string {
func defaultAuditLogsDownloadPath(start, end time.Time, format kernel.AuditLogExportChunkParamsFormat) string {
const stamp = "20060102"
return fmt.Sprintf("audit-logs-%s-%s.jsonl.gz", start.UTC().Format(stamp), end.UTC().Format(stamp))
return fmt.Sprintf("audit-logs-%s-%s.%s", start.UTC().Format(stamp), end.UTC().Format(stamp), format)
}

// checkAuditLogsDownloadTarget rejects paths that are not replaceable
Expand Down Expand Up @@ -289,20 +297,23 @@ func runAuditLogsDownload(cmd *cobra.Command, args []string) error {
service, _ := cmd.Flags().GetString("service")
authStrategy, _ := cmd.Flags().GetString("auth-strategy")
userIDs, _ := cmd.Flags().GetStringArray("user-id")
format, _ := cmd.Flags().GetString("format")
to, _ := cmd.Flags().GetString("to")
force, _ := cmd.Flags().GetBool("force")

return c.Download(cmd.Context(), AuditLogsDownloadInput{
Start: start, End: end, Search: search, Method: method,
ExcludeMethod: excludeMethod, IncludeGet: includeGet, Service: service,
AuthStrategy: authStrategy, UserIDs: userIDs, To: to, Force: force,
AuthStrategy: authStrategy, UserIDs: userIDs, Format: format,
To: to, Force: force,
})
}

var auditLogsDownloadCmd = &cobra.Command{
Use: "download",
Short: "Download audit logs as gzip-compressed JSONL",
Long: "Download audit logs as gzip-compressed JSONL in verified chunks. The time range is [start, end).\n\n" +
Short: "Download audit logs as JSONL",
Long: "Download audit logs as JSONL in verified chunks. The time range is [start, end).\n\n" +
"Output is gzip-compressed by default; pass --format jsonl for uncompressed JSONL.\n\n" +
"The API allows at most 30 days per download.\n\n" +
"GET requests are excluded by default; pass --include-get to include them.\n\n" +
"The output file is published only after every chunk is downloaded.",
Expand All @@ -321,7 +332,8 @@ func init() {
auditLogsDownloadCmd.Flags().String("service", "", "Filter by service")
auditLogsDownloadCmd.Flags().String("auth-strategy", "", "Filter by authentication strategy")
auditLogsDownloadCmd.Flags().StringArray("user-id", nil, "Filter by user ID (repeatable)")
auditLogsDownloadCmd.Flags().String("to", "", "Output .jsonl.gz file path")
auditLogsDownloadCmd.Flags().String("format", "jsonl.gz", "Export format: jsonl.gz or jsonl")
auditLogsDownloadCmd.Flags().String("to", "", "Output file path (defaults to a name derived from the window and --format)")
auditLogsDownloadCmd.Flags().Bool("force", false, "Overwrite the output file")
_ = auditLogsDownloadCmd.MarkFlagRequired("start")
_ = auditLogsDownloadCmd.MarkFlagRequired("end")
Expand Down
31 changes: 29 additions & 2 deletions cmd/audit_logs_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,36 @@ func TestAuditLogsDownloadDoesNotRetryClientErrors(t *testing.T) {
func TestDefaultAuditLogsDownloadPath(t *testing.T) {
start := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2026, 6, 28, 0, 0, 0, 0, time.UTC)
path := defaultAuditLogsDownloadPath(start, end)

assert.Equal(t, "audit-logs-20260601-20260628.jsonl.gz", path)
gz := defaultAuditLogsDownloadPath(start, end, kernel.AuditLogExportChunkParamsFormatJSONLGz)
assert.Equal(t, "audit-logs-20260601-20260628.jsonl.gz", gz)

plain := defaultAuditLogsDownloadPath(start, end, kernel.AuditLogExportChunkParamsFormatJSONL)
assert.Equal(t, "audit-logs-20260601-20260628.jsonl", plain)
}

func TestAuditLogsDownloadFormatParam(t *testing.T) {
base := AuditLogsDownloadInput{Start: "2026-06-01", End: "2026-06-02"}

for _, tc := range []struct {
format string
want kernel.AuditLogExportChunkParamsFormat
}{
{format: "", want: kernel.AuditLogExportChunkParamsFormatJSONLGz},
{format: "jsonl.gz", want: kernel.AuditLogExportChunkParamsFormatJSONLGz},
{format: "jsonl", want: kernel.AuditLogExportChunkParamsFormatJSONL},
} {
in := base
in.Format = tc.format
params, err := buildAuditLogsDownloadParams(in)
require.NoError(t, err)
assert.Equal(t, tc.want, params.Format)
}

in := base
in.Format = "csv"
_, err := buildAuditLogsDownloadParams(in)
require.ErrorContains(t, err, "invalid --format value")
}

func TestAuditLogsDownloadRejectsBadChunkBeforeWriting(t *testing.T) {
Expand Down
Loading
Loading