Skip to content
Merged
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ realmroot toolbox
realmroot toolbox github
realmroot toolbox sync github
realmroot toolbox github context
realmroot toolbox github context show realmroot
realmroot toolbox github context use realmroot
realmroot agent request --resource-server github --context realmroot --scope contents:read
realmroot toolbox github context show <context-id>
realmroot toolbox github context use <context-id>
realmroot agent request --resource-server github --context <context-id> --scope contents:read
realmroot toolbox cloudflare --search "list zones"
realmroot toolbox cloudflare --scope zone.read
realmroot toolbox cloudflare --all
Expand All @@ -93,7 +93,7 @@ realmroot exec
realmroot exec github
realmroot exec github -- git fetch origin
realmroot exec github -- gh pr list --repo realmroot/realmroot
realmroot exec github --context realmroot -- gh pr merge 42 --repo realmroot/realmroot
realmroot exec github --context <context-id> -- gh pr merge 42 --repo realmroot/realmroot
realmroot exec cloudflare -- wrangler deployments list --name realmroot-adapters
```

Expand Down Expand Up @@ -143,7 +143,7 @@ Resource authority or change the selected Context.

Generated operations automatically choose the least-privileged approved offer
inside the selected Context. `agent request`, generated operations, and `exec`
accept `--context <name>` as a one-command override without changing the
accept `--context <context-id>` as a one-command override without changing the
default. `exec` uses all already-approved authority in that Context so opaque
native protocols such as GraphQL work without exposing scope-selection or
credential-selection internals. It never requests or expands authority.
Expand Down
7 changes: 6 additions & 1 deletion internal/catalog/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type ResourceServer struct {
}

type AuthorizationDetail struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
AuthorizationDetail map[string]any `json:"authorizationDetail"`
Expand Down Expand Up @@ -177,14 +178,18 @@ func (c *Client) AuthorizationDetails(ctx context.Context, server ResourceServer
return nil, responseError("list Resource Server authorization details", response.StatusCode(), response.Body)
}
for _, item := range response.JSON200.Items {
id := ""
if item.Id != nil {
id = *item.Id
}
detail := map[string]any{"type": item.AuthorizationDetail.Type}
for name, value := range item.AuthorizationDetail.AdditionalProperties {
detail[name] = value
}
description := ""
description = item.Description
result = append(result, AuthorizationDetail{
Name: item.Name, Description: description, AuthorizationDetail: detail,
ID: id, Name: item.Name, Description: description, AuthorizationDetail: detail,
AccountAuthorizationStatus: string(item.AccountAuthorizationStatus),
AuthorizedScopes: append([]string(nil), item.AuthorizedScopes...),
RequestableScopes: append([]string(nil), item.RequestableScopes...), Metadata: item.Metadata,
Expand Down
4 changes: 2 additions & 2 deletions internal/catalog/command_surface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ var toolboxCommands = []CommandHelp{

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

var genericHTTPMethods = []string{"get", "head", "post", "put", "patch", "delete"}
Expand Down
27 changes: 12 additions & 15 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (a *App) execCommand() *cobra.Command {
return err
},
}
command.Flags().String("context", "", "Resource Server Context name for this command")
command.Flags().String("context", "", "Resource Server Context ID for this command")
return command
}

Expand Down Expand Up @@ -275,7 +275,7 @@ func (a *App) agentCommand() *cobra.Command {
func (a *App) requestCommand() *cobra.Command {
var resourceServer string
var scopes []string
var contextName string
var contextID string
var reason string
var handoff bool
command := &cobra.Command{
Expand All @@ -300,7 +300,7 @@ func (a *App) requestCommand() *cobra.Command {
if err != nil {
return err
}
details, err := a.resolveContext(agentService, server, contexts, contextName)
details, err := a.resolveContext(agentService, server, contexts, contextID)
if err != nil {
return err
}
Expand All @@ -317,7 +317,7 @@ func (a *App) requestCommand() *cobra.Command {
}
command.Flags().StringVar(&resourceServer, "resource-server", "", "Toolbox Resource Server name, such as github or platform")
command.Flags().StringArrayVar(&scopes, "scope", nil, "exact published scope to request (repeatable)")
command.Flags().StringVar(&contextName, "context", "", "Resource Server Context name for this request")
command.Flags().StringVar(&contextID, "context", "", "Resource Server Context ID for this request")
command.Flags().StringVar(&reason, "reason", "", "controller-facing reason for the request")
command.Flags().BoolVar(&handoff, "handoff", false, "hand the approval URL to a remote controller without opening a browser or waiting")
return command
Expand Down Expand Up @@ -381,7 +381,7 @@ func (a *App) toolboxCommand() *cobra.Command {
command.Flags().Bool("include", false, "include response headers")
command.Flags().String("search", "", "find operations by command, summary, method, path, or operation ID")
command.Flags().String("scope", "", "filter a Resource Server overview by published scope")
command.Flags().String("context", "", "Resource Server Context name for this operation")
command.Flags().String("context", "", "Resource Server Context ID for this operation")
command.Flags().Bool("all", false, "show the complete Resource Server inventory")
command.Flags().Bool("no-browser", false, "do not open controller approval pages")
command.Flags().Bool("no-paginate", false, "return only the first page")
Expand Down Expand Up @@ -636,22 +636,15 @@ func (a *App) showResourceServer(ctx context.Context, service *agent.Service, cl
if err != nil {
return err
}
overview := buildResourceServerOverview(server, details, inspection.Operations, a.discoveryOptions())
selected, selectedErr := service.SelectedContext(server.ResourceURL)
if selectedErr != nil && !errors.Is(selectedErr, os.ErrNotExist) {
return selectedErr
}
for index := range overview.Contexts {
for _, detail := range details {
if detail.Name == overview.Contexts[index].Name && sameDetails(detail.AuthorizationDetail, selected) {
overview.Contexts[index].Current = true
}
}
}
overview := buildResourceServerOverview(server, details, inspection.Operations, a.discoveryOptions(), selected)
effectiveSelected := selected
var effectiveDetail *catalog.AuthorizationDetail
if a.context != "" {
detail, detailErr := namedContext(details, a.context)
detail, detailErr := contextBySelector(details, a.context)
if detailErr != nil {
return detailErr
}
Expand Down Expand Up @@ -886,7 +879,11 @@ func (a *App) printContextSummary(overview resourceServerOverview) {
if item.Current {
current = " (current)"
}
fmt.Fprintf(a.stdout, " %s%s — %s\n", item.Name, current, item.AccountAuthorizationStatus)
if item.ID == "" {
fmt.Fprintf(a.stdout, " %s%s — %s\n", item.Name, current, item.AccountAuthorizationStatus)
continue
}
fmt.Fprintf(a.stdout, " %s %s%s — %s\n", item.ID, item.Name, current, item.AccountAuthorizationStatus)
}
if overview.ContextTruncated {
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)
Expand Down
20 changes: 10 additions & 10 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ func TestToolboxHelpDocumentsLocalCommandSurface(t *testing.T) {
"sync <resource-server>",
"get|head|post|put|patch|delete <resource-server>/<path>",
"<resource-server> context",
"<resource-server> context show <name>",
"<resource-server> context [use <name>|clear]",
"<resource-server> context show <context-id>",
"<resource-server> context [use <context-id>|clear]",
} {
if !strings.Contains(output, expected) {
t.Fatalf("help omitted %q:\n%s", expected, output)
Expand Down Expand Up @@ -313,7 +313,7 @@ func TestParseExecFlagsConsumesLogLevelBeforeNativeSeparator(t *testing.T) {
func TestResourceServerContextUsesDisplayContractWithoutRawDetails(t *testing.T) {
// [spec: cli/resource-server-context]
details := []catalog.AuthorizationDetail{{
Name: "realmroot", Description: "Organization GitHub App installation",
ID: "ctx_github_realmroot", Name: "realmroot", Description: "Organization GitHub App installation",
AuthorizationDetail: map[string]any{"type": "github_installation", "installation_id": "42"},
Metadata: map[string]string{"accountType": "Organization"},
AccountAuthorizationStatus: "authorized", AuthorizedScopes: []string{"issues:read"},
Expand All @@ -325,7 +325,7 @@ func TestResourceServerContextUsesDisplayContractWithoutRawDetails(t *testing.T)
t.Fatal(err)
}
output := string(encoded)
if !summaries[0].Current || !strings.Contains(output, `"name":"realmroot"`) ||
if !summaries[0].Current || !strings.Contains(output, `"id":"ctx_github_realmroot"`) || !strings.Contains(output, `"name":"realmroot"`) ||
strings.Contains(output, "installation_id") || strings.Contains(output, "authorizationDetail") {
t.Fatalf("Context summaries = %s", output)
}
Expand Down Expand Up @@ -859,7 +859,7 @@ func TestSmallResourceServerOverviewIncludesCompleteInventory(t *testing.T) {
details := []catalog.AuthorizationDetail{{Name: "wallet"}}
operations := []restish.OperationInspection{{ID: "showWallet", Command: []string{"wallet", "show"}, Method: "GET"}}

overview := buildResourceServerOverview(server, details, operations, discoveryOptions{})
overview := buildResourceServerOverview(server, details, operations, discoveryOptions{}, nil)

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

overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{})
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{}, nil)

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

overview := buildResourceServerOverview(server, details, makeOperations(80), discoveryOptions{})
overview := buildResourceServerOverview(server, details, makeOperations(80), discoveryOptions{}, nil)

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

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

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

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

overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{All: true})
overview := buildResourceServerOverview(server, nil, operations, discoveryOptions{All: true}, nil)

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