Skip to content

Commit ee50065

Browse files
realmroot[bot]jarvis
andauthored
Select Resource Contexts by stable ID (#19)
* feat(toolbox): select contexts by stable ID * fix(toolbox): mark only the selected legacy context --------- Co-authored-by: jarvis <jarvis@agents.realmroot.dev>
1 parent 07e3407 commit ee50065

10 files changed

Lines changed: 147 additions & 95 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ realmroot toolbox
7878
realmroot toolbox github
7979
realmroot toolbox sync github
8080
realmroot toolbox github context
81-
realmroot toolbox github context show realmroot
82-
realmroot toolbox github context use realmroot
83-
realmroot agent request --resource-server github --context realmroot --scope contents:read
81+
realmroot toolbox github context show <context-id>
82+
realmroot toolbox github context use <context-id>
83+
realmroot agent request --resource-server github --context <context-id> --scope contents:read
8484
realmroot toolbox cloudflare --search "list zones"
8585
realmroot toolbox cloudflare --scope zone.read
8686
realmroot toolbox cloudflare --all
@@ -93,7 +93,7 @@ realmroot exec
9393
realmroot exec github
9494
realmroot exec github -- git fetch origin
9595
realmroot exec github -- gh pr list --repo realmroot/realmroot
96-
realmroot exec github --context realmroot -- gh pr merge 42 --repo realmroot/realmroot
96+
realmroot exec github --context <context-id> -- gh pr merge 42 --repo realmroot/realmroot
9797
realmroot exec cloudflare -- wrangler deployments list --name realmroot-adapters
9898
```
9999

@@ -143,7 +143,7 @@ Resource authority or change the selected Context.
143143

144144
Generated operations automatically choose the least-privileged approved offer
145145
inside the selected Context. `agent request`, generated operations, and `exec`
146-
accept `--context <name>` as a one-command override without changing the
146+
accept `--context <context-id>` as a one-command override without changing the
147147
default. `exec` uses all already-approved authority in that Context so opaque
148148
native protocols such as GraphQL work without exposing scope-selection or
149149
credential-selection internals. It never requests or expands authority.

‎internal/catalog/catalog.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type ResourceServer struct {
4141
}
4242

4343
type AuthorizationDetail struct {
44+
ID string `json:"id"`
4445
Name string `json:"name"`
4546
Description string `json:"description,omitempty"`
4647
AuthorizationDetail map[string]any `json:"authorizationDetail"`
@@ -177,14 +178,18 @@ func (c *Client) AuthorizationDetails(ctx context.Context, server ResourceServer
177178
return nil, responseError("list Resource Server authorization details", response.StatusCode(), response.Body)
178179
}
179180
for _, item := range response.JSON200.Items {
181+
id := ""
182+
if item.Id != nil {
183+
id = *item.Id
184+
}
180185
detail := map[string]any{"type": item.AuthorizationDetail.Type}
181186
for name, value := range item.AuthorizationDetail.AdditionalProperties {
182187
detail[name] = value
183188
}
184189
description := ""
185190
description = item.Description
186191
result = append(result, AuthorizationDetail{
187-
Name: item.Name, Description: description, AuthorizationDetail: detail,
192+
ID: id, Name: item.Name, Description: description, AuthorizationDetail: detail,
188193
AccountAuthorizationStatus: string(item.AccountAuthorizationStatus),
189194
AuthorizedScopes: append([]string(nil), item.AuthorizedScopes...),
190195
RequestableScopes: append([]string(nil), item.RequestableScopes...), Metadata: item.Metadata,

‎internal/catalog/command_surface.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ var toolboxCommands = []CommandHelp{
1313

1414
var resourceServerCommands = []CommandHelp{
1515
{Name: "context", Usage: "<resource-server> context", Description: "list available Contexts"},
16-
{Name: "context", Usage: "<resource-server> context show <name>", Description: "show one Context"},
17-
{Name: "context", Usage: "<resource-server> context [use <name>|clear]", Description: "select or clear the default Context"},
16+
{Name: "context", Usage: "<resource-server> context show <context-id>", Description: "show one Context"},
17+
{Name: "context", Usage: "<resource-server> context [use <context-id>|clear]", Description: "select or clear the default Context"},
1818
}
1919

2020
var genericHTTPMethods = []string{"get", "head", "post", "put", "patch", "delete"}

‎internal/cli/cli.go‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (a *App) execCommand() *cobra.Command {
189189
return err
190190
},
191191
}
192-
command.Flags().String("context", "", "Resource Server Context name for this command")
192+
command.Flags().String("context", "", "Resource Server Context ID for this command")
193193
return command
194194
}
195195

@@ -275,7 +275,7 @@ func (a *App) agentCommand() *cobra.Command {
275275
func (a *App) requestCommand() *cobra.Command {
276276
var resourceServer string
277277
var scopes []string
278-
var contextName string
278+
var contextID string
279279
var reason string
280280
var handoff bool
281281
command := &cobra.Command{
@@ -300,7 +300,7 @@ func (a *App) requestCommand() *cobra.Command {
300300
if err != nil {
301301
return err
302302
}
303-
details, err := a.resolveContext(agentService, server, contexts, contextName)
303+
details, err := a.resolveContext(agentService, server, contexts, contextID)
304304
if err != nil {
305305
return err
306306
}
@@ -317,7 +317,7 @@ func (a *App) requestCommand() *cobra.Command {
317317
}
318318
command.Flags().StringVar(&resourceServer, "resource-server", "", "Toolbox Resource Server name, such as github or platform")
319319
command.Flags().StringArrayVar(&scopes, "scope", nil, "exact published scope to request (repeatable)")
320-
command.Flags().StringVar(&contextName, "context", "", "Resource Server Context name for this request")
320+
command.Flags().StringVar(&contextID, "context", "", "Resource Server Context ID for this request")
321321
command.Flags().StringVar(&reason, "reason", "", "controller-facing reason for the request")
322322
command.Flags().BoolVar(&handoff, "handoff", false, "hand the approval URL to a remote controller without opening a browser or waiting")
323323
return command
@@ -381,7 +381,7 @@ func (a *App) toolboxCommand() *cobra.Command {
381381
command.Flags().Bool("include", false, "include response headers")
382382
command.Flags().String("search", "", "find operations by command, summary, method, path, or operation ID")
383383
command.Flags().String("scope", "", "filter a Resource Server overview by published scope")
384-
command.Flags().String("context", "", "Resource Server Context name for this operation")
384+
command.Flags().String("context", "", "Resource Server Context ID for this operation")
385385
command.Flags().Bool("all", false, "show the complete Resource Server inventory")
386386
command.Flags().Bool("no-browser", false, "do not open controller approval pages")
387387
command.Flags().Bool("no-paginate", false, "return only the first page")
@@ -636,22 +636,15 @@ func (a *App) showResourceServer(ctx context.Context, service *agent.Service, cl
636636
if err != nil {
637637
return err
638638
}
639-
overview := buildResourceServerOverview(server, details, inspection.Operations, a.discoveryOptions())
640639
selected, selectedErr := service.SelectedContext(server.ResourceURL)
641640
if selectedErr != nil && !errors.Is(selectedErr, os.ErrNotExist) {
642641
return selectedErr
643642
}
644-
for index := range overview.Contexts {
645-
for _, detail := range details {
646-
if detail.Name == overview.Contexts[index].Name && sameDetails(detail.AuthorizationDetail, selected) {
647-
overview.Contexts[index].Current = true
648-
}
649-
}
650-
}
643+
overview := buildResourceServerOverview(server, details, inspection.Operations, a.discoveryOptions(), selected)
651644
effectiveSelected := selected
652645
var effectiveDetail *catalog.AuthorizationDetail
653646
if a.context != "" {
654-
detail, detailErr := namedContext(details, a.context)
647+
detail, detailErr := contextBySelector(details, a.context)
655648
if detailErr != nil {
656649
return detailErr
657650
}
@@ -886,7 +879,11 @@ func (a *App) printContextSummary(overview resourceServerOverview) {
886879
if item.Current {
887880
current = " (current)"
888881
}
889-
fmt.Fprintf(a.stdout, " %s%s — %s\n", item.Name, current, item.AccountAuthorizationStatus)
882+
if item.ID == "" {
883+
fmt.Fprintf(a.stdout, " %s%s — %s\n", item.Name, current, item.AccountAuthorizationStatus)
884+
continue
885+
}
886+
fmt.Fprintf(a.stdout, " %s %s%s — %s\n", item.ID, item.Name, current, item.AccountAuthorizationStatus)
890887
}
891888
if overview.ContextTruncated {
892889
fmt.Fprintf(a.stdout, " Showing %d of %d Contexts. Run `realmroot toolbox %s context` to show every Context.\n", len(overview.Contexts), overview.ContextCount, overview.ResourceServer.CommandName)

‎internal/cli/cli_test.go‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ func TestToolboxHelpDocumentsLocalCommandSurface(t *testing.T) {
232232
"sync <resource-server>",
233233
"get|head|post|put|patch|delete <resource-server>/<path>",
234234
"<resource-server> context",
235-
"<resource-server> context show <name>",
236-
"<resource-server> context [use <name>|clear]",
235+
"<resource-server> context show <context-id>",
236+
"<resource-server> context [use <context-id>|clear]",
237237
} {
238238
if !strings.Contains(output, expected) {
239239
t.Fatalf("help omitted %q:\n%s", expected, output)
@@ -313,7 +313,7 @@ func TestParseExecFlagsConsumesLogLevelBeforeNativeSeparator(t *testing.T) {
313313
func TestResourceServerContextUsesDisplayContractWithoutRawDetails(t *testing.T) {
314314
// [spec: cli/resource-server-context]
315315
details := []catalog.AuthorizationDetail{{
316-
Name: "realmroot", Description: "Organization GitHub App installation",
316+
ID: "ctx_github_realmroot", Name: "realmroot", Description: "Organization GitHub App installation",
317317
AuthorizationDetail: map[string]any{"type": "github_installation", "installation_id": "42"},
318318
Metadata: map[string]string{"accountType": "Organization"},
319319
AccountAuthorizationStatus: "authorized", AuthorizedScopes: []string{"issues:read"},
@@ -325,7 +325,7 @@ func TestResourceServerContextUsesDisplayContractWithoutRawDetails(t *testing.T)
325325
t.Fatal(err)
326326
}
327327
output := string(encoded)
328-
if !summaries[0].Current || !strings.Contains(output, `"name":"realmroot"`) ||
328+
if !summaries[0].Current || !strings.Contains(output, `"id":"ctx_github_realmroot"`) || !strings.Contains(output, `"name":"realmroot"`) ||
329329
strings.Contains(output, "installation_id") || strings.Contains(output, "authorizationDetail") {
330330
t.Fatalf("Context summaries = %s", output)
331331
}
@@ -859,7 +859,7 @@ func TestSmallResourceServerOverviewIncludesCompleteInventory(t *testing.T) {
859859
details := []catalog.AuthorizationDetail{{Name: "wallet"}}
860860
operations := []restish.OperationInspection{{ID: "showWallet", Command: []string{"wallet", "show"}, Method: "GET"}}
861861

862-
overview := buildResourceServerOverview(server, details, operations, discoveryOptions{})
862+
overview := buildResourceServerOverview(server, details, operations, discoveryOptions{}, nil)
863863

864864
if overview.Mode != overviewModeExpanded || len(overview.Scopes) != 1 || len(overview.Contexts) != 1 || len(overview.Operations) != 1 {
865865
t.Fatalf("overview = %#v", overview)
@@ -870,7 +870,7 @@ func TestLargeResourceServerOverviewIsCompact(t *testing.T) {
870870
server := catalog.ResourceServer{CommandName: "cloudflare", ConnectionScopes: []string{"zone.read"}, Scopes: make([]catalog.Scope, 252)}
871871
operations := makeOperations(2652)
872872

873-
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{})
873+
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{}, nil)
874874

875875
if overview.Mode != overviewModeCompact || len(overview.Scopes) != 0 || len(overview.Operations) != 0 {
876876
t.Fatalf("overview = %#v", overview)
@@ -887,7 +887,7 @@ func TestCompactOverviewKeepsBoundedAuthorizationDetails(t *testing.T) {
887887
details[index] = catalog.AuthorizationDetail{Name: "account", AuthorizationDetail: map[string]any{"type": "cloudflare_account"}}
888888
}
889889

890-
overview := buildResourceServerOverview(server, details, makeOperations(80), discoveryOptions{})
890+
overview := buildResourceServerOverview(server, details, makeOperations(80), discoveryOptions{}, nil)
891891

892892
if overview.Mode != overviewModeCompact || len(overview.Contexts) != maxCompactAuthorization || !overview.ContextTruncated {
893893
t.Fatalf("overview = %#v", overview)
@@ -902,7 +902,7 @@ func TestResourceServerSearchIsBoundedAndKeepsOperationSecurity(t *testing.T) {
902902
operations[index].CredentialAlternatives = [][]restish.CredentialRequirementInspection{{{ID: "oauth2", Needs: []string{"workers-routes.read"}}}}
903903
}
904904

905-
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{Search: "worker routes"})
905+
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{Search: "worker routes"}, nil)
906906

907907
if overview.Mode != overviewModeFiltered || overview.MatchCount != 80 || len(overview.Operations) != maxDiscoveryResults || !overview.Truncated {
908908
t.Fatalf("overview = %#v", overview)
@@ -920,7 +920,7 @@ func TestScopeFilterKeepsOnlyMatchingScopeAlternativesAndHidesCredentialSchemes(
920920
{{ID: "realmrootOidc", Kind: "oauth2", Needs: []string{"metadata:read"}}},
921921
},
922922
}}
923-
overview := buildResourceServerOverview(catalog.ResourceServer{CommandName: "github"}, nil, operations, discoveryOptions{Scope: "contents:read"})
923+
overview := buildResourceServerOverview(catalog.ResourceServer{CommandName: "github"}, nil, operations, discoveryOptions{Scope: "contents:read"}, nil)
924924

925925
if got := operationScopeSummary(overview.Operations[0]); got != "contents:read" {
926926
t.Fatalf("scope summary = %q", got)
@@ -966,7 +966,7 @@ func TestResourceServerAllExplicitlyExpandsLargeInventory(t *testing.T) {
966966
server := catalog.ResourceServer{CommandName: "cloudflare", Scopes: make([]catalog.Scope, 252)}
967967
operations := makeOperations(80)
968968

969-
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{All: true})
969+
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{All: true}, nil)
970970

971971
if overview.Mode != overviewModeExpanded || len(overview.Scopes) != 252 || len(overview.Operations) != 80 || overview.Truncated {
972972
t.Fatalf("overview = %#v", overview)

0 commit comments

Comments
 (0)