maxc-cli 0.8.0__tar.gz → 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/PKG-INFO +1 -1
  2. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/__init__.py +1 -1
  3. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/_samples.py +4 -3
  4. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/app.py +90 -12
  5. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_providers.py +19 -3
  6. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/cli.py +75 -8
  7. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/config.py +2 -0
  8. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/helpers.py +18 -0
  9. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/oauth.py +12 -5
  10. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
  11. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/PKG-INFO +1 -1
  12. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/SOURCES.txt +1 -0
  13. maxc_cli-0.8.1/tests/test_connection_bootstrap.py +279 -0
  14. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_oauth.py +40 -4
  15. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/MANIFEST.in +0 -0
  16. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/README.md +0 -0
  17. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/pyproject.toml +0 -0
  18. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/scripts/pyinstaller_entry.py +0 -0
  19. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/scripts/regression_test.py +0 -0
  20. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/setup.cfg +0 -0
  21. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/setup.py +0 -0
  22. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/__main__.py +0 -0
  23. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/agent_platforms.py +0 -0
  24. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/audit.py +0 -0
  25. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_continuation.py +0 -0
  26. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/__init__.py +0 -0
  27. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/auth.py +0 -0
  28. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/catalog.py +0 -0
  29. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/data.py +0 -0
  30. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/job.py +0 -0
  31. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/mcp.py +0 -0
  32. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/meta.py +0 -0
  33. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/odps.py +0 -0
  34. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/query.py +0 -0
  35. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/semantic.py +0 -0
  36. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/cache.py +0 -0
  37. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
  38. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/enterprise_tls.py +0 -0
  39. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/exceptions.py +0 -0
  40. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/help_format.py +0 -0
  41. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/job_ids.py +0 -0
  42. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/masking.py +0 -0
  43. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/mcp_serve.py +0 -0
  44. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/models.py +0 -0
  45. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/odps_runtime.py +0 -0
  46. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/output.py +0 -0
  47. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/proxy_auth.py +0 -0
  48. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic.py +0 -0
  49. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic_management.py +0 -0
  50. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/setting_parser.py +0 -0
  51. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/SKILL.md +0 -0
  52. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  53. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  54. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/command-patterns.md +0 -0
  55. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  56. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  57. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  58. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  59. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/red-lines.md +0 -0
  60. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  61. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/setup-install.md +0 -0
  62. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  63. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  64. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  65. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/state_permissions.py +0 -0
  66. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/store.py +0 -0
  67. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli/utils.py +0 -0
  68. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  69. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  70. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/requires.txt +0 -0
  71. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
  72. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_hints_and_cli.py +0 -0
  73. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_platforms.py +0 -0
  74. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands.py +0 -0
  75. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands_context.py +0 -0
  76. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_ai_native_contract_regressions.py +0 -0
  77. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_auth_logout.py +0 -0
  78. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_auth.py +0 -0
  79. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_data.py +0 -0
  80. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_data_serialization.py +0 -0
  81. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_mcp.py +0 -0
  82. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_backend_meta.py +0 -0
  83. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_build_release_archive_compat.py +0 -0
  84. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_build_release_script.py +0 -0
  85. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cache.py +0 -0
  86. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_catalog.py +0 -0
  87. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_catalog_bootstrap.py +0 -0
  88. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_arg_validation.py +0 -0
  89. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_mock.py +0 -0
  90. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  91. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_compat.py +0 -0
  92. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_config_atomic_write.py +0 -0
  93. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_e2e_smoke.py +0 -0
  94. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_effective_hints_contract.py +0 -0
  95. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_enterprise_tls.py +0 -0
  96. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_envelope_shape.py +0 -0
  97. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_error_self_correction.py +0 -0
  98. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_error_translation.py +0 -0
  99. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_exit_codes.py +0 -0
  100. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_external_auth.py +0 -0
  101. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_flag_hoist.py +0 -0
  102. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_help_format.py +0 -0
  103. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_help_version_e2e.py +0 -0
  104. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_helpers.py +0 -0
  105. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_helpers_csv.py +0 -0
  106. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_installer_contracts.py +0 -0
  107. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_integration.py +0 -0
  108. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_integration_real.py +0 -0
  109. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_improvements.py +0 -0
  110. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_inspection.py +0 -0
  111. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_job_store_durability.py +0 -0
  112. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_kb_commands.py +0 -0
  113. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_manifest_runtime_contract.py +0 -0
  114. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_masking.py +0 -0
  115. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_mcp_serve.py +0 -0
  116. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_meta_schema_and_partition_cols.py +0 -0
  117. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_odps_runtime.py +0 -0
  118. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_output_action_safety.py +0 -0
  119. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_output_format_contract.py +0 -0
  120. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_packaging_metadata.py +0 -0
  121. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_phase1_improvements.py +0 -0
  122. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_proxy_auth.py +0 -0
  123. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_pyinstaller_bundle.py +0 -0
  124. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_python39_compat.py +0 -0
  125. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_query_auto_promote.py +0 -0
  126. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_query_result_csv_fallback.py +0 -0
  127. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_management.py +0 -0
  128. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_scope.py +0 -0
  129. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_semantic_transport.py +0 -0
  130. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_setting_parser.py +0 -0
  131. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_cli_consistency.py +0 -0
  132. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_eval.py +0 -0
  133. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_skill_renderer.py +0 -0
  134. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_startup_imports.py +0 -0
  135. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_state_permissions.py +0 -0
  136. {maxc_cli-0.8.0 → maxc_cli-0.8.1}/tests/test_state_portability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.8.0"
5
+ __version__ = "0.8.1"
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  SAMPLES: dict[str, str] = {
10
10
  "__top__": (
11
11
  "maxc auth login\n"
12
- 'maxc query "SELECT 1"\n'
12
+ 'maxc query "SELECT 1" --project my_proj --json\n'
13
13
  "maxc meta list-tables --project my_proj"
14
14
  ),
15
15
  "semantic.list": "maxc semantic list --namespace <account_id> --json",
@@ -23,7 +23,7 @@ SAMPLES: dict[str, str] = {
23
23
  "semantic.delete": "maxc semantic delete sales --namespace <account_id> --expected-spec-id <spec_id> --json",
24
24
  # ── query ──────────────────────────────────────────────────────────────
25
25
  "query": (
26
- 'maxc query "SELECT 1"\n'
26
+ 'maxc query "SELECT 1" --project my_proj --json\n'
27
27
  "maxc query cost \"SELECT * FROM default.big_table WHERE ds='20260101'\"\n"
28
28
  "maxc query explain \"SELECT * FROM default.events WHERE dt='20260101'\""
29
29
  ),
@@ -31,7 +31,8 @@ SAMPLES: dict[str, str] = {
31
31
  "auth": "maxc auth login --oauth\nmaxc auth whoami --json\nmaxc auth logout --json",
32
32
  "auth.login": (
33
33
  "maxc auth login --oauth\n"
34
- "maxc auth login --from-env --json"
34
+ "maxc auth login --from-env --json\n"
35
+ "maxc auth login --reuse-auth --project my_proj --region cn-hangzhou --json"
35
36
  ),
36
37
  "auth.login-proxy": (
37
38
  "maxc auth login-proxy --project <project> --odps-endpoint <endpoint> --json",
@@ -158,6 +158,7 @@ class _PickerInputs:
158
158
  env_settings: 'dict[str, str]'
159
159
  existing_auth: 'AuthConfig'
160
160
  reselect: 'bool' = False
161
+ allow_interactive: 'bool' = True
161
162
 
162
163
 
163
164
  @dataclass(frozen=True)
@@ -193,9 +194,15 @@ class MaxCApp:
193
194
  cwd: 'Path',
194
195
  config_path: 'Path | None' = None,
195
196
  load_backend: 'bool' = True,
197
+ project_override: 'str | None' = None,
196
198
  ) -> 'None':
197
199
  self.cwd = cwd
198
200
  self.config = load_config(cwd, config_path)
201
+ # Request-level routing must be applied before backend authentication
202
+ # validates the connection. It is never persisted as a login default.
203
+ if project_override:
204
+ self.config.command_project = project_override
205
+ self.config.default_project = project_override
199
206
  self._cache: LocalCache | None = None
200
207
  # Credential/catalog providers may need the cache later, but ordinary
201
208
  # authenticated commands must not create or migrate SQLite merely by
@@ -4708,6 +4715,7 @@ class MaxCApp:
4708
4715
  projects: 'list[dict[str, Any]]',
4709
4716
  *,
4710
4717
  title_prefix: 'str',
4718
+ endpoint_flag: 'str' = "--endpoint",
4711
4719
  ) -> 'list[SuggestedAction]':
4712
4720
  actions: list[SuggestedAction] = []
4713
4721
  for project_data in projects:
@@ -4717,7 +4725,7 @@ class MaxCApp:
4717
4725
  tokens = [*base_tokens, "--project", project_id]
4718
4726
  endpoint_value = project_data.get("endpoint")
4719
4727
  endpoint_placeholder = endpoint_value is None
4720
- tokens.extend(["--endpoint", str(endpoint_value or "__MAXC_ENDPOINT__")])
4728
+ tokens.extend([endpoint_flag, str(endpoint_value or "__MAXC_ENDPOINT__")])
4721
4729
  if project_data.get("region"):
4722
4730
  tokens.extend(["--region", str(project_data["region"])])
4723
4731
  if project_data.get("tunnel_endpoint"):
@@ -4755,6 +4763,8 @@ class MaxCApp:
4755
4763
  region_name: 'str | None' = None,
4756
4764
  tunnel_endpoint: 'str | None' = None,
4757
4765
  from_env: 'bool' = False,
4766
+ reuse_auth: 'bool' = False,
4767
+ allow_interactive_picker: 'bool' = True,
4758
4768
  no_validate: 'bool' = False,
4759
4769
  target_config_path: 'Path | None' = None,
4760
4770
  catalog_endpoint: 'str | None' = None,
@@ -4768,8 +4778,39 @@ class MaxCApp:
4768
4778
  existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
4769
4779
  env_settings = load_odps_env()
4770
4780
 
4771
- # Resolve credentials first — the picker needs the AK/secret/STS in hand.
4772
- if continuation_id:
4781
+ # Connection setup reuses the effective identity, including the
4782
+ # wrapper's refreshable profile, without importing its temporary keys.
4783
+ if reuse_auth:
4784
+ if any((access_id, secret_access_key, security_token, continuation_id)) or from_env or _oauth_config:
4785
+ raise ValidationError("--reuse-auth cannot replace credentials or resume a login.")
4786
+ effective, _, _ = resolve_odps_settings(self.config)
4787
+ provider = infer_auth_provider(self.config, effective)
4788
+ resolved_access_id = effective.get("access_id")
4789
+ resolved_secret = effective.get("secret_access_key")
4790
+ resolved_token = effective.get("security_token")
4791
+ if provider == "oauth":
4792
+ from .auth_providers import _oauth_persist_path
4793
+ from .oauth import ensure_oauth_sts
4794
+
4795
+ sts = ensure_oauth_sts(self.config.auth, config_path=_oauth_persist_path(self.config))
4796
+ resolved_access_id = sts.access_key_id
4797
+ resolved_secret = sts.access_key_secret
4798
+ resolved_token = sts.security_token
4799
+ if provider == "sts_token" and not resolved_token:
4800
+ raise ValidationError(
4801
+ "The existing STS identity is missing its security token.",
4802
+ suggestion="Restore the existing STS profile before configuring the connection.",
4803
+ )
4804
+ if not resolved_access_id or not resolved_secret:
4805
+ raise ValidationError(
4806
+ "No reusable AccessKey/STS/OAuth credentials are available.",
4807
+ suggestion="Configure the intended identity before selecting a project.",
4808
+ )
4809
+ # Preserve the active provider's suppression of unrelated shell
4810
+ # values. Routing defaults below come from its effective settings.
4811
+ env_settings = effective
4812
+ existing_auth = AuthConfig.from_mapping(self.config.auth.to_mapping())
4813
+ elif continuation_id:
4773
4814
  if any((access_id, secret_access_key, security_token)) or from_env:
4774
4815
  raise ValidationError(
4775
4816
  "Do not combine an auth continuation with credential flags or --from-env.",
@@ -4848,6 +4889,7 @@ class MaxCApp:
4848
4889
  env_settings=env_settings,
4849
4890
  existing_auth=existing_auth,
4850
4891
  reselect=reselect,
4892
+ allow_interactive=allow_interactive_picker,
4851
4893
  )
4852
4894
  )
4853
4895
  except ProjectPickerPending as exc:
@@ -4895,6 +4937,18 @@ class MaxCApp:
4895
4937
  warnings=[],
4896
4938
  ),
4897
4939
  )
4940
+ if reuse_auth:
4941
+ base_tokens = self._auth_action_prefix(target_path)
4942
+ base_tokens.extend(["auth", "login", "--reuse-auth"])
4943
+ if catalog_endpoint:
4944
+ base_tokens.extend(["--catalog-endpoint", catalog_endpoint])
4945
+ if no_validate:
4946
+ base_tokens.append("--no-validate")
4947
+ pending_envelope.agent_hints.actions = self._project_selection_actions(
4948
+ base_tokens, projects_data, title_prefix="Configure connection for",
4949
+ endpoint_flag="--odps-endpoint",
4950
+ )
4951
+ return pending_envelope
4898
4952
  if _oauth_config is not None:
4899
4953
  return pending_envelope
4900
4954
  if continuation_id:
@@ -4934,6 +4988,15 @@ class MaxCApp:
4934
4988
  pending_envelope.metadata["continuation_expires_at_unix"] = expires_at
4935
4989
  return pending_envelope
4936
4990
 
4991
+ from .catalog_bootstrap import region_to_endpoint, region_to_tunnel_endpoint
4992
+
4993
+ fallback_region = derived_region or env_settings.get("region_name") or existing_auth.region_name
4994
+ derived_region = fallback_region
4995
+ if not derived_endpoint and not env_settings.get("endpoint") and not existing_auth.endpoint:
4996
+ derived_endpoint = region_to_endpoint(fallback_region)
4997
+ if not derived_tunnel and not env_settings.get("tunnel_endpoint") and not existing_auth.tunnel_endpoint:
4998
+ derived_tunnel = region_to_tunnel_endpoint(fallback_region)
4999
+
4937
5000
  resolved_auth = AuthConfig(
4938
5001
  access_id=resolved_access_id,
4939
5002
  secret_access_key=resolved_secret,
@@ -4983,10 +5046,25 @@ class MaxCApp:
4983
5046
  persisted_auth.provider = "oauth"
4984
5047
  persisted_auth.oauth = _oauth_config
4985
5048
  migrate_legacy_session_override(target_path)
4986
- persist_login_config(
4987
- target_path,
4988
- auth=persisted_auth,
4989
- )
5049
+ if reuse_auth:
5050
+ connection_fields = {
5051
+ name: getattr(resolved_auth, name)
5052
+ for name in ("project", "endpoint", "region_name", "tunnel_endpoint")
5053
+ if getattr(resolved_auth, name)
5054
+ }
5055
+
5056
+ def save_connection(payload: 'dict[str, Any]') -> 'None':
5057
+ auth_mapping = payload.setdefault("auth", {})
5058
+ if not isinstance(auth_mapping, dict):
5059
+ raise ValidationError("The auth configuration must be a mapping.")
5060
+ auth_mapping.update(connection_fields)
5061
+ payload["default_project"] = resolved_auth.project
5062
+ if resolved_auth.region_name:
5063
+ payload["default_region"] = resolved_auth.region_name
5064
+
5065
+ update_config_mapping(target_path, save_connection)
5066
+ else:
5067
+ persist_login_config(target_path, auth=persisted_auth)
4990
5068
 
4991
5069
  continuation_cleanup_warning: str | None = None
4992
5070
  if continuation_id:
@@ -5007,7 +5085,7 @@ class MaxCApp:
5007
5085
  # Always remind callers that AK/SK is stored in plaintext YAML (chmod
5008
5086
  # 0600) — flagged in CLAUDE.md as a known limitation. Skip for STS
5009
5087
  # tokens since those are short-lived and self-expiring.
5010
- if not resolved_auth.security_token:
5088
+ if not reuse_auth and not resolved_auth.security_token:
5011
5089
  warnings.append(
5012
5090
  f"AccessKey saved in plaintext at `{target_path}` (file mode 0600). "
5013
5091
  f"For shared/CI environments prefer `auth login-external` with a credential helper, "
@@ -5017,7 +5095,7 @@ class MaxCApp:
5017
5095
  warnings.append(
5018
5096
  "Credentials were imported from environment variables (--from-env) and saved to config."
5019
5097
  )
5020
- elif any(
5098
+ elif not reuse_auth and any(
5021
5099
  env_settings.get(name)
5022
5100
  for name in ("access_id", "secret_access_key", "security_token", "endpoint", "region_name", "tunnel_endpoint")
5023
5101
  ):
@@ -5055,8 +5133,8 @@ class MaxCApp:
5055
5133
 
5056
5134
  login_metadata = {
5057
5135
  "config_path": str(target_path),
5058
- "written_fields": sorted(persisted_auth.to_mapping().keys()),
5059
- "auth_storage": "config_file",
5136
+ "written_fields": sorted(connection_fields if reuse_auth else persisted_auth.to_mapping().keys()),
5137
+ "auth_storage": "existing_identity" if reuse_auth else "config_file",
5060
5138
  }
5061
5139
  envelope = Envelope(
5062
5140
  command="auth.login",
@@ -5750,7 +5828,7 @@ class MaxCApp:
5750
5828
  from . import catalog_bootstrap as _catalog_bootstrap
5751
5829
 
5752
5830
  # 2. Picker not viable (non-TTY or --no-picker).
5753
- if inputs.no_picker or not sys.stdin.isatty():
5831
+ if inputs.no_picker or not inputs.allow_interactive or not sys.stdin.isatty():
5754
5832
  # Non-TTY + picker not disabled: list projects for structured output
5755
5833
  catalog_warning: str | None = None
5756
5834
  if not inputs.no_picker and not from_env:
@@ -259,7 +259,14 @@ def resolve_auth_connection(
259
259
  if missing:
260
260
  raise ValidationError(
261
261
  f"STS authentication is missing required fields: {', '.join(missing)}.",
262
- suggestion="Provide access_id, secret_access_key, security_token, project, and endpoint.",
262
+ suggestion=(
263
+ "Pass `--project <project>` on the data command; "
264
+ "or specify `maxc auth login --reuse-auth --project <project> "
265
+ "--odps-endpoint <endpoint>`."
266
+ if set(missing) <= {"project", "endpoint"}
267
+ else "Provide access_id, secret_access_key, security_token, project, and endpoint."
268
+ ),
269
+ context={"missing_fields": missing},
263
270
  )
264
271
  try:
265
272
  from odps.accounts import StsAccount
@@ -303,7 +310,10 @@ def resolve_auth_connection(
303
310
  if not (settings.get("project") or config.default_project) or not settings.get("endpoint"):
304
311
  raise ValidationError(
305
312
  "OAuth login is incomplete: project or endpoint missing.",
306
- suggestion="Run `maxc auth login --oauth --project <project> --endpoint <endpoint>`.",
313
+ suggestion="Pass `--project <project>` on the data command, or use `maxc auth login --reuse-auth` to configure a default.",
314
+ context={"missing_fields": [
315
+ name for name in ("project", "endpoint") if not settings.get(name)
316
+ ]},
307
317
  )
308
318
  try:
309
319
  from odps.accounts import StsAccount
@@ -334,7 +344,13 @@ def resolve_auth_connection(
334
344
  if missing:
335
345
  raise ValidationError(
336
346
  f"MaxCompute connection settings are incomplete: {', '.join(missing)}.",
337
- suggestion="Run `maxc auth login` or set the required environment variables.",
347
+ suggestion=(
348
+ "Pass `--project <project>` on the data command; "
349
+ "or specify --project <project> --odps-endpoint <endpoint>."
350
+ if set(missing) <= {"project", "endpoint"}
351
+ else "Run `maxc auth login` or set the required environment variables."
352
+ ),
353
+ context={"missing_fields": missing},
338
354
  )
339
355
  return ResolvedAuthConnection(
340
356
  auth_type="access_key",
@@ -19,7 +19,7 @@ from . import agent_platforms
19
19
  from ._samples import SAMPLES
20
20
  from .exceptions import ErrorPayload, MaxCError, ValidationError
21
21
  from .help_format import AliyunRawTextFormatter, AliyunStyleFormatter
22
- from .models import AgentHints, Envelope, action, suggested_action_is_safe
22
+ from .models import AgentHints, Envelope, SuggestedAction, action, suggested_action_is_safe
23
23
  from .output import emit_json, emit_ndjson, render_error, render_key_values, render_table
24
24
  from .utils import current_cli_entry_point, extract_table_names, now_utc_iso, read_sql_input
25
25
 
@@ -786,7 +786,11 @@ def build_parser() -> argparse.ArgumentParser:
786
786
  "--project",
787
787
  help="Target MaxCompute project (omit to pop an interactive picker over the catalog)",
788
788
  )
789
- auth_login.add_argument("--endpoint", help="MaxCompute endpoint URL")
789
+ auth_login.add_argument("--endpoint", "--odps-endpoint", dest="endpoint", help="MaxCompute endpoint URL (defaults from project/profile region)")
790
+ auth_login.add_argument(
791
+ "--reuse-auth", action="store_true",
792
+ help="Select project/endpoint using existing credentials; save connection settings only",
793
+ )
790
794
  auth_login.add_argument("--region", dest="region_name", help="MaxCompute region name")
791
795
  auth_login.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
792
796
  auth_login.add_argument(
@@ -1628,11 +1632,14 @@ def run(
1628
1632
  app: MaxCApp | None = None
1629
1633
  try:
1630
1634
  _validate_output_request(args, command_name)
1631
- app = app_type(
1632
- cwd=working_dir,
1633
- config_path=config_path,
1634
- load_backend=_should_load_backend(command_name),
1635
- )
1635
+ app_kwargs = {
1636
+ "cwd": working_dir,
1637
+ "config_path": config_path,
1638
+ "load_backend": _should_load_backend(command_name),
1639
+ }
1640
+ if app_kwargs["load_backend"] and getattr(args, "project", None):
1641
+ app_kwargs["project_override"] = args.project
1642
+ app = app_type(**app_kwargs)
1636
1643
  args.handler(app, args, stdout)
1637
1644
  return getattr(args, "_envelope_exit_code", 0)
1638
1645
  except MaxCError as exc:
@@ -1702,6 +1709,40 @@ def run(
1702
1709
  ),
1703
1710
  }
1704
1711
  _hints = _error_hints.get(exc.error_code)
1712
+ missing_fields = (exc.context or {}).get("missing_fields")
1713
+ if (exc.error_code == "VALIDATION_ERROR" and missing_fields
1714
+ and set(missing_fields) <= {"project", "endpoint"}):
1715
+ prefix = current_cli_entry_point()
1716
+ if requested_config_path is not None:
1717
+ prefix += " --config " + shlex.quote(str(requested_config_path))
1718
+ if getattr(args, "user_agent", None):
1719
+ prefix += " --user-agent " + shlex.quote(args.user_agent)
1720
+ exc.recoverable = True
1721
+ connection_action = SuggestedAction(
1722
+ id="auth.login", title="Configure connection using existing credentials",
1723
+ command=prefix + " auth login --reuse-auth --json",
1724
+ executable=True, effect="local_write", confirmation_required=True,
1725
+ agent_allowed=True,
1726
+ )
1727
+ actions = []
1728
+ if "project" in missing_fields and hasattr(args, "project"):
1729
+ retry_tokens = [*current_cli_entry_point().split(), *argv_list,
1730
+ "--project", "__MAXC_PROJECT__"]
1731
+ retry_command = " ".join(shlex.quote(token) for token in retry_tokens).replace("__MAXC_PROJECT__", "<project>")
1732
+ original_action = action(command_name)
1733
+ forced_write = bool(getattr(args, "force", False))
1734
+ actions.append(SuggestedAction(
1735
+ id=command_name, title="Specify the target project",
1736
+ command=retry_command, executable=False,
1737
+ effect="remote_write" if forced_write else original_action.effect,
1738
+ confirmation_required=forced_write or original_action.confirmation_required,
1739
+ agent_allowed=original_action.agent_allowed,
1740
+ placeholders={"project": "<project>"},
1741
+ ))
1742
+ exc.suggestion = "Specify the target project: " + retry_command
1743
+ if "endpoint" in missing_fields:
1744
+ exc.suggestion += ". Configure the missing endpoint before retrying."
1745
+ _hints = AgentHints(actions=actions + [connection_action])
1705
1746
  # Build schema context for SQL errors to enable agent self-correction
1706
1747
  if app is not None and exc.error_code in (
1707
1748
  "SQL_ERROR", "NOT_FOUND", "SCHEMA_NOT_FOUND", "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND",
@@ -1725,6 +1766,10 @@ def run(
1725
1766
  error=exc.to_payload(),
1726
1767
  agent_hints=_hints,
1727
1768
  )
1769
+ if missing_fields and set(missing_fields) <= {"project", "endpoint"} and _hints:
1770
+ payload.error.recovery_steps = [
1771
+ item.command for item in _hints.actions
1772
+ ]
1728
1773
  if _is_job_command(args):
1729
1774
  _prepare_job_failure_envelope(payload, args)
1730
1775
  if _is_job_wait_stream(args):
@@ -2440,6 +2485,14 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
2440
2485
  args.security_token,
2441
2486
  )
2442
2487
  )
2488
+ if args.reuse_auth and (
2489
+ args.oauth or args.from_env or explicit_credentials
2490
+ or args.login_continuation or args.oauth_continuation
2491
+ ):
2492
+ raise ValidationError(
2493
+ "`--reuse-auth` cannot be combined with a new identity or auth continuation.",
2494
+ suggestion="Use --reuse-auth alone to configure the existing identity's connection.",
2495
+ )
2443
2496
  if args.oauth:
2444
2497
  if args.from_env or explicit_credentials:
2445
2498
  raise ValidationError(
@@ -2506,6 +2559,8 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
2506
2559
  region_name=args.region_name,
2507
2560
  tunnel_endpoint=args.tunnel_endpoint,
2508
2561
  from_env=args.from_env,
2562
+ reuse_auth=args.reuse_auth,
2563
+ allow_interactive_picker=not args.reuse_auth or not _is_json_mode(args),
2509
2564
  no_validate=args.no_validate,
2510
2565
  target_config_path=args.requested_config_path,
2511
2566
  catalog_endpoint=args.catalog_endpoint,
@@ -2721,6 +2776,14 @@ def _manifest_requirements(command: str) -> dict[str, Any]:
2721
2776
  "mode": "required",
2722
2777
  "reason": "A new OAuth authorization and STS exchange use network APIs.",
2723
2778
  },
2779
+ {
2780
+ "when": {"all": [
2781
+ _manifest_condition("reuse_auth", equals=True),
2782
+ {"runtime": "active_provider_is_oauth_and_cached_sts_expiring"},
2783
+ ]},
2784
+ "mode": "required",
2785
+ "reason": "Reusing saved OAuth may refresh temporary credentials without browser authorization.",
2786
+ },
2724
2787
  {
2725
2788
  "when": _manifest_condition("no_validate", equals=False),
2726
2789
  "mode": "required",
@@ -2968,12 +3031,16 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
2968
3031
  "create_or_replace",
2969
3032
  "auth_config",
2970
3033
  when={"runtime": "login_succeeds"},
3034
+ note="--reuse-auth preserves the existing identity and writes connection fields only.",
2971
3035
  ),
2972
3036
  _manifest_effect(
2973
3037
  "local",
2974
3038
  "create",
2975
3039
  "owner_only_access_key_continuation",
2976
- when={"runtime": "access_key_project_selection_pending"},
3040
+ when={"all": [
3041
+ {"runtime": "access_key_project_selection_pending"},
3042
+ _manifest_condition("reuse_auth", equals=False),
3043
+ ]},
2977
3044
  ),
2978
3045
  _manifest_effect(
2979
3046
  "local",
@@ -381,6 +381,8 @@ class MaxCConfig:
381
381
  sources: 'list[Path]'
382
382
  mcqa: 'McqaConfig' = field(default_factory=McqaConfig)
383
383
  mcp: 'McpConfig' = field(default_factory=McpConfig)
384
+ # Only set in memory for this invocation, never loaded or saved from YAML.
385
+ command_project: 'str | None' = None
384
386
 
385
387
 
386
388
  def _optional_string(value: 'Any') -> 'str | None':
@@ -280,10 +280,28 @@ def resolve_odps_settings(
280
280
  suppressed_env_vars.append(field)
281
281
  else:
282
282
  for field, env_value in env_settings.items():
283
+ if (field == "region_name" and settings.get(field)
284
+ and os.environ.get("MAXC_CLI_NAME") == "aliyun maxc"):
285
+ # The wrapper's profile region is a default, not a request to
286
+ # reroute the Catalog project selected in connection setup.
287
+ continue
283
288
  if env_value:
284
289
  settings[field] = env_value
285
290
  sources[field] = "environment"
286
291
 
292
+ if auth_override is None and getattr(config, "command_project", None):
293
+ settings["project"] = config.command_project
294
+ sources["project"] = "command_line"
295
+
296
+ # The wrapper passes the selected Alibaba Cloud profile's region. Derive
297
+ # the public data-plane endpoint only when no endpoint was selected.
298
+ if not settings.get("endpoint") and settings.get("region_name"):
299
+ from .catalog_bootstrap import region_to_endpoint
300
+
301
+ settings["endpoint"] = region_to_endpoint(settings["region_name"])
302
+ if settings["endpoint"]:
303
+ sources["endpoint"] = sources.get("region_name", "config_file")
304
+
287
305
  return settings, sources, suppressed_env_vars
288
306
 
289
307
 
@@ -533,10 +533,17 @@ def exchange_sts(
533
533
  "securityToken",
534
534
  "expiration",
535
535
  )
536
+ # Match aliyun-cli's Go JSON decoder: prefer the exact tagged name, then
537
+ # accept a case-insensitive match (the service also returns PascalCase).
538
+ folded_payload = {key.lower(): value for key, value in payload.items()}
539
+ credentials = {
540
+ field: payload.get(field, folded_payload.get(field.lower()))
541
+ for field in required_fields
542
+ }
536
543
  missing = [
537
544
  field
538
545
  for field in required_fields
539
- if not isinstance(payload.get(field), str) or not payload.get(field)
546
+ if not isinstance(credentials[field], str) or not credentials[field]
540
547
  ]
541
548
  if missing:
542
549
  raise OAuthError(
@@ -545,10 +552,10 @@ def exchange_sts(
545
552
  + "."
546
553
  )
547
554
  return StsCredential(
548
- access_key_id=payload["accessKeyId"],
549
- access_key_secret=payload["accessKeySecret"],
550
- security_token=payload["securityToken"],
551
- expiration_iso=payload["expiration"],
555
+ access_key_id=credentials["accessKeyId"],
556
+ access_key_secret=credentials["accessKeySecret"],
557
+ security_token=credentials["securityToken"],
558
+ expiration_iso=credentials["expiration"],
552
559
  )
553
560
 
554
561
 
@@ -114,6 +114,28 @@ secret in the response.
114
114
 
115
115
  ## Project And Endpoint
116
116
 
117
+ A saved default project is optional. With an existing identity, pass the
118
+ intended project on each data command, for example:
119
+
120
+ ```bash
121
+ {{cli}} query "SELECT 1" --project <project> --json
122
+ ```
123
+
124
+ If neither `--project` nor a configured/environment default exists, the command
125
+ returns `VALIDATION_ERROR` with a retry template containing `--project`. It
126
+ never opens a project picker or changes the connection automatically. Retain
127
+ the original Alibaba Cloud `--profile` on recovery commands.
128
+
129
+ To configure a default explicitly, run `{{cli}} auth login --reuse-auth`. This
130
+ reuses the Catalog picker and saves connection fields only. The JSON variant
131
+ returns project actions without prompting or starting new OAuth authorization;
132
+ a wrapper profile continues to own and refresh its credentials.
133
+
134
+ A known profile Region supplies the default public data-plane endpoint; a
135
+ Catalog project selected during explicit setup supplies its actual Region.
136
+ Explicit `--odps-endpoint` on `auth login` overrides the endpoint and works
137
+ through the wrapper. Unknown regions require an endpoint from the user.
138
+
117
139
  - Use values supplied by the profile, Catalog result, current configuration, or
118
140
  the user.
119
141
  - Never invent an endpoint or derive a related project from a name suffix.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxc-cli
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: Agent-native MaxCompute CLI for external coding agents
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.9
@@ -86,6 +86,7 @@ tests/test_cli_mock.py
86
86
  tests/test_cli_query_parse_and_sanitize.py
87
87
  tests/test_compat.py
88
88
  tests/test_config_atomic_write.py
89
+ tests/test_connection_bootstrap.py
89
90
  tests/test_e2e_smoke.py
90
91
  tests/test_effective_hints_contract.py
91
92
  tests/test_enterprise_tls.py