Skip to content

Commit f22b917

Browse files
authored
Add browser proxy routes to CLI create (#269)
## Summary - Add repeatable, create-only `--proxy-route` with typed SDK request construction and light syntax/count validation. - Display returned proxy routes alongside private hosts in create/get output, including SDK-backed JSON output. - Upgrade kernel-go-sdk to v0.112.0 and adapt WebMCP list handling to its updated SDK types. ## Validation - `go test ./...` and `make test` passed. - `make build` passed; `gofmt` and `git diff --check` clean. - `make lint` ran with 70 existing repo-wide findings (50 errcheck, 3 ineffassign, 16 staticcheck, 1 unused); none on changed lines. - No live API smoke test performed. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes browser session egress configuration at creation time and upgrades the SDK; incorrect routes could misroute traffic, though host matching is API-validated and routes are create-only. > > **Overview** > Adds **create-only** `--proxy-route` to `kernel browsers create`, mapping `HOST[,HOST...]=PROXY` (proxy ID by default, or `id:` / `name:`) into the browser network config with client-side limits (10 routes, 50 hosts per route) and rejection when combined with pool acquire flags. > > **Network request shaping** now omits empty `private_hosts` and only sends `network` when private hosts or proxy routes are present; **create/get** table output adds a **Proxy Routes** row alongside private hosts. > > Bumps **kernel-go-sdk** to v0.112.0 and updates **WebMCP list** for the new tool payload shape (`tool.name`, `readOnlyHint`) and `ListTools` params. README documents the new flag. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 72f0623. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 02ff53f commit f22b917

7 files changed

Lines changed: 213 additions & 15 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ kernel search --request '{"query":"browser automation","include_domains":["examp
255255
- `--kiosk` - Launch browser in kiosk mode
256256
- `--region us-east|eu-west|ap-southeast` - Geographic region for the session. Fixed once the session is created; requires a Start-Up or Enterprise plan and defaults to `us-east`.
257257
- `--private-host <host>` - Destination the browser reaches directly through the session's own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel the session joins (repeatable or comma-separated, max 32). Accepts hostname patterns (`*.example.ts.net`), IPs (`10.1.30.63`, `[fd00::1]`), and private CIDRs (`100.64.0.0/10`). Replaces the default private ranges (RFC1918, `100.64.0.0/10`, `fc00::/7`); omit to keep them. Fixed once the session is created. Unrelated to a proxy's `--bypass-host`, which only chooses between upstream proxy and Kernel-managed direct egress.
258+
- `--proxy-route '<host>[,<host>...]=<proxy>'` - Route matching browser requests through a selected proxy (repeatable, max 10 routes with 1–50 hosts each). Example: `--proxy-route 'api.ipify.org,*.ipify.org=name:my-dc-proxy'`. The proxy is an ID by default; use `id:<id>` or `name:<name>` explicitly. Exact hostnames beat wildcards; longer wildcard suffixes beat shorter ones. `*.example.com` matches subdomains, not `example.com`. Matching ignores case and ports. Unmatched hosts use `--proxy-*` or default egress, while `--start-url` uses the top-level proxy during setup. Routes are create-only and are not available on pool sessions.
258259
- `--start-url <url>` - Initial page to open on launch
259260
- `--proxy-id <id>` / `--proxy-name <name>` - Use that proxy for the session regardless of stealth (mutually exclusive with each other and with `--proxy-mode`)
260261
- `--proxy-mode direct|default` - Egress mode instead of a selected proxy: `direct` for no proxy regardless of stealth, `default` for the stealth-derived default (Kernel's stealth proxy with `--stealth`, direct egress otherwise). Omit all proxy flags to get the default.

‎cmd/browsers.go‎

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,76 @@ func buildNetworkParam(privateHosts []string) (kernel.BrowserNetworkConfigParam,
230230
if len(hosts) > maxPrivateHosts {
231231
return network, fmt.Errorf("too many --private-host entries: %d (maximum %d)", len(hosts), maxPrivateHosts)
232232
}
233-
network.PrivateHosts = hosts
233+
if len(hosts) > 0 {
234+
network.PrivateHosts = hosts
235+
}
234236
return network, nil
235237
}
236238

239+
const (
240+
maxProxyRoutes = 10
241+
maxProxyRouteHosts = 50
242+
)
243+
244+
// parseProxyRoutes converts each --proxy-route HOST[,HOST...]=ID|id:ID|name:NAME
245+
// into the SDK's create-only network parameter. Host semantics are validated by the API.
246+
func parseProxyRoutes(values []string) ([]kernel.BrowserNetworkConfigProxyRouteParam, error) {
247+
if len(values) > maxProxyRoutes {
248+
return nil, fmt.Errorf("too many --proxy-route entries: %d (maximum %d)", len(values), maxProxyRoutes)
249+
}
250+
routes := make([]kernel.BrowserNetworkConfigProxyRouteParam, 0, len(values))
251+
for _, value := range values {
252+
hostList, selector, ok := strings.Cut(value, "=")
253+
if !ok || strings.TrimSpace(selector) == "" || strings.Contains(selector, "=") {
254+
return nil, fmt.Errorf("invalid --proxy-route %q: expected HOST[,HOST...]=PROXY", value)
255+
}
256+
hosts := strings.Split(hostList, ",")
257+
if len(hosts) > maxProxyRouteHosts {
258+
return nil, fmt.Errorf("too many hosts in --proxy-route: %d (maximum %d)", len(hosts), maxProxyRouteHosts)
259+
}
260+
for i, host := range hosts {
261+
hosts[i] = strings.TrimSpace(host)
262+
if hosts[i] == "" {
263+
return nil, fmt.Errorf("invalid --proxy-route %q: hosts must be non-empty", value)
264+
}
265+
}
266+
selector = strings.TrimSpace(selector)
267+
proxy := kernel.BrowserNetworkConfigProxyRouteProxyParam{}
268+
switch {
269+
case strings.HasPrefix(selector, "name:"):
270+
proxy.Name = kernel.Opt(strings.TrimSpace(strings.TrimPrefix(selector, "name:")))
271+
if proxy.Name.Value == "" {
272+
return nil, fmt.Errorf("invalid --proxy-route %q: proxy name must be non-empty", value)
273+
}
274+
case strings.HasPrefix(selector, "id:"):
275+
selector = strings.TrimSpace(strings.TrimPrefix(selector, "id:"))
276+
if selector == "" {
277+
return nil, fmt.Errorf("invalid --proxy-route %q: proxy ID must be non-empty", value)
278+
}
279+
proxy.ID = kernel.Opt(selector)
280+
default:
281+
proxy.ID = kernel.Opt(selector)
282+
}
283+
routes = append(routes, kernel.BrowserNetworkConfigProxyRouteParam{Hosts: hosts, Proxy: proxy})
284+
}
285+
return routes, nil
286+
}
287+
288+
func formatProxyRoutes(network kernel.BrowserNetworkConfig) string {
289+
if len(network.ProxyRoutes) == 0 {
290+
return "-"
291+
}
292+
routes := make([]string, 0, len(network.ProxyRoutes))
293+
for _, route := range network.ProxyRoutes {
294+
proxy := route.Proxy.ID
295+
if proxy == "" {
296+
proxy = route.Proxy.Name
297+
}
298+
routes = append(routes, strings.Join(route.Hosts, ", ")+" = "+proxy)
299+
}
300+
return strings.Join(routes, "; ")
301+
}
302+
237303
// formatPrivateHosts renders a network configuration for table output. A missing
238304
// private_hosts list means the API's default private ranges apply; an explicit
239305
// empty list means nothing routes around Kernel-managed egress.
@@ -394,6 +460,7 @@ type BrowsersCreateInput struct {
394460
ProxyMode string
395461
Region string
396462
PrivateHosts []string
463+
ProxyRoutes []string
397464
StartURL string
398465
Extensions []string
399466
Vaults []string
@@ -660,7 +727,12 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
660727
if err != nil {
661728
return err
662729
}
663-
if len(network.PrivateHosts) > 0 {
730+
routes, err := parseProxyRoutes(in.ProxyRoutes)
731+
if err != nil {
732+
return err
733+
}
734+
network.ProxyRoutes = routes
735+
if len(network.PrivateHosts) > 0 || len(network.ProxyRoutes) > 0 {
664736
params.Network = network
665737
}
666738

@@ -732,7 +804,9 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
732804
return util.PrintPrettyJSON(browser)
733805
}
734806

735-
printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags)
807+
tableData := buildBrowserTableData(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags)
808+
tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)}, []string{"Proxy Routes", formatProxyRoutes(browser.Network)})
809+
PrintTableNoPad(tableData, true)
736810
if len(browser.Vaults) > 0 {
737811
rows := pterm.TableData{{"Attached vault ID", "Name"}}
738812
for _, vault := range browser.Vaults {
@@ -874,6 +948,7 @@ func (b BrowsersCmd) Get(ctx context.Context, in BrowsersGetInput) error {
874948
tableData = append(tableData, []string{"Proxy", proxy})
875949
}
876950
tableData = append(tableData, []string{"Private Hosts", formatPrivateHosts(browser.Network)})
951+
tableData = append(tableData, []string{"Proxy Routes", formatProxyRoutes(browser.Network)})
877952
if vaults := formatVaultReferences(browser.Vaults); vaults != "" {
878953
tableData = append(tableData, []string{"Vaults", vaults})
879954
}
@@ -3172,6 +3247,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`,
31723247
browsersCreateCmd.Flags().String("proxy-mode", "", "Proxy egress mode instead of a selected proxy: 'direct' for no proxy regardless of stealth, or 'default' for the browser default (Kernel's stealth proxy when --stealth is set, direct egress otherwise)")
31733248
browsersCreateCmd.Flags().String("region", "", "Geographic region for the session: 'us-east', 'eu-west', or 'ap-southeast'. Fixed once the session is created; requires a Start-Up or Enterprise plan and defaults to us-east")
31743249
browsersCreateCmd.Flags().StringSlice("private-host", nil, "Destinations the browser reaches directly through its own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel the session joins (repeat or comma-separated, max 32). Accepts hostname patterns ('*.example.ts.net'), IPs ('10.1.30.63', '[fd00::1]'), and private CIDRs ('100.64.0.0/10'). Replaces the default private ranges (RFC1918, 100.64.0.0/10, fc00::/7); omit to keep them. Fixed once the session is created")
3250+
browsersCreateCmd.Flags().StringArray("proxy-route", nil, "Route HOST[,HOST...]=PROXY through a proxy (repeatable, max 10 routes and 50 hosts per route). PROXY is an ID by default; use id:ID or name:NAME explicitly. Exact hosts beat wildcards (longer suffixes win); *.example.com excludes example.com. Unmatched hosts use --proxy-* or default egress; start_url uses the top-level proxy. Create-only")
31753251
browsersCreateCmd.Flags().String("start-url", "", "Initial page to open on launch")
31763252
browsersCreateCmd.Flags().StringSlice("extension", []string{}, "Extension IDs or names to load (repeatable; may be passed multiple times or comma-separated)")
31773253
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")
@@ -3305,6 +3381,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
33053381
proxyMode, _ := cmd.Flags().GetString("proxy-mode")
33063382
region, _ := cmd.Flags().GetString("region")
33073383
privateHosts, _ := cmd.Flags().GetStringSlice("private-host")
3384+
proxyRoutes, _ := cmd.Flags().GetStringArray("proxy-route")
33083385
startURL, _ := cmd.Flags().GetString("start-url")
33093386
extensions, _ := cmd.Flags().GetStringSlice("extension")
33103387
vaults, _ := cmd.Flags().GetStringArray("vault")
@@ -3331,6 +3408,10 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
33313408
}
33323409
}
33333410

3411+
if (poolID != "" || poolName != "") && cmd.Flags().Changed("proxy-route") {
3412+
return fmt.Errorf("--proxy-route cannot be used with --pool-id or --pool-name; routes require a new browser")
3413+
}
3414+
33343415
if poolID != "" && poolName != "" {
33353416
pterm.Error.Println("must specify at most one of --pool-id or --pool-name")
33363417
return nil
@@ -3446,6 +3527,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
34463527
ProxyMode: proxyMode,
34473528
Region: region,
34483529
PrivateHosts: privateHosts,
3530+
ProxyRoutes: proxyRoutes,
34493531
StartURL: startURL,
34503532
Extensions: extensions,
34513533
Vaults: vaults,

‎cmd/browsers_test.go‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,121 @@ func TestBrowsersCreate_WithPrivateHosts(t *testing.T) {
610610
}))
611611
}
612612

613+
func TestParseProxyRoutes(t *testing.T) {
614+
routes, err := parseProxyRoutes([]string{" api.ipify.org , *.ipify.org =name:my-dc-proxy", "other.example=id:proxy-123", "fallback.example=proxy-456"})
615+
require.NoError(t, err)
616+
require.Len(t, routes, 3)
617+
assert.Equal(t, []string{"api.ipify.org", "*.ipify.org"}, routes[0].Hosts)
618+
assert.Equal(t, "my-dc-proxy", routes[0].Proxy.Name.Value)
619+
assert.False(t, routes[0].Proxy.ID.Valid())
620+
assert.Equal(t, "proxy-123", routes[1].Proxy.ID.Value)
621+
assert.Equal(t, "proxy-456", routes[2].Proxy.ID.Value)
622+
assert.False(t, routes[2].Proxy.Name.Valid())
623+
624+
for _, value := range []string{"", "host", "=id:proxy", "host=", "host= id: ", "host=name: ", "host=proxy=other", "host,,other=proxy", "host,=proxy", ",host=proxy"} {
625+
t.Run(value, func(t *testing.T) {
626+
_, err := parseProxyRoutes([]string{value})
627+
assert.Error(t, err)
628+
})
629+
}
630+
_, err = parseProxyRoutes(make([]string, maxProxyRoutes+1))
631+
assert.ErrorContains(t, err, "maximum 10")
632+
atLimit := make([]string, maxProxyRoutes)
633+
for i := range atLimit {
634+
atLimit[i] = fmt.Sprintf("host-%d=proxy", i)
635+
}
636+
_, err = parseProxyRoutes(atLimit)
637+
require.NoError(t, err)
638+
_, err = parseProxyRoutes([]string{strings.Repeat("host,", maxProxyRouteHosts) + "host=proxy"})
639+
assert.ErrorContains(t, err, "maximum 50")
640+
_, err = parseProxyRoutes([]string{strings.Repeat("host,", maxProxyRouteHosts-1) + "host=proxy"})
641+
require.NoError(t, err)
642+
// Hostname and wildcard validity, duplicates, and overlaps belong to the API.
643+
_, err = parseProxyRoutes([]string{"*.com=proxy", "*.com=proxy"})
644+
assert.NoError(t, err)
645+
}
646+
647+
func TestBrowsersCreate_WithProxyRoutes(t *testing.T) {
648+
setupStdoutCapture(t)
649+
var captured kernel.BrowserNewParams
650+
fake := &FakeBrowsersService{
651+
NewFunc: func(_ context.Context, body kernel.BrowserNewParams, _ ...option.RequestOption) (*kernel.BrowserNewResponse, error) {
652+
captured = body
653+
var resp kernel.BrowserNewResponse
654+
err := json.Unmarshal([]byte(`{"session_id":"sess-routes","network":{"private_hosts":["internal.example"],"proxy_routes":[{"hosts":["api.ipify.org","*.ipify.org"],"proxy":{"id":"resolved-proxy"}}]}}`), &resp)
655+
require.NoError(t, err)
656+
return &resp, nil
657+
},
658+
}
659+
b := BrowsersCmd{browsers: fake}
660+
err := b.Create(context.Background(), BrowsersCreateInput{
661+
PrivateHosts: []string{"internal.example"},
662+
ProxyRoutes: []string{"api.ipify.org,*.ipify.org=name:my-dc-proxy", "other.example=proxy-456"},
663+
ProxyMode: "direct",
664+
})
665+
require.NoError(t, err)
666+
require.Len(t, captured.Network.ProxyRoutes, 2)
667+
assert.Equal(t, []string{"api.ipify.org", "*.ipify.org"}, captured.Network.ProxyRoutes[0].Hosts)
668+
assert.Equal(t, "my-dc-proxy", captured.Network.ProxyRoutes[0].Proxy.Name.Value)
669+
assert.False(t, captured.Network.ProxyRoutes[0].Proxy.ID.Valid())
670+
assert.Equal(t, "proxy-456", captured.Network.ProxyRoutes[1].Proxy.ID.Value)
671+
assert.Equal(t, []string{"internal.example"}, captured.Network.PrivateHosts)
672+
assert.Equal(t, kernel.BrowserProxyModeDirect, captured.Proxy.Mode)
673+
raw, err := captured.MarshalJSON()
674+
require.NoError(t, err)
675+
assert.Contains(t, string(raw), `"proxy_routes":[{"hosts":["api.ipify.org","*.ipify.org"],"proxy":{"name":"my-dc-proxy"}}`)
676+
assert.Contains(t, outBuf.String(), "Private Hosts")
677+
assert.Contains(t, outBuf.String(), "Proxy Routes")
678+
assert.Contains(t, outBuf.String(), "api.ipify.org, *.ipify.org = resolved-proxy")
679+
680+
jsonOutput := captureStdout(t, func() {
681+
require.NoError(t, b.Create(context.Background(), BrowsersCreateInput{ProxyRoutes: []string{"host=proxy"}, Output: "json"}))
682+
})
683+
assert.Contains(t, jsonOutput, `"proxy_routes"`)
684+
assert.Contains(t, jsonOutput, `"resolved-proxy"`)
685+
assert.Nil(t, captured.Network.PrivateHosts)
686+
assert.Len(t, captured.Network.ProxyRoutes, 1)
687+
raw, err = captured.MarshalJSON()
688+
require.NoError(t, err)
689+
assert.Contains(t, string(raw), `"proxy_routes"`)
690+
assert.NotContains(t, string(raw), `"private_hosts"`)
691+
assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{ProxyRoutes: []string{"host="}}))
692+
}
693+
694+
func TestBrowsersGet_ProxyRoutes(t *testing.T) {
695+
setupStdoutCapture(t)
696+
fake := &FakeBrowsersService{
697+
GetFunc: func(_ context.Context, _ string, _ kernel.BrowserGetParams, _ ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
698+
var resp kernel.BrowserGetResponse
699+
err := json.Unmarshal([]byte(`{"session_id":"sess-routes","network":{"private_hosts":["internal.example"],"proxy_routes":[{"hosts":["api.ipify.org"],"proxy":{"id":"resolved-proxy"}}]}}`), &resp)
700+
require.NoError(t, err)
701+
return &resp, nil
702+
},
703+
}
704+
b := BrowsersCmd{browsers: fake}
705+
require.NoError(t, b.Get(context.Background(), BrowsersGetInput{Identifier: "sess-routes"}))
706+
assert.Contains(t, outBuf.String(), "Private Hosts")
707+
assert.Contains(t, outBuf.String(), "Proxy Routes")
708+
assert.Contains(t, outBuf.String(), "api.ipify.org = resolved-proxy")
709+
jsonOutput := captureStdout(t, func() {
710+
require.NoError(t, b.Get(context.Background(), BrowsersGetInput{Identifier: "sess-routes", Output: "json"}))
711+
})
712+
assert.Contains(t, jsonOutput, `"proxy_routes"`)
713+
assert.Contains(t, jsonOutput, `"resolved-proxy"`)
714+
}
715+
716+
func TestProxyRouteFlagIsCreateOnly(t *testing.T) {
717+
create, _, err := rootCmd.Find([]string{"browsers", "create"})
718+
require.NoError(t, err)
719+
assert.NotNil(t, create.Flags().Lookup("proxy-route"))
720+
for _, path := range [][]string{{"browsers", "update"}, {"browser-pools", "create"}, {"browser-pools", "update"}} {
721+
cmd, _, err := rootCmd.Find(path)
722+
require.NoError(t, err)
723+
assert.Nil(t, cmd.Flags().Lookup("proxy-route"))
724+
}
725+
assert.False(t, poolLeaseAllowedFlags()["proxy-route"])
726+
}
727+
613728
func TestBrowsersCreate_WithRegion(t *testing.T) {
614729
setupStdoutCapture(t)
615730

‎cmd/browsers_webmcp.go‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919

2020
// BrowserWebMCPService defines the subset we use for native page tools.
2121
type BrowserWebMCPService interface {
22-
ListTools(ctx context.Context, idOrName string, opts ...option.RequestOption) (*kernel.ToolsResponse, error)
22+
ListTools(ctx context.Context, idOrName string, query kernel.BrowserWebmcpListToolsParams, opts ...option.RequestOption) (*kernel.ToolsResponse, error)
2323
InvokeTool(ctx context.Context, idOrName string, body kernel.BrowserWebmcpInvokeToolParams, opts ...option.RequestOption) (*kernel.InvocationResult, error)
2424
}
2525

@@ -39,7 +39,7 @@ func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput)
3939
if err := validateJSONOutput(in.Output); err != nil {
4040
return err
4141
}
42-
res, err := b.webmcp.ListTools(ctx, in.Identifier)
42+
res, err := b.webmcp.ListTools(ctx, in.Identifier, kernel.BrowserWebmcpListToolsParams{})
4343
if err != nil {
4444
return util.CleanedUpSdkError{Err: err}
4545
}
@@ -53,10 +53,10 @@ func (b BrowsersCmd) WebMCPList(ctx context.Context, in BrowsersWebMCPListInput)
5353
rows := pterm.TableData{{"Name", "Tool Ref", "Page URL", "Tab ID", "Read Only"}}
5454
for _, tool := range res.Tools {
5555
readOnly := "-"
56-
if tool.Annotations.JSON.ReadOnly.Valid() {
57-
readOnly = strconv.FormatBool(tool.Annotations.ReadOnly)
56+
if tool.Tool.Annotations.JSON.ReadOnlyHint.Valid() {
57+
readOnly = strconv.FormatBool(tool.Tool.Annotations.ReadOnlyHint)
5858
}
59-
rows = append(rows, []string{tool.Name, tool.ToolRef, tool.Source.PageURL, strconv.FormatInt(tool.Source.TabID, 10), readOnly})
59+
rows = append(rows, []string{tool.Tool.Name, tool.ToolRef, tool.Source.PageURL, strconv.FormatInt(tool.Source.TabID, 10), readOnly})
6060
}
6161
PrintTableNoPad(rows, true)
6262
return nil

‎cmd/browsers_webmcp_test.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func executeWebMCPCommand(t *testing.T, handler http.HandlerFunc, stdin string,
3636
return stdout, buf.String(), err
3737
}
3838

39-
const webMCPToolsFixture = `{"tools":[{"name":"search","tool_ref":"opaque/ref+==","description":"Search the page","input_schema":{"type":"object"},"annotations":{"read_only":true,"autosubmit":false,"consequential":false,"untrusted_content":true},"source":{"window_id":1,"tab_id":42,"page_url":"https://example.com","page_title":"Example","frame":null}}],"future_field":true}`
39+
const webMCPToolsFixture = `{"tools":[{"tool":{"name":"search","description":"Search the page","inputSchema":{"type":"object"},"annotations":{"readOnlyHint":true,"autosubmit":false}},"tool_ref":"opaque/ref+==","source":{"window_id":1,"tab_id":42,"page_url":"https://example.com","page_title":"Example","frame":null}}],"future_field":true}`
4040

4141
func TestWebMCPCommandWiring(t *testing.T) {
4242
for _, name := range []string{"list", "invoke"} {
@@ -93,15 +93,15 @@ func TestWebMCPListEmpty(t *testing.T) {
9393

9494
func TestWebMCPListAnnotations(t *testing.T) {
9595
for _, tc := range []struct{ annotation, want string }{
96-
{`{"read_only":true}`, "true"},
97-
{`{"read_only":false}`, "false"},
96+
{`{"readOnlyHint":true}`, "true"},
97+
{`{"readOnlyHint":false}`, "false"},
9898
{`{}`, "-"},
9999
{`null`, "-"},
100100
} {
101101
t.Run(tc.annotation, func(t *testing.T) {
102102
_, table, err := executeWebMCPCommand(t, func(w http.ResponseWriter, r *http.Request) {
103103
w.Header().Set("Content-Type", "application/json")
104-
fmt.Fprintf(w, `{"tools":[{"name":"search","annotations":%s}]}`, tc.annotation)
104+
fmt.Fprintf(w, `{"tools":[{"tool":{"name":"search","annotations":%s}}]}`, tc.annotation)
105105
}, "", "list", "my-browser")
106106
require.NoError(t, err)
107107
rows := strings.Split(strings.TrimSpace(table), "\n")

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
1010
github.com/golang-jwt/jwt/v5 v5.2.2
1111
github.com/joho/godotenv v1.5.1
12-
github.com/kernel/kernel-go-sdk v0.110.0
12+
github.com/kernel/kernel-go-sdk v0.112.0
1313
github.com/klauspost/compress v1.18.5
1414
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1515
github.com/pterm/pterm v0.12.80

0 commit comments

Comments
 (0)