Skip to content

Commit c76d318

Browse files
authored
Merge branch 'main' into rgarcia/cua-native-kernel-api
2 parents a7919d2 + 0bb2afc commit c76d318

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1325
-319
lines changed

.cursor/commands/qa.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Here are all valid language + template combinations:
5858
| typescript | openai-computer-use | ts-openai-cua | ts-openai-cua | Yes | OPENAI_API_KEY |
5959
| typescript | gemini-computer-use | ts-gemini-cua | ts-gemini-cua | Yes | GOOGLE_API_KEY |
6060
| typescript | claude-agent-sdk | ts-claude-agent-sdk | ts-claude-agent-sdk | Yes | ANTHROPIC_API_KEY |
61-
| typescript | yutori-computer-use | ts-yutori-cua | ts-yutori-cua | Yes | YUTORI_API_KEY |
61+
| typescript | yutori | ts-yutori-cua | ts-yutori-cua | Yes | YUTORI_API_KEY |
6262

6363
| python | sample-app | py-sample-app | python-basic | No | - |
6464
| python | gemini-computer-use | py-gemini-cua | python-gemini-cua | Yes | GOOGLE_API_KEY |
@@ -68,7 +68,7 @@ Here are all valid language + template combinations:
6868
| python | openai-computer-use | py-openai-cua | python-openai-cua | Yes | OPENAI_API_KEY |
6969
| python | openagi-computer-use | py-openagi-cua | python-openagi-cua | Yes | OAGI_API_KEY |
7070
| python | claude-agent-sdk | py-claude-agent-sdk | py-claude-agent-sdk | Yes | ANTHROPIC_API_KEY |
71-
| python | yutori-computer-use | py-yutori-cua | python-yutori-cua | Yes | YUTORI_API_KEY |
71+
| python | yutori | py-yutori-cua | python-yutori-cua | Yes | YUTORI_API_KEY |
7272

7373
> **Yutori:** Test both default browser and `"kiosk": true` (uses Playwright for goto_url when kiosk is enabled).
7474
@@ -86,7 +86,7 @@ Run each of these (they are non-interactive when all flags are provided):
8686
../bin/kernel create -n ts-openai-cua -l typescript -t openai-computer-use
8787
../bin/kernel create -n ts-gemini-cua -l typescript -t gemini-computer-use
8888
../bin/kernel create -n ts-claude-agent-sdk -l typescript -t claude-agent-sdk
89-
../bin/kernel create -n ts-yutori-cua -l typescript -t yutori-computer-use
89+
../bin/kernel create -n ts-yutori-cua -l typescript -t yutori
9090

9191
# Python templates
9292
../bin/kernel create -n py-sample-app -l python -t sample-app
@@ -97,7 +97,7 @@ Run each of these (they are non-interactive when all flags are provided):
9797
../bin/kernel create -n py-openagi-cua -l python -t openagi-computer-use
9898
../bin/kernel create -n py-claude-agent-sdk -l python -t claude-agent-sdk
9999
../bin/kernel create -n py-gemini-cua -l python -t gemini-computer-use
100-
../bin/kernel create -n py-yutori-cua -l python -t yutori-computer-use
100+
../bin/kernel create -n py-yutori-cua -l python -t yutori
101101
```
102102

103103
## Step 5: Deploy Each Template

AGENTS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Kernel CLI
2+
3+
## Cursor Cloud specific instructions
4+
5+
This is a Go CLI application (no long-running services). Development commands are in the `Makefile`:
6+
7+
- **Build:** `make build` → produces `./bin/kernel`
8+
- **Test:** `make test` → runs `go vet` + `go test ./...`
9+
- **Lint:** `make lint` → runs `golangci-lint run` (requires `golangci-lint` on `PATH`)
10+
11+
### Testing against production
12+
13+
When `KERNEL_API_KEY` is set (provided as a secret), the CLI hits the **production** Kernel API. After `make build`, test with `./bin/kernel`. Useful smoke-test sequence:
14+
15+
1. `./bin/kernel auth status` — verify auth
16+
2. `./bin/kernel app list` — list deployed apps
17+
3. `./bin/kernel browsers create` — create a browser session (remember to delete it after)
18+
4. `./bin/kernel browsers delete <session_id>` — clean up
19+
20+
Be mindful that these operations affect production resources.
21+
22+
### Gotchas
23+
24+
- `golangci-lint` is installed via `go install` to `$(go env GOPATH)/bin`. This directory must be on `PATH` (the update script handles this via `.bashrc`).
25+
- The Makefile's `lint` target uses `|| true`, so it always exits 0 even when lint issues exist. Pre-existing lint warnings (errcheck, staticcheck) are present in the codebase and expected.
26+
- The `go-keyring` dependency requires D-Bus and `libsecret` on Linux. These are pre-installed in the Cloud VM.
27+
- `kernel create` works locally without authentication. Most other commands (`deploy`, `invoke`, `browsers`, etc.) require a `KERNEL_API_KEY` env var or `kernel login` OAuth flow.
28+
- Go module path is `github.com/kernel/cli`. The project requires Go 1.25.0 (specified in `go.mod`).

cmd/auth_connections.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,74 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e
219219
if auth.FlowStep != "" {
220220
tableData = append(tableData, []string{"Flow Step", string(auth.FlowStep)})
221221
}
222+
if len(auth.DiscoveredFields) > 0 {
223+
discoveredFields := make([]string, 0, len(auth.DiscoveredFields))
224+
for _, field := range auth.DiscoveredFields {
225+
fieldName := field.Name
226+
if fieldName == "" {
227+
fieldName = field.Label
228+
} else if field.Label != "" && field.Label != field.Name {
229+
fieldName = fmt.Sprintf("%s (%s)", field.Name, field.Label)
230+
}
231+
232+
fieldMeta := make([]string, 0, 2)
233+
if field.Type != "" {
234+
fieldMeta = append(fieldMeta, field.Type)
235+
}
236+
if field.Required {
237+
fieldMeta = append(fieldMeta, "required")
238+
}
239+
if len(fieldMeta) > 0 {
240+
fieldName = fmt.Sprintf("%s [%s]", fieldName, strings.Join(fieldMeta, ", "))
241+
}
242+
discoveredFields = append(discoveredFields, fieldName)
243+
}
244+
tableData = append(tableData, []string{"Discovered Fields", strings.Join(discoveredFields, "; ")})
245+
}
246+
if len(auth.MfaOptions) > 0 {
247+
mfaOptions := make([]string, 0, len(auth.MfaOptions))
248+
for _, option := range auth.MfaOptions {
249+
optionName := option.Label
250+
if optionName == "" {
251+
optionName = option.Type
252+
} else if option.Type != "" {
253+
optionName = fmt.Sprintf("%s (%s)", option.Label, option.Type)
254+
}
255+
mfaOptions = append(mfaOptions, optionName)
256+
}
257+
tableData = append(tableData, []string{"MFA Options", strings.Join(mfaOptions, "; ")})
258+
}
259+
if len(auth.PendingSSOButtons) > 0 {
260+
pendingSSOButtons := make([]string, 0, len(auth.PendingSSOButtons))
261+
for _, button := range auth.PendingSSOButtons {
262+
buttonLabel := button.Label
263+
if buttonLabel == "" {
264+
buttonLabel = button.Provider
265+
} else if button.Provider != "" {
266+
buttonLabel = fmt.Sprintf("%s (%s)", button.Label, button.Provider)
267+
}
268+
pendingSSOButtons = append(pendingSSOButtons, buttonLabel)
269+
}
270+
tableData = append(tableData, []string{"Pending SSO Buttons", strings.Join(pendingSSOButtons, "; ")})
271+
}
272+
if auth.ExternalActionMessage != "" {
273+
tableData = append(tableData, []string{"External Action", auth.ExternalActionMessage})
274+
}
222275
if auth.HostedURL != "" {
223276
tableData = append(tableData, []string{"Hosted URL", auth.HostedURL})
224277
}
225278
if auth.LiveViewURL != "" {
226279
tableData = append(tableData, []string{"Live View URL", auth.LiveViewURL})
227280
}
281+
if auth.WebsiteError != "" {
282+
tableData = append(tableData, []string{"Website Error", auth.WebsiteError})
283+
}
284+
if !auth.FlowExpiresAt.IsZero() {
285+
tableData = append(tableData, []string{"Flow Expires At", util.FormatLocal(auth.FlowExpiresAt)})
286+
}
287+
if auth.ErrorCode != "" {
288+
tableData = append(tableData, []string{"Error Code", auth.ErrorCode})
289+
}
228290
if auth.ErrorMessage != "" {
229291
tableData = append(tableData, []string{"Error Message", auth.ErrorMessage})
230292
}
@@ -272,6 +334,13 @@ func (c AuthConnectionCmd) List(ctx context.Context, in AuthConnectionListInput)
272334
}
273335

274336
if in.Output == "json" {
337+
if page == nil {
338+
fmt.Println("[]")
339+
return nil
340+
}
341+
if page.RawJSON() != "" {
342+
return util.PrintPrettyJSON(page)
343+
}
275344
if len(auths) == 0 {
276345
fmt.Println("[]")
277346
return nil
@@ -381,6 +450,37 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn
381450
return fmt.Errorf("must provide at least one of: --field, --mfa-option-id, or --sso-button-selector")
382451
}
383452

453+
// Resolve MFA option: the user may pass the label (e.g. "Get a text"), the
454+
// type (e.g. "sms"), or the display string ("Get a text (sms)"). The API
455+
// expects the type, so look up the connection's available options and map
456+
// whatever the user provided to the correct type value.
457+
if hasMfaOption {
458+
conn, err := c.svc.Get(ctx, in.ID)
459+
if err != nil {
460+
return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err)}
461+
}
462+
if len(conn.MfaOptions) > 0 {
463+
resolved := false
464+
for _, opt := range conn.MfaOptions {
465+
displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type)
466+
if strings.EqualFold(in.MfaOptionID, opt.Type) ||
467+
strings.EqualFold(in.MfaOptionID, opt.Label) ||
468+
strings.EqualFold(in.MfaOptionID, displayName) {
469+
in.MfaOptionID = opt.Type
470+
resolved = true
471+
break
472+
}
473+
}
474+
if !resolved {
475+
available := make([]string, 0, len(conn.MfaOptions))
476+
for _, opt := range conn.MfaOptions {
477+
available = append(available, fmt.Sprintf("%s (%s)", opt.Label, opt.Type))
478+
}
479+
return fmt.Errorf("unknown MFA option %q; available: %s", in.MfaOptionID, strings.Join(available, ", "))
480+
}
481+
}
482+
}
483+
384484
params := kernel.AuthConnectionSubmitParams{
385485
SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
386486
Fields: in.FieldValues,

0 commit comments

Comments
 (0)