Repository navigation
Save each organization's Contact Center region - #22
Conversation
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 <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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW
edumansky
left a comment
There was a problem hiding this comment.
Codex review (gpt-6-astra, reasoning effort: high)
Regional routing misses AI agent token lookups and explicit CC organization overrides. All Go tests pass, but they do not cover these paths.
| ccRegion = region | ||
| CcBaseURL = ccHost(region) | ||
| AiAgentBaseURL = aiAgentHost(region) |
There was a problem hiding this comment.
[P2] Resolve the AI agent token lookup host dynamically
When a non-US region is selected, internal/aiagent/aiagent.go still uses its package-level baseURL, which copies config.AiAgentBaseURL during initialization and remains pointed at US1. Commands such as cc ai-agent get and transcript operations call BotToken before their regional request, so their prerequisite lookup goes to the wrong data center and fails. Make that lookup read the currently selected Studio host at request time.
There was a problem hiding this comment.
Valid, fixed in b2a8831. internal/aiagent now reads the Studio host through studioURL() when BotToken runs, after the command has chosen the region, instead of copying config.AiAgentBaseURL at startup. Regression test: TestBotTokenUsesTheSelectedRegionsStudio (internal/aiagent/aiagent_test.go).
| ccRegionFlag, _ := cmd.Flags().GetString("cc-region") | ||
| if err := resolveCCRegion(ccRegionFlag, cfg); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
[P2] Select the saved region using the explicit CC organization
When a caller overrides the target organization with the supported --orgid flag, region resolution still uses config.OrgID() rather than the organization actually sent in the request. For example, after saving EU1 for an organization, cc site delete --orgid=<that-org> --dry-run still shows a US1 URL when no global organization is configured. Resolve the region using the normalized explicit --orgid when present, falling back to the globally resolved organization.
There was a problem hiding this comment.
Valid, fixed in b2a8831. Region resolution now runs after --orgid is normalized and uses that org when the command was given one, falling back to the resolved org. Regression test: TestAnExplicitOrgidSelectsThatOrganizationsRegion (cmd/ccregion_test.go). With a built binary, cc site delete --id x --orgid=<org saved as eu1> --dry-run now shows https://api.wxcc-eu1.cisco.com/....
Codex review: outcomeCodex (
Checks
🤖 Generated with Claude Code |
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW
edumansky
left a comment
There was a problem hiding this comment.
Fable 5.2 review (reasoning effort: high)
No P1 found. Every CC and Studio request builder (generated commands, cmd/cc/custom_*.go, internal/search/paginate.go, internal/aiagent.BotToken, paging helpers, MCP child processes) reads the host at command time; there is no package-level or init() capture, and dry-runs confirmed eu1 for --organization, --orgid <uuid>, --orgid=<base64>, uploads and ai-agent. No skipAuth command touches a CC host. Two P2s: a data center that exists in DNS is missing from the allowed list, and commands that carry the org in a flag other than --orgid ignore it for region selection. The rest are P3 normalization/org-selection/test-hygiene gaps. All 14 hostnames for the 7 listed regions resolve in DNS.
Not findings: when a request reaches the wrong data center there is no hint to set cc-region (unlike the 451 hint for --region, internal/client/client.go:83-87); that follows from the no-probing design, but EU users who haven't saved a region will see an unexplained us1 error. cmd/root.go:133-145 (not changed by this PR) fills --org-id with the base64 org ID on the cc commands in finding 2, although its comment says "non-CC commands".
Disclosure: the reviewer ran cc ai-agent list --orgid=1111… --token dummy --dry-run --debug. --dry-run only stops writes, so one real GET went to studio.aiagent-eu1.cisco.com with the bearer dummy and a fake org UUID; it returned 401. No real credentials or tenant data were sent.
|
|
||
| // 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"} |
There was a problem hiding this comment.
[P2] India data center (in1) is missing, and strict validation leaves no way to reach it
api.wxcc-in1.cisco.com and studio.aiagent-in1.cisco.com both resolve; invented names (uk1, us2, eu3, xx9, …) do not, so there is no wildcard. An org in that data center cannot use webex cc at all. Reproduced: webex cc site delete --id x --token dummy --dry-run --cc-region in1 and config set cc-region in1 --organization … both fail with unknown Contact Center region "in1". Evidence is DNS only. Fix: add in1 to CCRegions and the docs/help lists; consider accepting any ^[a-z]+[0-9]+$ with a warning so a new data center doesn't need a release.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. in1 is added to CCRegions and to every list in the help, README, site and skills. Cisco documents a Webex Contact Center data center in India (https://help.webex.com/en-us/article/neahf4bb/Set-up-Webex-Contact-Center-in-India), and both hosts follow the naming of the other seven. Unlisted values are still refused, so a mistyped region fails loudly. Regression test: TestIndiaIsADataCenter.
| ccOrg := config.OrgID() | ||
| if f := cmd.Flags().Lookup("orgid"); f != nil && f.Value.String() != "" { | ||
| ccOrg = f.Value.String() | ||
| } |
There was a problem hiding this comment.
[P2] Commands that name the org with --org-id or --x-organization-id ignore it for the region
Only a flag named orgid is consulted. Eleven cc commands carry the org another way: agent-summaries list/list-2, subscriptions register-v1/v2/update-v1/v2 (--org-id in the body); agent-wellbeing subscribe-realtime-burnout-events/record-realtime-burnout-events, external-data-updates update-task-global-variables, search-metadata get (--x-organization-id header); journey create-workspace (--organization-id in the body). Reproduced: with eu1 saved for org EU and the default user in a US org, cc agent-summaries list --org-id <EU> … --dry-run printed POST https://api.wxcc-us1.cisco.com/summary/list, and cc external-data-updates update-task-global-variables --x-organization-id <EU> --dry-run printed PUT https://api.wxcc-us1… with X-Organization-Id: <EU>. Fix: also consult org-id, x-organization-id and organization-id (decoded) in the ccOrg block.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. requestOrgID checks --orgid, --org-id, --x-organization-id and --organization-id when the command was given one, and falls back to the resolved org. Regression test: TestEveryWayACommandNamesItsOrgSelectsThatOrgsRegion, with one subtest per flag. Dry run: cc external-data-updates update-task-global-variables --x-organization-id <org saved as eu1> now goes to api.wxcc-eu1.cisco.com.
| func configuredOrgID(cmd *cobra.Command, cfg *appconfig.Config) string { | ||
| if org, _ := cmd.Flags().GetString("organization"); org != "" { | ||
| return config.DecodeOrgID(org) | ||
| } |
There was a problem hiding this comment.
[P3] Org keys are not normalized or validated
DecodeOrgID returns UUIDs and unrecognized strings as-is. Reproduced: config set cc-region eu2 --organization AAAAAAAA-AAAA-… saved under the uppercase key; cc site delete --organization aaaaaaaa-aaaa-… --dry-run then went to us1. config set cc-region eu1 --organization Acme reported success and saved the key "Acme". Fix: lowercase the key on save and lookup, and reject a value that does not decode to a UUID.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. orgKey lowercases the decoded UUID wherever a saved region is written or looked up, and config set cc-region refuses an organization that is not an org ID. Regression tests: TestAnOrgIsSavedAndFoundWhateverTheCaseOfItsID, TestConfigSetCCRegionRefusesSomethingThatIsNotAnOrgID.
| flagUser, envUser := requestedUser(cmd) | ||
| user := flagUser | ||
| if user == "" { | ||
| user = envUser | ||
| } | ||
| if user == "" { | ||
| user = cfg.DefaultUser | ||
| } | ||
| return config.DecodeOrgID(cfg.Users[user].OrgID) | ||
| } |
There was a problem hiding this comment.
[P3] configuredOrgID picks a different org than API commands when the token is not a stored login
With --token or WEBEX_TOKEN, API commands ignore --user, WEBEX_USER and the folder-local user and use the default user's org (cmd/root.go:94-108, internal/auth/resolve.go:71-89); configuredOrgID uses the named user's org. Reproduced: default user A (US org), folder-local user B (EU org); config set cc-region eu1 saved for B's org; WEBEX_TOKEN=dummy webex cc site delete --dry-run and --token dummy --user b@… both went to api.wxcc-us1…/organization/<A's org>/…. Traced, not run: with WEBEX_REFRESH_TOKEN, API commands use the org from the refresh response, while config set/get uses the default user's org or fails. Fix: mirror the chain; config set already prints the org it chose, which limits the harm.
There was a problem hiding this comment.
Not changed. This only happens when a raw --token or WEBEX_TOKEN is used together with --user or a folder-local user. config set cc-region prints the org it saved for, so the mismatch is visible, and --organization gives an exact choice. Copying the full token-resolution chain into a command that runs without a login would add more code than the case justifies.
| func resolveCCRegion(flag, orgID string, cfg *appconfig.Config) error { | ||
| region := flag | ||
| if region == "" { | ||
| region = os.Getenv("WEBEX_CC_REGION") | ||
| } | ||
| if region == "" { | ||
| region = cfg.CCRegions[orgID] | ||
| } | ||
| return setCCRegion(region) | ||
| } |
There was a problem hiding this comment.
[P3] A whitespace-only WEBEX_CC_REGION or --cc-region overrides the saved region with us1
The emptiness test runs before TrimSpace. Reproduced: with eu1 saved, WEBEX_CC_REGION=" " webex cc site delete … --dry-run went to api.wxcc-us1. Fix: trim before the == "" checks.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. resolveCCRegion trims the flag and the environment value before checking whether they are empty. Regression test: TestABlankRegionDoesNotHideTheSavedOne.
|
|
||
| // 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"` |
There was a problem hiding this comment.
[P3] An older binary that rewrites config.json drops cc_regions
Reproduced: after the PR binary saved eu1, a binary built from origin/main ran config set region eu, which rewrote the file without cc_regions; config get cc-region then returned us1 (default). Any login, auth switch or set-org from an older install does the same. This follows from the config struct design; a fix would be to preserve unknown fields on save, or at minimum treat it as a known limitation.
There was a problem hiding this comment.
Not changed. This applies to every config field added since a given release, not only cc_regions. Preserving unknown fields on save would be a separate change to appconfig.
|
|
||
| **`--orgid` is auto-populated from the logged-in user's org.** Use `--orgid=<uuid>` (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 <us1|eu1|eu2|anz1|ca1|jp1|sg1>`, or pass `--cc-region` for one command. |
There was a problem hiding this comment.
[P3] The skill tells agents to run a command the MCP server blocks
config set is in blockedPrefixes (cmd/mcp/dispatcher.go:30), so an agent on MCP gets "command 'config set' is not permitted" (also skill/SKILL.md:71). Traced through blockedCommand, not run. Fix: tell MCP agents to pass "cc-region" in flags, and that saving is done by the user in a terminal.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. Both skills now tell MCP agents to pass "cc-region" in flags, and say that a person saves the region with config set in a terminal.
| 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) | ||
| } |
There was a problem hiding this comment.
[P3] config get cc-region reports the saved value, not the effective one
Reproduced: WEBEX_CC_REGION=jp1 webex config get cc-region and --cc-region sg1 both print eu1 (saved …), though the skill tells agents to check this command to learn where requests go. With no resolvable org it prints us1 (default) without saying no org was found. There is also no way to remove a saved region: config set cc-region "" is refused (reproduced).
There was a problem hiding this comment.
Partly fixed in 4a1e3f3. config get shows saved values for every key, so cc-region keeps that behaviour, and the cc skill now says it shows the saved region. When no organization is found, it now says so: us1 (default; no organization found: log in, or pass --organization). Regression test: TestConfigGetCCRegionSaysWhenThereIsNoOrganization. Removing a saved region is not added: config set cc-region us1 has the same effect.
| 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) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
[P3] Tests are not hermetic and leak state
Reproduced: WEBEX_READ_ONLY=1 go test ./cmd fails TestAnExplicitOrgidSelectsThatOrganizationsRegion ("read-only mode does not accept --token"); WEBEX_REGION=<invalid> fails three tests. isolate clears neither variable, nor WEBEX_REFRESH_TOKEN. With an overlay test, after that test rootCmd's token flag and config.Token() are still "dummy". Fix: have isolate empty WEBEX_READ_ONLY, WEBEX_REGION, WEBEX_TOKEN, WEBEX_REFRESH_TOKEN, and reset the token flag and config.SetToken("") in cleanup. Also internal/aiagent/aiagent_test.go:45-51 asserts only studioURL(), not the host BotToken actually calls.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3. isolate empties WEBEX_READ_ONLY, WEBEX_REGION, WEBEX_TOKEN, WEBEX_REFRESH_TOKEN, WEBEX_USER and WEBEX_CC_REGION, and its cleanup resets the token flag and config.SetToken(""). go test ./cmd passes with WEBEX_READ_ONLY=1 WEBEX_REGION=bogus set. TestBotTokenUsesTheSelectedRegionsStudio now calls BotToken and checks that it reaches the selected Studio, a fake server here, rather than only checking studioURL().
| // DefaultCCRegion is used when no region is given or saved for the org. | ||
| const DefaultCCRegion = "us1" | ||
|
|
||
| var ccRegion = DefaultCCRegion |
There was a problem hiding this comment.
[P3] Dead code
ccRegion and CCRegion() (line 99) have no callers.
There was a problem hiding this comment.
Valid, fixed in 4a1e3f3: ccRegion and CCRegion() are removed.
Fable 5.2 review (high effort): outcomeFable reviewed
Checks
🤖 Generated with Claude Code |
cccommands, AI Agent Studio included, always went to the US data center (api.wxcc-us1.cisco.com,studio.aiagent-us1.cisco.com), so the CLI could not reach an organization whose Contact Center is in another one. This adds commands to set and change the CC API region.What changes
--cc-region us1|eu1|eu2|anz1|ca1|jp1|sg1andWEBEX_CC_REGIONchoose the data center for one command.webex config set cc-region <region>saves the region for the current organization, or the one named with--organization.config.jsonkeeps a map from org UUID to region, so organizations in different data centers each keep their own.webex config get cc-regionshows the current organization's region and where it came from.us1. With none of these set, nothing changes from today.Tests
cmd/ccregion_test.go:getfor an org with a saved region and one without;go vet ./...andgo testfor every package, run one at a time: pass.--dry-runsends toapi.wxcc-eu1.cisco.comfor an org saved aseu1, tous1for another org, and tosg1with--cc-region sg1.codegen/api_spec.json, a generated file a fresh checkout does not have, so it's environmental. This PR doesn't touch codegen.npm run buildinsite/: passes. On a Mac with Homebrew'svips, run the install withSHARP_IGNORE_GLOBAL_LIBVIPS=1 npm ci. Otherwise sharp tries to compile against the system libvips and the install fails.🤖 Generated with Claude Code
https://claude.ai/code/session_01JAtGHcFAKjQdB6dwDKd8mW