From 9dcaa10477ca40b79043f36f8fdb3b53d27723f0 Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Fri, 13 Nov 2026 23:22:31 -0700 Subject: [PATCH 1/4] Save each organization's Contact Center data center Contact Center and AI Agent Studio were always sent to us1, so cc commands could not reach an organization in another data center. --cc-region and WEBEX_CC_REGION choose one per command, and `webex config set cc-region ` saves it for the current organization (or --organization), so organizations in different data centers each keep their own. Without any of these, us1 is used as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW --- AGENTS.md | 2 +- README.md | 6 + cmd/ccregion_test.go | 158 ++++++++++++++++++++++ cmd/config_cmd.go | 34 ++++- cmd/root.go | 57 ++++++++ internal/appconfig/appconfig.go | 4 + internal/config/config.go | 35 ++++- site/src/content/docs/cc.mdx | 2 +- site/src/content/docs/getting-started.mdx | 13 ++ skill/SKILL.md | 1 + 10 files changed, 302 insertions(+), 10 deletions(-) create mode 100644 cmd/ccregion_test.go diff --git a/AGENTS.md b/AGENTS.md index 1da738a..6e123c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ gh release edit v0.X.0 --title "v0.X.0" --notes "## Improvements ## Key Files -- `cmd/root.go` — Root cobra command, global flags (token, org, output, debug, dry-run, paginate, 429 retries, region) +- `cmd/root.go` — Root cobra command, global flags (token, org, output, debug, dry-run, paginate, 429 retries, region, cc-region) - `cmd/auth.go` — Auth subcommand (status, list, switch, set-org, clear-org) - `cmd/login.go` / `cmd/logout.go` — OAuth PKCE and device login - `cmd/mcp.go` / `cmd/mcp/` — MCP serve/stop, tools, resources, usage log diff --git a/README.md b/README.md index 97e3192..ce5b331 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ webex cc users list --output csv > users.csv | `--debug` | Show HTTP request/response details | | `--paginate` | Auto-paginate list results (ignored by commands that return one object) | | `--dry-run` | Print write requests without executing them | +| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | ## Configuration @@ -256,11 +257,16 @@ webex config set client-id # Use custom OAuth client ID webex config set client-secret # Use custom OAuth client secret webex config set scopes # Override OAuth scopes webex config set read-only-scopes # Override scopes for login --read-only +webex config set cc-region # Save the current organization's Contact Center data center webex config get client-id # View current value ``` Config is stored in `~/.webex-cli/config.json`. +### Contact Center data center + +Each organization's Contact Center, and its AI Agent Studio, is in one data center: `us1`, `eu1`, `eu2`, `anz1`, `ca1`, `jp1` or `sg1`. `cc` commands use, in order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. `webex config set cc-region eu1` saves the region for the current organization (or the one named with `--organization`), so organizations in different data centers each keep their own. `--cc-region` alone saves nothing. + ## MCP Server > **Claude Desktop users:** see [CLAUDE_DESKTOP.md](CLAUDE_DESKTOP.md) for installation, DXT extension setup, and skill configuration specific to Claude Desktop. diff --git a/cmd/ccregion_test.go b/cmd/ccregion_test.go new file mode 100644 index 0000000..0168fdb --- /dev/null +++ b/cmd/ccregion_test.go @@ -0,0 +1,158 @@ +package cmd + +import ( + "strings" + "testing" + + "github.com/Cloverhound/webex-cli/internal/appconfig" + "github.com/Cloverhound/webex-cli/internal/config" +) + +const ( + euOrg = "11111111-1111-1111-1111-111111111111" + usOrg = "22222222-2222-2222-2222-222222222222" +) + +// isolate gives a test its own config file and resets what it changes. +func isolate(t *testing.T) { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("WEBEX_CC_REGION", "") + t.Setenv("WEBEX_USER", "") + t.Cleanup(func() { + rootCmd.PersistentFlags().Set("organization", "") + rootCmd.PersistentFlags().Set("cc-region", "") + rootCmd.SetArgs(nil) + config.SetOrgID("") + if err := setCCRegion(""); err != nil { + t.Error(err) + } + }) +} + +func run(t *testing.T, args ...string) (string, error) { + t.Helper() + rootCmd.SetArgs(args) + var err error + out := captureStdout(t, func() { err = rootCmd.Execute() }) + return out, err +} + +func TestConfigSetCCRegionSavesItForTheOrganization(t *testing.T) { + isolate(t) + if _, err := run(t, "config", "set", "cc-region", "eu1", "--organization", config.EncodeOrgID(euOrg)); err != nil { + t.Fatal(err) + } + cfg, err := appconfig.Load() + if err != nil { + t.Fatal(err) + } + if got := cfg.CCRegions[euOrg]; got != "eu1" { + t.Errorf("saved region for %s = %q, want eu1 (saved: %v)", euOrg, got, cfg.CCRegions) + } + if got, ok := cfg.CCRegions[usOrg]; ok { + t.Errorf("another organization got region %q", got) + } +} + +func TestConfigSetCCRegionUsesTheDefaultUsersOrganization(t *testing.T) { + isolate(t) + cfg, _ := appconfig.Load() + cfg.DefaultUser = "me@example.com" + cfg.Users["me@example.com"] = appconfig.UserInfo{OrgID: euOrg} + if err := cfg.Save(); err != nil { + t.Fatal(err) + } + if _, err := run(t, "config", "set", "cc-region", "anz1"); err != nil { + t.Fatal(err) + } + cfg, _ = appconfig.Load() + if got := cfg.CCRegions[euOrg]; got != "anz1" { + t.Errorf("saved region = %q, want anz1", got) + } +} + +func TestConfigSetCCRegionNeedsAnOrganization(t *testing.T) { + isolate(t) + if _, err := run(t, "config", "set", "cc-region", "eu1"); err == nil || !strings.Contains(err.Error(), "organization") { + t.Errorf("err = %v, want one asking for an organization", err) + } +} + +func TestConfigSetCCRegionRefusesAnUnknownRegion(t *testing.T) { + isolate(t) + if _, err := run(t, "config", "set", "cc-region", "eu9", "--organization", euOrg); err == nil || !strings.Contains(err.Error(), "eu9") { + t.Errorf("err = %v, want one naming eu9", err) + } +} + +func TestConfigGetCCRegionShowsTheOrganizationsRegion(t *testing.T) { + isolate(t) + cfg, _ := appconfig.Load() + cfg.CCRegions = map[string]string{euOrg: "eu2"} + if err := cfg.Save(); err != nil { + t.Fatal(err) + } + out, err := run(t, "config", "get", "cc-region", "--organization", euOrg) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "eu2") || !strings.Contains(out, euOrg) { + t.Errorf("output = %q, want eu2 for %s", out, euOrg) + } + out, err = run(t, "config", "get", "cc-region", "--organization", usOrg) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "us1 (default)") { + t.Errorf("output = %q, want us1 (default)", out) + } +} + +func TestTheSavedRegionSelectsTheOrganizationsHosts(t *testing.T) { + isolate(t) + cfg := &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}} + + config.SetOrgID(euOrg) + if err := resolveCCRegion("", cfg); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-eu1.cisco.com" || config.AiAgentBaseURL != "https://studio.aiagent-eu1.cisco.com" { + t.Errorf("hosts = %s, %s; want eu1's", config.CcBaseURL, config.AiAgentBaseURL) + } + + config.SetOrgID(usOrg) + if err := resolveCCRegion("", cfg); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-us1.cisco.com" || config.AiAgentBaseURL != "https://studio.aiagent-us1.cisco.com" { + t.Errorf("hosts = %s, %s; want us1's for an organization with no saved region", config.CcBaseURL, config.AiAgentBaseURL) + } +} + +func TestTheFlagAndEnvironmentOverrideTheSavedRegion(t *testing.T) { + isolate(t) + cfg := &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}} + config.SetOrgID(euOrg) + + t.Setenv("WEBEX_CC_REGION", "ca1") + if err := resolveCCRegion("", cfg); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-ca1.cisco.com" { + t.Errorf("with WEBEX_CC_REGION=ca1, CC host = %s", config.CcBaseURL) + } + + if err := resolveCCRegion("JP1", cfg); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-jp1.cisco.com" { + t.Errorf("with --cc-region JP1, CC host = %s", config.CcBaseURL) + } + + if err := resolveCCRegion("mars", cfg); err == nil { + t.Error("an unknown region was accepted") + } +} diff --git a/cmd/config_cmd.go b/cmd/config_cmd.go index ea42d7e..b224b14 100644 --- a/cmd/config_cmd.go +++ b/cmd/config_cmd.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" ) -const configKeys = "client-id, client-secret, scopes, read-only-scopes, region" +const configKeys = "client-id, client-secret, scopes, read-only-scopes, region, cc-region" var configCmd = &cobra.Command{ Use: "config", @@ -30,8 +30,9 @@ To use your own Webex Integration instead of the built-in default: var configSetCmd = &cobra.Command{ Use: "set ", Short: "Set a configuration value", - Long: "Set a configuration value. Valid keys: " + configKeys + ". read-only-scopes accepts only read scopes.", - Args: cobra.ExactArgs(2), + Long: "Set a configuration value. Valid keys: " + configKeys + ". read-only-scopes accepts only read scopes. " + + "cc-region is saved for the current organization (or --organization), since each org's Contact Center is in one data center.", + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { key := args[0] value := args[1] @@ -41,6 +42,7 @@ var configSetCmd = &cobra.Command{ return fmt.Errorf("loading config: %w", err) } + done := "Set " + key switch key { case "client-id": cfg.ClientID = value @@ -58,6 +60,23 @@ var configSetCmd = &cobra.Command{ return err } cfg.Region = config.Region() + case "cc-region": + region, err := normalizeCCRegion(value) + if err != nil { + return err + } + if region == "" { + return fmt.Errorf("cc-region needs a value (valid: %s)", strings.Join(config.CCRegions, ", ")) + } + org := configuredOrgID(cmd, cfg) + if org == "" { + return fmt.Errorf("no organization to save cc-region for: log in, or pass --organization") + } + if cfg.CCRegions == nil { + cfg.CCRegions = map[string]string{} + } + cfg.CCRegions[org] = region + done = fmt.Sprintf("Set cc-region to %s for organization %s", region, org) default: return fmt.Errorf("unknown config key: %s (valid: %s)", key, configKeys) } @@ -66,7 +85,7 @@ var configSetCmd = &cobra.Command{ return fmt.Errorf("saving config: %w", err) } - fmt.Printf("Set %s\n", key) + fmt.Println(done) return nil }, } @@ -129,6 +148,13 @@ var configGetCmd = &cobra.Command{ } else { fmt.Println("us (default)") } + case "cc-region": + org := configuredOrgID(cmd, cfg) + if region := cfg.CCRegions[org]; org != "" && region != "" { + fmt.Printf("%s (saved for organization %s)\n", region, org) + } else { + fmt.Printf("%s (default)\n", config.DefaultCCRegion) + } default: return fmt.Errorf("unknown config key: %s (valid: %s)", key, configKeys) } diff --git a/cmd/root.go b/cmd/root.go index 68f3782..f37c259 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -107,6 +107,11 @@ var rootCmd = &cobra.Command{ } } + ccRegionFlag, _ := cmd.Flags().GetString("cc-region") + if err := resolveCCRegion(ccRegionFlag, cfg); err != nil { + return err + } + // Auto-populate --orgid on CC commands from the resolved org. // CC API uses UUID format, so decode any base64 values. if f := cmd.Flags().Lookup("orgid"); f != nil { @@ -166,6 +171,57 @@ func setRegion(region string) error { return nil } +// resolveCCRegion selects the Contact Center data center for the resolved org: +// --cc-region flag > WEBEX_CC_REGION env > the region saved for the org > us1. +func resolveCCRegion(flag string, cfg *appconfig.Config) error { + region := flag + if region == "" { + region = os.Getenv("WEBEX_CC_REGION") + } + if region == "" { + region = cfg.CCRegions[config.OrgID()] + } + return setCCRegion(region) +} + +// setCCRegion validates a Contact Center data center and points requests at it. +func setCCRegion(region string) error { + normalized, err := normalizeCCRegion(region) + if err != nil { + return err + } + config.SetCCRegion(normalized) + return nil +} + +func normalizeCCRegion(region string) (string, error) { + normalized := strings.ToLower(strings.TrimSpace(region)) + if normalized != "" && !slices.Contains(config.CCRegions, normalized) { + return "", fmt.Errorf("unknown Contact Center region %q (valid: %s)", region, strings.Join(config.CCRegions, ", ")) + } + return normalized, nil +} + +// configuredOrgID returns the org a command would use, from the flag and the config +// file alone, for commands that run without a login. +func configuredOrgID(cmd *cobra.Command, cfg *appconfig.Config) string { + if org, _ := cmd.Flags().GetString("organization"); org != "" { + return config.DecodeOrgID(org) + } + if cfg.DefaultOrgID != "" { + return config.DecodeOrgID(cfg.DefaultOrgID) + } + flagUser, envUser := requestedUser(cmd) + user := flagUser + if user == "" { + user = envUser + } + if user == "" { + user = cfg.DefaultUser + } + return config.DecodeOrgID(cfg.Users[user].OrgID) +} + // RootCommand returns the root cobra command for external introspection. func RootCommand() *cobra.Command { return rootCmd } @@ -219,6 +275,7 @@ func init() { rootCmd.PersistentFlags().String("user", "", "Use a specific authenticated user (email)") rootCmd.PersistentFlags().String("organization", "", "Override organization ID for this command") rootCmd.PersistentFlags().String("region", "", "Data region for region-specific APIs: "+strings.Join(config.Regions, ", ")+" (default us)") + rootCmd.PersistentFlags().String("cc-region", "", "Contact Center data center: "+strings.Join(config.CCRegions, ", ")+" (default: the organization's saved region, else us1)") rootCmd.PersistentFlags().Int("max-retry", 3, "Max number of 429 retries before giving up (0 = no retries)") rootCmd.PersistentFlags().Int("max-retry-timer", 60, "Max total seconds to wait across all 429 retries (0 = unlimited)") diff --git a/internal/appconfig/appconfig.go b/internal/appconfig/appconfig.go index 541658d..7b81846 100644 --- a/internal/appconfig/appconfig.go +++ b/internal/appconfig/appconfig.go @@ -45,6 +45,10 @@ type Config struct { ReadOnly bool `json:"read_only,omitempty"` ReadOnlyScopes string `json:"read_only_scopes,omitempty"` + // CCRegions holds each org's Contact Center data center, keyed by org UUID: + // orgs one login manages can live in different data centers. + CCRegions map[string]string `json:"cc_regions,omitempty"` + path string // file path, not serialized } diff --git a/internal/config/config.go b/internal/config/config.go index 619d840..d23c350 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -60,17 +60,44 @@ func OrgIDBase64() string { return orgIDBase64 } const ( CallingBaseURL = "https://webexapis.com/v1" - CcBaseURL = "https://api.wxcc-us1.cisco.com" // AnalyticsBaseURL serves the Admin analytics reports. Paths for those // endpoints already carry their own /v1 segment. AnalyticsBaseURL = "https://analytics.webexapis.com" +) + +// CCRegions lists the Contact Center data centers. An org's Contact Center lives +// in one of them, and each has its own API and AI Agent Studio host. +var CCRegions = []string{"us1", "eu1", "eu2", "anz1", "ca1", "jp1", "sg1"} + +// DefaultCCRegion is used when no region is given or saved for the org. +const DefaultCCRegion = "us1" + +var ccRegion = DefaultCCRegion + +var ( + CcBaseURL = ccHost(DefaultCCRegion) - // AiAgentBaseURL is AI Agent Studio in the same data center as CcBaseURL; - // each Contact Center data center has its own Studio host. - AiAgentBaseURL = "https://studio.aiagent-us1.cisco.com" + // AiAgentBaseURL is AI Agent Studio in the same data center as CcBaseURL. + AiAgentBaseURL = aiAgentHost(DefaultCCRegion) ) +func ccHost(region string) string { return "https://api.wxcc-" + region + ".cisco.com" } +func aiAgentHost(region string) string { return "https://studio.aiagent-" + region + ".cisco.com" } + +// SetCCRegion points Contact Center and AI Agent Studio requests at a data center +// from CCRegions. Empty means DefaultCCRegion. +func SetCCRegion(region string) { + if region == "" { + region = DefaultCCRegion + } + ccRegion = region + CcBaseURL = ccHost(region) + AiAgentBaseURL = aiAgentHost(region) +} + +func CCRegion() string { return ccRegion } + // analyticsCallingHosts maps a data region to the Detailed Call History (CDR) FQDN. // Calls to the default host are routed to the nearest region; if that region does // not hold the org's data the API returns HTTP 451 with the correct endpoint. diff --git a/site/src/content/docs/cc.mdx b/site/src/content/docs/cc.mdx index c2933c1..dd43267 100644 --- a/site/src/content/docs/cc.mdx +++ b/site/src/content/docs/cc.mdx @@ -208,7 +208,7 @@ webex cc ai-agent-session list --agent-id --paginate - A tool needs `input_entities` in JSON Schema form even when it takes no input. Start from a tool in `get-configuration`. - In an organization with Advanced Data Protection, the messages of live sessions are encrypted. `ai-agent-transcript create-audit-record` adds a permanent compliance entry under your name, and with a regular Webex login the messages stay encrypted afterwards. - A voice session's `interaction_id` is the Contact Center task ID that `captures list` accepts. -- The Studio host is the US data center's, as for the rest of `webex cc`. +- Studio is in the organization's Contact Center data center, which `--cc-region` or `webex config set cc-region` selects, as for the rest of `webex cc`. ## Other refreshed Contact Center APIs diff --git a/site/src/content/docs/getting-started.mdx b/site/src/content/docs/getting-started.mdx index 174fb91..f31a088 100644 --- a/site/src/content/docs/getting-started.mdx +++ b/site/src/content/docs/getting-started.mdx @@ -87,6 +87,19 @@ webex messaging [flags] # Webex Messaging APIs | `--output json\|table\|csv\|raw` | Output format (default: json) | | `--debug` | Show HTTP request/response details | | `--paginate` | Auto-paginate list results (ignored by commands that return one object) | +| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | + +## Contact Center data center + +Each organization's Contact Center, and its AI Agent Studio, is in one data center. `cc` commands use, in order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. Save an organization's region once: + +```bash +webex config set cc-region eu1 # the current organization +webex config set cc-region anz1 --organization # another organization +webex config get cc-region +``` + +`--cc-region` on its own applies to one command and saves nothing. ## Admin Examples diff --git a/skill/SKILL.md b/skill/SKILL.md index 0291969..a8598c4 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -68,6 +68,7 @@ Aliases: `devices` for `device`, `meeting` for `meetings`, `msg` for `messaging` - `--paginate` — Auto-paginate list results - `--dry-run` — Print write requests (POST/PUT/DELETE/PATCH) without executing them; read operations still run normally - `--region us|ca|eu|eun|in|au` — Data region for region-specific hosts, used by `calling reports-detailed-call-history` (default: `us`). Also settable with `webex config set region ` or `WEBEX_REGION`. A `451` error means the org's data lives in another region — the error names the correct one. +- `--cc-region us1|eu1|eu2|anz1|ca1|jp1|sg1` — Contact Center data center for `cc` commands, including AI Agent Studio. Order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. Save it once per organization with `webex config set cc-region ` (add `--organization ` for another org); the flag alone saves nothing. ## Contact Center `--orgid` Handling From 785447f83b287ae6421e9226f84dee078924e898 Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Fri, 13 Nov 2026 23:29:24 -0700 Subject: [PATCH 2/4] Point Contact Center help and docs at the saved data center The cc and config help, the cc skill, the Contact Center docs page and the README's Contact Center section now say where cc requests go and how to save an organization's data center. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW --- README.md | 2 +- cmd/cc.go | 6 +++++- cmd/config_cmd.go | 5 ++++- site/src/content/docs/cc.mdx | 2 ++ skill/cc/SKILL.md | 2 ++ 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce5b331..eb490d4 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ All upload commands support `--dry-run` to preview the request without sending i ### Contact Center (`webex cc`) -64 resource groups including sites, queues, entry points, teams, flows, skills, desktop layouts, global variables, business hours, auxiliary codes, campaigns, callbacks, realtime stats, AI assistant, journey analytics, subscriptions, and more. +64 resource groups including sites, queues, entry points, teams, flows, skills, desktop layouts, global variables, business hours, auxiliary codes, campaigns, callbacks, realtime stats, AI assistant, journey analytics, subscriptions, and more. For an organization outside the US data center, see [Contact Center data center](#contact-center-data-center). ### Admin (`webex admin`) diff --git a/cmd/cc.go b/cmd/cc.go index 4e0b40e..3c10b76 100644 --- a/cmd/cc.go +++ b/cmd/cc.go @@ -8,7 +8,11 @@ var CcCmd = &cobra.Command{ Use: "cc", Aliases: []string{"contact-center"}, Short: "Webex Contact Center APIs", - Long: `Manage Webex Contact Center resources — agents, queues, tasks, routing, and more.`, + Long: `Manage Webex Contact Center resources — agents, queues, tasks, routing, and more. + +Requests go to the organization's Contact Center data center: --cc-region, else +$WEBEX_CC_REGION, else the region saved with 'webex config set cc-region ', +else us1. Regions: us1, eu1, eu2, anz1, ca1, jp1, sg1.`, } func init() { diff --git a/cmd/config_cmd.go b/cmd/config_cmd.go index b224b14..32ab935 100644 --- a/cmd/config_cmd.go +++ b/cmd/config_cmd.go @@ -24,7 +24,10 @@ To use your own Webex Integration instead of the built-in default: 3. Add the OAuth scopes required for the APIs you plan to use 4. Run: webex config set client-id webex config set client-secret - webex config set scopes "spark:all spark-admin:all "`, + webex config set scopes "spark:all spark-admin:all " + +For an organization whose Contact Center is outside the US data center, save its +region once: webex config set cc-region eu1 (add --organization for another org)`, } var configSetCmd = &cobra.Command{ diff --git a/site/src/content/docs/cc.mdx b/site/src/content/docs/cc.mdx index dd43267..f637a39 100644 --- a/site/src/content/docs/cc.mdx +++ b/site/src/content/docs/cc.mdx @@ -5,6 +5,8 @@ description: Webex Contact Center API commands reference. The `webex cc` subcommand provides access to the Webex Contact Center API. The `--orgid` flag is auto-populated from your login, so you typically don't need to pass it. +Requests go to the organization's Contact Center data center: the one saved with `webex config set cc-region `, else `us1`. `--cc-region` overrides it for one command. See [Contact Center data center](../getting-started/#contact-center-data-center). + ## Resource Groups | Resource | Description | diff --git a/skill/cc/SKILL.md b/skill/cc/SKILL.md index fa860ca..09c5c78 100644 --- a/skill/cc/SKILL.md +++ b/skill/cc/SKILL.md @@ -10,6 +10,8 @@ Alias: `webex contact-center ` **`--orgid` is auto-populated from the logged-in user's org.** Use `--orgid=` (with `=`) only when overriding. Both UUID and base64 org ID formats are accepted. +**Data center:** `cc` commands, including `ai-agent*`, go to the org's saved Contact Center data center, else `us1`. For an org outside the US, check `webex config get cc-region`; save it once with `webex config set cc-region `, or pass `--cc-region` for one command. + ## Resources | Resource | Common operations | From b2a8831e8aaa18df1599ea8b7bdd4606e857d405 Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Fri, 13 Nov 2026 23:33:24 -0700 Subject: [PATCH 3/4] Use the region of the org a request names, and for agent token lookups An explicit --orgid on a cc command now selects that org's saved data center. The AI agent token lookup reads the Studio host when it runs, after the region is chosen, rather than when the program starts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW --- cmd/ccregion_test.go | 39 +++++++++++++++++++++++++------- cmd/root.go | 22 +++++++++++------- internal/aiagent/aiagent.go | 7 +++--- internal/aiagent/aiagent_test.go | 13 +++++++++-- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/cmd/ccregion_test.go b/cmd/ccregion_test.go index 0168fdb..89ac4f2 100644 --- a/cmd/ccregion_test.go +++ b/cmd/ccregion_test.go @@ -6,6 +6,7 @@ import ( "github.com/Cloverhound/webex-cli/internal/appconfig" "github.com/Cloverhound/webex-cli/internal/config" + "github.com/spf13/cobra" ) const ( @@ -115,16 +116,14 @@ func TestTheSavedRegionSelectsTheOrganizationsHosts(t *testing.T) { isolate(t) cfg := &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}} - config.SetOrgID(euOrg) - if err := resolveCCRegion("", cfg); err != nil { + if err := resolveCCRegion("", euOrg, cfg); err != nil { t.Fatal(err) } if config.CcBaseURL != "https://api.wxcc-eu1.cisco.com" || config.AiAgentBaseURL != "https://studio.aiagent-eu1.cisco.com" { t.Errorf("hosts = %s, %s; want eu1's", config.CcBaseURL, config.AiAgentBaseURL) } - config.SetOrgID(usOrg) - if err := resolveCCRegion("", cfg); err != nil { + if err := resolveCCRegion("", usOrg, cfg); err != nil { t.Fatal(err) } if config.CcBaseURL != "https://api.wxcc-us1.cisco.com" || config.AiAgentBaseURL != "https://studio.aiagent-us1.cisco.com" { @@ -132,27 +131,51 @@ func TestTheSavedRegionSelectsTheOrganizationsHosts(t *testing.T) { } } +func TestAnExplicitOrgidSelectsThatOrganizationsRegion(t *testing.T) { + isolate(t) + t.Setenv("WEBEX_TOKEN", "") + cfg, _ := appconfig.Load() + cfg.CCRegions = map[string]string{euOrg: "eu1"} + if err := cfg.Save(); err != nil { + t.Fatal(err) + } + var host string + probe := &cobra.Command{Use: "region-probe", RunE: func(*cobra.Command, []string) error { + host = config.CcBaseURL + return nil + }} + probe.Flags().String("orgid", "", "") + CcCmd.AddCommand(probe) + t.Cleanup(func() { CcCmd.RemoveCommand(probe) }) + + if _, err := run(t, "cc", "region-probe", "--token", "dummy", "--orgid="+config.EncodeOrgID(euOrg)); err != nil { + t.Fatal(err) + } + if host != "https://api.wxcc-eu1.cisco.com" { + t.Errorf("CC host for --orgid=%s = %s, want eu1's", euOrg, host) + } +} + func TestTheFlagAndEnvironmentOverrideTheSavedRegion(t *testing.T) { isolate(t) cfg := &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}} - config.SetOrgID(euOrg) t.Setenv("WEBEX_CC_REGION", "ca1") - if err := resolveCCRegion("", cfg); err != nil { + if err := resolveCCRegion("", euOrg, cfg); err != nil { t.Fatal(err) } if config.CcBaseURL != "https://api.wxcc-ca1.cisco.com" { t.Errorf("with WEBEX_CC_REGION=ca1, CC host = %s", config.CcBaseURL) } - if err := resolveCCRegion("JP1", cfg); err != nil { + if err := resolveCCRegion("JP1", euOrg, cfg); err != nil { t.Fatal(err) } if config.CcBaseURL != "https://api.wxcc-jp1.cisco.com" { t.Errorf("with --cc-region JP1, CC host = %s", config.CcBaseURL) } - if err := resolveCCRegion("mars", cfg); err == nil { + if err := resolveCCRegion("mars", euOrg, cfg); err == nil { t.Error("an unknown region was accepted") } } diff --git a/cmd/root.go b/cmd/root.go index f37c259..cc4911b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -107,11 +107,6 @@ var rootCmd = &cobra.Command{ } } - ccRegionFlag, _ := cmd.Flags().GetString("cc-region") - if err := resolveCCRegion(ccRegionFlag, cfg); err != nil { - return err - } - // Auto-populate --orgid on CC commands from the resolved org. // CC API uses UUID format, so decode any base64 values. if f := cmd.Flags().Lookup("orgid"); f != nil { @@ -124,6 +119,17 @@ var rootCmd = &cobra.Command{ } } + // The data center is the one of the org the request names, which --orgid + // can set apart from the resolved org. + ccOrg := config.OrgID() + if f := cmd.Flags().Lookup("orgid"); f != nil && f.Value.String() != "" { + ccOrg = f.Value.String() + } + ccRegionFlag, _ := cmd.Flags().GetString("cc-region") + if err := resolveCCRegion(ccRegionFlag, ccOrg, cfg); err != nil { + return err + } + // Auto-populate --org-id on non-CC commands from base64 org ID. // These APIs require base64 format, so convert if needed. if f := cmd.Flags().Lookup("org-id"); f != nil { @@ -171,15 +177,15 @@ func setRegion(region string) error { return nil } -// resolveCCRegion selects the Contact Center data center for the resolved org: +// resolveCCRegion selects the Contact Center data center for orgID (a UUID): // --cc-region flag > WEBEX_CC_REGION env > the region saved for the org > us1. -func resolveCCRegion(flag string, cfg *appconfig.Config) error { +func resolveCCRegion(flag, orgID string, cfg *appconfig.Config) error { region := flag if region == "" { region = os.Getenv("WEBEX_CC_REGION") } if region == "" { - region = cfg.CCRegions[config.OrgID()] + region = cfg.CCRegions[orgID] } return setCCRegion(region) } diff --git a/internal/aiagent/aiagent.go b/internal/aiagent/aiagent.go index e1bdb02..38e7f99 100644 --- a/internal/aiagent/aiagent.go +++ b/internal/aiagent/aiagent.go @@ -11,8 +11,9 @@ import ( "github.com/Cloverhound/webex-cli/internal/config" ) -// baseURL is a variable so tests can point it at a fake Studio. -var baseURL = config.AiAgentBaseURL +// studioURL is read at call time, after the command has chosen the org's data +// center, and is a variable so tests can point it at a fake Studio. +var studioURL = func() string { return config.AiAgentBaseURL } // BotToken returns the runtime token of an AI agent in the current // organization. Studio's v1 API selects the agent of a call by this token, and @@ -22,7 +23,7 @@ func BotToken(agentID string) (string, error) { if orgID == "" { return "", fmt.Errorf("no organization: log in or pass --organization") } - req := client.NewRequest(baseURL, "GET", "/api/v2/organizations/{orgid}/agents/") + req := client.NewRequest(studioURL(), "GET", "/api/v2/organizations/{orgid}/agents/") req.PathParam("orgid", orgID) req.OffsetPaging("offset", "limit", 0) body, _, err := req.DoPaginated(false) diff --git a/internal/aiagent/aiagent_test.go b/internal/aiagent/aiagent_test.go index 09dbccb..ae0d528 100644 --- a/internal/aiagent/aiagent_test.go +++ b/internal/aiagent/aiagent_test.go @@ -26,8 +26,9 @@ func studio(t *testing.T) *httptest.Server { }) })) t.Cleanup(srv.Close) - baseURL = srv.URL - t.Cleanup(func() { baseURL = config.AiAgentBaseURL }) + old := studioURL + studioURL = func() string { return srv.URL } + t.Cleanup(func() { studioURL = old }) config.SetOrgID("org-1") t.Cleanup(func() { config.SetOrgID("") }) return srv @@ -41,6 +42,14 @@ func TestBotTokenFindsTheAgent(t *testing.T) { } } +func TestBotTokenUsesTheSelectedRegionsStudio(t *testing.T) { + config.SetCCRegion("eu1") + t.Cleanup(func() { config.SetCCRegion("") }) + if got := studioURL(); got != "https://studio.aiagent-eu1.cisco.com" { + t.Errorf("BotToken's Studio host = %s, want eu1's", got) + } +} + func TestBotTokenNamesAMissingAgent(t *testing.T) { studio(t) if _, err := BotToken("zz"); err == nil || !strings.Contains(err.Error(), "zz") { From 4a1e3f348c15916e640387162148dc3b37c8062b Mon Sep 17 00:00:00 2001 From: Ed Umansky Date: Fri, 13 Nov 2026 23:54:17 -0700 Subject: [PATCH 4/4] Find the region from every cc org flag, add India, and tidy org keys - A cc command that names its org with --org-id, --organization-id or --x-organization-id now uses that org's saved region, as --orgid does. - in1 (India) joins the data centers. - Saved regions are keyed by the lower-case org UUID, and `config set cc-region` refuses an organization that is not an org ID. - A blank --cc-region or WEBEX_CC_REGION no longer hides the saved region. - `config get cc-region` says when it finds no organization. - The skills tell MCP agents to pass cc-region as a flag, since the MCP server does not run `config set`. - The region tests clear the environment and the token they set. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW --- README.md | 4 +- cmd/cc.go | 2 +- cmd/ccregion_test.go | 99 ++++++++++++++++++++++- cmd/config_cmd.go | 10 ++- cmd/root.go | 42 +++++++--- internal/aiagent/aiagent_test.go | 11 ++- internal/config/config.go | 7 +- site/src/content/docs/getting-started.mdx | 2 +- skill/SKILL.md | 2 +- skill/cc/SKILL.md | 2 +- 10 files changed, 149 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index eb490d4..51cef3e 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ webex cc users list --output csv > users.csv | `--debug` | Show HTTP request/response details | | `--paginate` | Auto-paginate list results (ignored by commands that return one object) | | `--dry-run` | Print write requests without executing them | -| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | +| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1\|in1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | ## Configuration @@ -265,7 +265,7 @@ Config is stored in `~/.webex-cli/config.json`. ### Contact Center data center -Each organization's Contact Center, and its AI Agent Studio, is in one data center: `us1`, `eu1`, `eu2`, `anz1`, `ca1`, `jp1` or `sg1`. `cc` commands use, in order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. `webex config set cc-region eu1` saves the region for the current organization (or the one named with `--organization`), so organizations in different data centers each keep their own. `--cc-region` alone saves nothing. +Each organization's Contact Center, and its AI Agent Studio, is in one data center: `us1`, `eu1`, `eu2`, `anz1`, `ca1`, `jp1`, `sg1` or `in1`. `cc` commands use, in order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. `webex config set cc-region eu1` saves the region for the current organization (or the one named with `--organization`), so organizations in different data centers each keep their own. `--cc-region` alone saves nothing. ## MCP Server diff --git a/cmd/cc.go b/cmd/cc.go index 3c10b76..6f76613 100644 --- a/cmd/cc.go +++ b/cmd/cc.go @@ -12,7 +12,7 @@ var CcCmd = &cobra.Command{ Requests go to the organization's Contact Center data center: --cc-region, else $WEBEX_CC_REGION, else the region saved with 'webex config set cc-region ', -else us1. Regions: us1, eu1, eu2, anz1, ca1, jp1, sg1.`, +else us1. Regions: us1, eu1, eu2, anz1, ca1, jp1, sg1, in1.`, } func init() { diff --git a/cmd/ccregion_test.go b/cmd/ccregion_test.go index 89ac4f2..39b054b 100644 --- a/cmd/ccregion_test.go +++ b/cmd/ccregion_test.go @@ -20,12 +20,15 @@ func isolate(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) - t.Setenv("WEBEX_CC_REGION", "") - t.Setenv("WEBEX_USER", "") + for _, env := range []string{"WEBEX_CC_REGION", "WEBEX_USER", "WEBEX_TOKEN", "WEBEX_REFRESH_TOKEN", "WEBEX_REGION", readOnlyEnv} { + t.Setenv(env, "") + } t.Cleanup(func() { - rootCmd.PersistentFlags().Set("organization", "") - rootCmd.PersistentFlags().Set("cc-region", "") + for _, flag := range []string{"organization", "cc-region", "token"} { + rootCmd.PersistentFlags().Set(flag, "") + } rootCmd.SetArgs(nil) + config.SetToken("") config.SetOrgID("") if err := setCCRegion(""); err != nil { t.Error(err) @@ -156,6 +159,94 @@ func TestAnExplicitOrgidSelectsThatOrganizationsRegion(t *testing.T) { } } +// regionFor runs a cc command that names its org with orgFlag and returns the CC +// host it would send to. +func regionFor(t *testing.T, orgFlag, org string) string { + t.Helper() + var host string + probe := &cobra.Command{Use: "region-probe-" + orgFlag, RunE: func(*cobra.Command, []string) error { + host = config.CcBaseURL + return nil + }} + probe.Flags().String(orgFlag, "", "") + CcCmd.AddCommand(probe) + t.Cleanup(func() { CcCmd.RemoveCommand(probe) }) + if _, err := run(t, "cc", probe.Use, "--token", "dummy", "--"+orgFlag+"="+org); err != nil { + t.Fatal(err) + } + return host +} + +func TestEveryWayACommandNamesItsOrgSelectsThatOrgsRegion(t *testing.T) { + for _, flag := range []string{"orgid", "org-id", "x-organization-id", "organization-id"} { + t.Run(flag, func(t *testing.T) { + isolate(t) + cfg, _ := appconfig.Load() + cfg.CCRegions = map[string]string{euOrg: "eu1"} + if err := cfg.Save(); err != nil { + t.Fatal(err) + } + if got := regionFor(t, flag, config.EncodeOrgID(euOrg)); got != "https://api.wxcc-eu1.cisco.com" { + t.Errorf("CC host for --%s=%s = %s, want eu1's", flag, euOrg, got) + } + }) + } +} + +func TestIndiaIsADataCenter(t *testing.T) { + isolate(t) + if err := resolveCCRegion("in1", euOrg, &appconfig.Config{}); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-in1.cisco.com" || config.AiAgentBaseURL != "https://studio.aiagent-in1.cisco.com" { + t.Errorf("hosts = %s, %s; want in1's", config.CcBaseURL, config.AiAgentBaseURL) + } +} + +func TestAnOrgIsSavedAndFoundWhateverTheCaseOfItsID(t *testing.T) { + isolate(t) + const org = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + if _, err := run(t, "config", "set", "cc-region", "eu2", "--organization", strings.ToUpper(org)); err != nil { + t.Fatal(err) + } + cfg, _ := appconfig.Load() + if err := resolveCCRegion("", org, cfg); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-eu2.cisco.com" { + t.Errorf("region saved for the upper-case org ID was not found for the lower-case one: %v", cfg.CCRegions) + } +} + +func TestConfigSetCCRegionRefusesSomethingThatIsNotAnOrgID(t *testing.T) { + isolate(t) + if _, err := run(t, "config", "set", "cc-region", "eu1", "--organization", "Acme"); err == nil { + t.Error("an organization that is not an org ID was accepted") + } +} + +func TestABlankRegionDoesNotHideTheSavedOne(t *testing.T) { + isolate(t) + t.Setenv("WEBEX_CC_REGION", " ") + if err := resolveCCRegion(" ", euOrg, &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}}); err != nil { + t.Fatal(err) + } + if config.CcBaseURL != "https://api.wxcc-eu1.cisco.com" { + t.Errorf("CC host = %s, want the saved eu1", config.CcBaseURL) + } +} + +func TestConfigGetCCRegionSaysWhenThereIsNoOrganization(t *testing.T) { + isolate(t) + out, err := run(t, "config", "get", "cc-region") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(out, "no organization") { + t.Errorf("output = %q, want it to say no organization was found", out) + } +} + func TestTheFlagAndEnvironmentOverrideTheSavedRegion(t *testing.T) { isolate(t) cfg := &appconfig.Config{CCRegions: map[string]string{euOrg: "eu1"}} diff --git a/cmd/config_cmd.go b/cmd/config_cmd.go index 32ab935..cd1a5cc 100644 --- a/cmd/config_cmd.go +++ b/cmd/config_cmd.go @@ -75,6 +75,9 @@ var configSetCmd = &cobra.Command{ if org == "" { return fmt.Errorf("no organization to save cc-region for: log in, or pass --organization") } + if !orgUUID.MatchString(org) { + return fmt.Errorf("%q is not an organization ID", org) + } if cfg.CCRegions == nil { cfg.CCRegions = map[string]string{} } @@ -153,9 +156,12 @@ var configGetCmd = &cobra.Command{ } case "cc-region": org := configuredOrgID(cmd, cfg) - if region := cfg.CCRegions[org]; org != "" && region != "" { + switch region := cfg.CCRegions[org]; { + case org == "": + fmt.Printf("%s (default; no organization found: log in, or pass --organization)\n", config.DefaultCCRegion) + case region != "": fmt.Printf("%s (saved for organization %s)\n", region, org) - } else { + default: fmt.Printf("%s (default)\n", config.DefaultCCRegion) } default: diff --git a/cmd/root.go b/cmd/root.go index cc4911b..8880eda 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "regexp" "slices" "strings" @@ -119,11 +120,9 @@ var rootCmd = &cobra.Command{ } } - // The data center is the one of the org the request names, which --orgid - // can set apart from the resolved org. - ccOrg := config.OrgID() - if f := cmd.Flags().Lookup("orgid"); f != nil && f.Value.String() != "" { - ccOrg = f.Value.String() + ccOrg := requestOrgID(cmd) + if ccOrg == "" { + ccOrg = orgKey(config.OrgID()) } ccRegionFlag, _ := cmd.Flags().GetString("cc-region") if err := resolveCCRegion(ccRegionFlag, ccOrg, cfg); err != nil { @@ -177,19 +176,38 @@ func setRegion(region string) error { return nil } -// resolveCCRegion selects the Contact Center data center for orgID (a UUID): +// resolveCCRegion selects the Contact Center data center for orgID: // --cc-region flag > WEBEX_CC_REGION env > the region saved for the org > us1. func resolveCCRegion(flag, orgID string, cfg *appconfig.Config) error { - region := flag + region := strings.TrimSpace(flag) if region == "" { - region = os.Getenv("WEBEX_CC_REGION") + region = strings.TrimSpace(os.Getenv("WEBEX_CC_REGION")) } if region == "" { - region = cfg.CCRegions[orgID] + region = cfg.CCRegions[orgKey(orgID)] } return setCCRegion(region) } +// requestOrgID returns the org a command names with a flag of its own, which can +// differ from the resolved org. Contact Center commands name it in the path +// (--orgid), the body (--org-id, --organization-id) or a header +// (--x-organization-id). +func requestOrgID(cmd *cobra.Command) string { + for _, name := range []string{"orgid", "org-id", "x-organization-id", "organization-id"} { + if f := cmd.Flags().Lookup(name); f != nil && f.Changed && f.Value.String() != "" { + return orgKey(f.Value.String()) + } + } + return "" +} + +// orgKey is the form saved regions are keyed by: the lower-case UUID, whichever +// form the org ID was given in. +func orgKey(id string) string { return strings.ToLower(config.DecodeOrgID(strings.TrimSpace(id))) } + +var orgUUID = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) + // setCCRegion validates a Contact Center data center and points requests at it. func setCCRegion(region string) error { normalized, err := normalizeCCRegion(region) @@ -212,10 +230,10 @@ func normalizeCCRegion(region string) (string, error) { // file alone, for commands that run without a login. func configuredOrgID(cmd *cobra.Command, cfg *appconfig.Config) string { if org, _ := cmd.Flags().GetString("organization"); org != "" { - return config.DecodeOrgID(org) + return orgKey(org) } if cfg.DefaultOrgID != "" { - return config.DecodeOrgID(cfg.DefaultOrgID) + return orgKey(cfg.DefaultOrgID) } flagUser, envUser := requestedUser(cmd) user := flagUser @@ -225,7 +243,7 @@ func configuredOrgID(cmd *cobra.Command, cfg *appconfig.Config) string { if user == "" { user = cfg.DefaultUser } - return config.DecodeOrgID(cfg.Users[user].OrgID) + return orgKey(cfg.Users[user].OrgID) } // RootCommand returns the root cobra command for external introspection. diff --git a/internal/aiagent/aiagent_test.go b/internal/aiagent/aiagent_test.go index ae0d528..f520926 100644 --- a/internal/aiagent/aiagent_test.go +++ b/internal/aiagent/aiagent_test.go @@ -42,11 +42,18 @@ func TestBotTokenFindsTheAgent(t *testing.T) { } } +// The region is chosen after startup, so BotToken must reach the Studio host +// selected then, which a fake stands in for here. func TestBotTokenUsesTheSelectedRegionsStudio(t *testing.T) { + srv := studio(t) + old := studioURL + t.Cleanup(func() { studioURL = old }) + studioURL = func() string { return config.AiAgentBaseURL } config.SetCCRegion("eu1") + config.AiAgentBaseURL = srv.URL t.Cleanup(func() { config.SetCCRegion("") }) - if got := studioURL(); got != "https://studio.aiagent-eu1.cisco.com" { - t.Errorf("BotToken's Studio host = %s, want eu1's", got) + if got, err := BotToken("a1"); err != nil || got != "token-1" { + t.Fatalf("BotToken = %q, %v; it did not reach the selected Studio", got, err) } } diff --git a/internal/config/config.go b/internal/config/config.go index d23c350..f9ebee6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -68,13 +68,11 @@ const ( // CCRegions lists the Contact Center data centers. An org's Contact Center lives // in one of them, and each has its own API and AI Agent Studio host. -var CCRegions = []string{"us1", "eu1", "eu2", "anz1", "ca1", "jp1", "sg1"} +var CCRegions = []string{"us1", "eu1", "eu2", "anz1", "ca1", "jp1", "sg1", "in1"} // DefaultCCRegion is used when no region is given or saved for the org. const DefaultCCRegion = "us1" -var ccRegion = DefaultCCRegion - var ( CcBaseURL = ccHost(DefaultCCRegion) @@ -91,13 +89,10 @@ func SetCCRegion(region string) { if region == "" { region = DefaultCCRegion } - ccRegion = region CcBaseURL = ccHost(region) AiAgentBaseURL = aiAgentHost(region) } -func CCRegion() string { return ccRegion } - // analyticsCallingHosts maps a data region to the Detailed Call History (CDR) FQDN. // Calls to the default host are routed to the nearest region; if that region does // not hold the org's data the API returns HTTP 451 with the correct endpoint. diff --git a/site/src/content/docs/getting-started.mdx b/site/src/content/docs/getting-started.mdx index f31a088..57163d8 100644 --- a/site/src/content/docs/getting-started.mdx +++ b/site/src/content/docs/getting-started.mdx @@ -87,7 +87,7 @@ webex messaging [flags] # Webex Messaging APIs | `--output json\|table\|csv\|raw` | Output format (default: json) | | `--debug` | Show HTTP request/response details | | `--paginate` | Auto-paginate list results (ignored by commands that return one object) | -| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | +| `--cc-region us1\|eu1\|eu2\|anz1\|ca1\|jp1\|sg1\|in1` | Contact Center data center for `cc` commands (default: the organization's saved region, else `us1`) | ## Contact Center data center diff --git a/skill/SKILL.md b/skill/SKILL.md index a8598c4..05fee38 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -68,7 +68,7 @@ Aliases: `devices` for `device`, `meeting` for `meetings`, `msg` for `messaging` - `--paginate` — Auto-paginate list results - `--dry-run` — Print write requests (POST/PUT/DELETE/PATCH) without executing them; read operations still run normally - `--region us|ca|eu|eun|in|au` — Data region for region-specific hosts, used by `calling reports-detailed-call-history` (default: `us`). Also settable with `webex config set region ` or `WEBEX_REGION`. A `451` error means the org's data lives in another region — the error names the correct one. -- `--cc-region us1|eu1|eu2|anz1|ca1|jp1|sg1` — Contact Center data center for `cc` commands, including AI Agent Studio. Order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. Save it once per organization with `webex config set cc-region ` (add `--organization ` for another org); the flag alone saves nothing. +- `--cc-region us1|eu1|eu2|anz1|ca1|jp1|sg1|in1` — Contact Center data center for `cc` commands, including AI Agent Studio. Order: `--cc-region`, `WEBEX_CC_REGION`, the region saved for the organization, then `us1`. A person saves it once per organization in a terminal with `webex config set cc-region ` (add `--organization ` for another org); the flag alone saves nothing. The MCP server does not run `config set`: pass `cc-region` in `flags` instead. ## Contact Center `--orgid` Handling diff --git a/skill/cc/SKILL.md b/skill/cc/SKILL.md index 09c5c78..c29cab4 100644 --- a/skill/cc/SKILL.md +++ b/skill/cc/SKILL.md @@ -10,7 +10,7 @@ Alias: `webex contact-center ` **`--orgid` is auto-populated from the logged-in user's org.** Use `--orgid=` (with `=`) only when overriding. Both UUID and base64 org ID formats are accepted. -**Data center:** `cc` commands, including `ai-agent*`, go to the org's saved Contact Center data center, else `us1`. For an org outside the US, check `webex config get cc-region`; save it once with `webex config set cc-region `, or pass `--cc-region` for one command. +**Data center:** `cc` commands, including `ai-agent*`, go to the org's saved Contact Center data center, else `us1`. For an org outside the US, `webex config get cc-region` shows the saved one. Pass `--cc-region ` for one command (through MCP, `"cc-region"` in `flags`); a person saves it with `webex config set cc-region ` in a terminal, since MCP does not run `config set`. ## Resources