maxc-cli 0.7.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.7.0 → maxc_cli-0.8.1}/PKG-INFO +16 -1
  2. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/README.md +15 -0
  3. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/_samples.py +8 -3
  5. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/app.py +114 -12
  6. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_providers.py +19 -3
  7. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/job.py +66 -1
  8. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/cli.py +106 -8
  9. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/config.py +2 -0
  10. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/enterprise_tls.py +44 -22
  11. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/helpers.py +18 -0
  12. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/oauth.py +12 -5
  13. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/SKILL.md +3 -0
  14. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-auth.md +22 -0
  15. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/command-patterns.md +27 -0
  16. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/PKG-INFO +16 -1
  17. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/SOURCES.txt +2 -0
  18. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands_context.py +25 -23
  19. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cache.py +30 -16
  20. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_mock.py +3 -0
  21. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_config_atomic_write.py +2 -0
  22. maxc_cli-0.8.1/tests/test_connection_bootstrap.py +279 -0
  23. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_enterprise_tls.py +109 -1
  24. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_external_auth.py +4 -4
  25. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_installer_contracts.py +5 -0
  26. maxc_cli-0.8.1/tests/test_job_inspection.py +160 -0
  27. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_job_store_durability.py +9 -0
  28. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_oauth.py +41 -5
  29. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_output_format_contract.py +2 -1
  30. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_management.py +1 -1
  31. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/MANIFEST.in +0 -0
  32. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/pyproject.toml +0 -0
  33. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/scripts/pyinstaller_entry.py +0 -0
  34. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/scripts/regression_test.py +0 -0
  35. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/setup.cfg +0 -0
  36. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/setup.py +0 -0
  37. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/__main__.py +0 -0
  38. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/agent_platforms.py +0 -0
  39. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/audit.py +0 -0
  40. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/auth_continuation.py +0 -0
  41. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/__init__.py +0 -0
  42. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/auth.py +0 -0
  43. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/catalog.py +0 -0
  44. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/data.py +0 -0
  45. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/mcp.py +0 -0
  46. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/meta.py +0 -0
  47. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/odps.py +0 -0
  48. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/query.py +0 -0
  49. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/backend/semantic.py +0 -0
  50. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/cache.py +0 -0
  51. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
  52. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/exceptions.py +0 -0
  53. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/help_format.py +0 -0
  54. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/job_ids.py +0 -0
  55. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/masking.py +0 -0
  56. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/mcp_serve.py +0 -0
  57. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/models.py +0 -0
  58. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/odps_runtime.py +0 -0
  59. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/output.py +0 -0
  60. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/proxy_auth.py +0 -0
  61. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic.py +0 -0
  62. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/semantic_management.py +0 -0
  63. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/setting_parser.py +0 -0
  64. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  65. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  66. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  67. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  68. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  69. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  70. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/red-lines.md +0 -0
  71. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  72. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/setup-install.md +0 -0
  73. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  74. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  75. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  76. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/state_permissions.py +0 -0
  77. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/store.py +0 -0
  78. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli/utils.py +0 -0
  79. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  80. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  81. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/requires.txt +0 -0
  82. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
  83. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_hints_and_cli.py +0 -0
  84. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_platforms.py +0 -0
  85. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_agent_skill_commands.py +0 -0
  86. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_ai_native_contract_regressions.py +0 -0
  87. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_auth_logout.py +0 -0
  88. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_auth.py +0 -0
  89. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_data.py +0 -0
  90. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_data_serialization.py +0 -0
  91. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_mcp.py +0 -0
  92. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_backend_meta.py +0 -0
  93. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_build_release_archive_compat.py +0 -0
  94. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_build_release_script.py +0 -0
  95. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_catalog.py +0 -0
  96. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_catalog_bootstrap.py +0 -0
  97. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_arg_validation.py +0 -0
  98. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  99. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_compat.py +0 -0
  100. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_e2e_smoke.py +0 -0
  101. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_effective_hints_contract.py +0 -0
  102. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_envelope_shape.py +0 -0
  103. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_error_self_correction.py +0 -0
  104. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_error_translation.py +0 -0
  105. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_exit_codes.py +0 -0
  106. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_flag_hoist.py +0 -0
  107. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_help_format.py +0 -0
  108. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_help_version_e2e.py +0 -0
  109. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_helpers.py +0 -0
  110. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_helpers_csv.py +0 -0
  111. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_integration.py +0 -0
  112. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_integration_real.py +0 -0
  113. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_job_improvements.py +0 -0
  114. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_kb_commands.py +0 -0
  115. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_manifest_runtime_contract.py +0 -0
  116. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_masking.py +0 -0
  117. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_mcp_serve.py +0 -0
  118. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_meta_schema_and_partition_cols.py +0 -0
  119. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_odps_runtime.py +0 -0
  120. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_output_action_safety.py +0 -0
  121. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_packaging_metadata.py +0 -0
  122. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_phase1_improvements.py +0 -0
  123. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_proxy_auth.py +0 -0
  124. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_pyinstaller_bundle.py +0 -0
  125. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_python39_compat.py +0 -0
  126. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_query_auto_promote.py +0 -0
  127. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_query_result_csv_fallback.py +0 -0
  128. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_scope.py +0 -0
  129. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_semantic_transport.py +0 -0
  130. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_setting_parser.py +0 -0
  131. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_cli_consistency.py +0 -0
  132. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_eval.py +0 -0
  133. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_skill_renderer.py +0 -0
  134. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_startup_imports.py +0 -0
  135. {maxc_cli-0.7.0 → maxc_cli-0.8.1}/tests/test_state_permissions.py +0 -0
  136. {maxc_cli-0.7.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.7.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
@@ -311,3 +311,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
311
311
  write must be reconciled before retrying. Publication checks are not SQL or
312
312
  business validation. History lists the latest 20 summaries, and an immutable
313
313
  revision read does not guarantee historical DataBridge analysis.
314
+
315
+ ### Task metrics and worker logs
316
+
317
+ ```bash
318
+ aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
319
+ aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
320
+ aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
321
+ aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
322
+ ```
323
+
324
+ These read-only commands use the same project and saved job context as `job status`.
325
+ Task detail preserves the service payload (including `mapReduce.jsonSummary`).
326
+ Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
327
+ envelope. The requested size defaults to 1 MiB and must be positive; returned logs
328
+ may be partial. Use `--project` to select the owning project explicitly.
@@ -289,3 +289,18 @@ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
289
289
  write must be reconciled before retrying. Publication checks are not SQL or
290
290
  business validation. History lists the latest 20 summaries, and an immutable
291
291
  revision read does not guarantee historical DataBridge analysis.
292
+
293
+ ### Task metrics and worker logs
294
+
295
+ ```bash
296
+ aliyun maxc job task-detail <instance_id> --task-name AnonymousSQLTask --json
297
+ aliyun maxc job task-summary <instance_id> --task-name AnonymousSQLTask --json
298
+ aliyun maxc job workers <instance_id> --task-name AnonymousSQLTask --json
299
+ aliyun maxc job worker-log <instance_id> <log_id> --log-type stdout --size 1048576 --json
300
+ ```
301
+
302
+ These read-only commands use the same project and saved job context as `job status`.
303
+ Task detail preserves the service payload (including `mapReduce.jsonSummary`).
304
+ Select `log_id` from `data.workers`; logs are returned as `data.content` in the JSON
305
+ envelope. The requested size defaults to 1 MiB and must be positive; returned logs
306
+ may be partial. Use `--project` to select the owning project explicitly.
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.7.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",
@@ -59,6 +60,10 @@ SAMPLES: dict[str, str] = {
59
60
  "maxc job wait <job_id>\n"
60
61
  "maxc job wait <job_id> --timeout 600 --stream"
61
62
  ),
63
+ "job.task-detail": "maxc job task-detail <job_id> --task-name AnonymousSQLTask --json",
64
+ "job.task-summary": "maxc job task-summary <job_id> --task-name AnonymousSQLTask --json",
65
+ "job.workers": "maxc job workers <job_id> --task-name AnonymousSQLTask --json",
66
+ "job.worker-log": "maxc job worker-log <job_id> <log_id> --log-type stdout --size 1048576 --json",
62
67
  "job.diagnose": "maxc job diagnose <job_id>\nmaxc job diagnose <job_id> --json",
63
68
  "job.result": (
64
69
  "maxc job result <job_id>\n"
@@ -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
@@ -1811,6 +1818,30 @@ class MaxCApp:
1811
1818
  self.log("job.cancel", envelope.status, envelope.metadata)
1812
1819
  return envelope
1813
1820
 
1821
+ def job_inspect(
1822
+ self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
1823
+ task_name: 'str | None' = None, log_id: 'str | None' = None,
1824
+ log_type: 'str' = "stdout", size: 'int' = 1048576,
1825
+ ) -> 'Envelope':
1826
+ if not self.remote_jobs:
1827
+ raise FeatureUnavailableError("Task diagnostics require a MaxCompute backend.")
1828
+ resolved = self._resolve_remote_job_id(job_id, project=project)
1829
+ payload = self.backend.inspect_job(
1830
+ resolved.instance_id, section=section, project=resolved.project,
1831
+ session_context=resolved.session_context, task_name=task_name,
1832
+ log_id=log_id, log_type=log_type, size=size,
1833
+ )
1834
+ envelope = Envelope(
1835
+ command=f"job.{section}", status="success",
1836
+ data={"job_id": resolved.external_job_id, **payload},
1837
+ metadata={"project": resolved.project, "job_id": resolved.external_job_id},
1838
+ agent_hints=AgentHints(warnings=[
1839
+ "The service may return only part of the log; requested_size does not prove completeness."
1840
+ ] if section == "worker-log" else []),
1841
+ )
1842
+ self.log(envelope.command, envelope.status, envelope.metadata)
1843
+ return envelope
1844
+
1814
1845
  def job_diagnose(self, job_id: 'str', *, project: 'str | None' = None) -> 'Envelope':
1815
1846
  if self.remote_jobs:
1816
1847
  resolved = self._resolve_remote_job_id(job_id, project=project)
@@ -4684,6 +4715,7 @@ class MaxCApp:
4684
4715
  projects: 'list[dict[str, Any]]',
4685
4716
  *,
4686
4717
  title_prefix: 'str',
4718
+ endpoint_flag: 'str' = "--endpoint",
4687
4719
  ) -> 'list[SuggestedAction]':
4688
4720
  actions: list[SuggestedAction] = []
4689
4721
  for project_data in projects:
@@ -4693,7 +4725,7 @@ class MaxCApp:
4693
4725
  tokens = [*base_tokens, "--project", project_id]
4694
4726
  endpoint_value = project_data.get("endpoint")
4695
4727
  endpoint_placeholder = endpoint_value is None
4696
- tokens.extend(["--endpoint", str(endpoint_value or "__MAXC_ENDPOINT__")])
4728
+ tokens.extend([endpoint_flag, str(endpoint_value or "__MAXC_ENDPOINT__")])
4697
4729
  if project_data.get("region"):
4698
4730
  tokens.extend(["--region", str(project_data["region"])])
4699
4731
  if project_data.get("tunnel_endpoint"):
@@ -4731,6 +4763,8 @@ class MaxCApp:
4731
4763
  region_name: 'str | None' = None,
4732
4764
  tunnel_endpoint: 'str | None' = None,
4733
4765
  from_env: 'bool' = False,
4766
+ reuse_auth: 'bool' = False,
4767
+ allow_interactive_picker: 'bool' = True,
4734
4768
  no_validate: 'bool' = False,
4735
4769
  target_config_path: 'Path | None' = None,
4736
4770
  catalog_endpoint: 'str | None' = None,
@@ -4744,8 +4778,39 @@ class MaxCApp:
4744
4778
  existing_auth = AuthConfig.from_mapping(existing_payload.get("auth", {}) or {})
4745
4779
  env_settings = load_odps_env()
4746
4780
 
4747
- # Resolve credentials first — the picker needs the AK/secret/STS in hand.
4748
- 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:
4749
4814
  if any((access_id, secret_access_key, security_token)) or from_env:
4750
4815
  raise ValidationError(
4751
4816
  "Do not combine an auth continuation with credential flags or --from-env.",
@@ -4824,6 +4889,7 @@ class MaxCApp:
4824
4889
  env_settings=env_settings,
4825
4890
  existing_auth=existing_auth,
4826
4891
  reselect=reselect,
4892
+ allow_interactive=allow_interactive_picker,
4827
4893
  )
4828
4894
  )
4829
4895
  except ProjectPickerPending as exc:
@@ -4871,6 +4937,18 @@ class MaxCApp:
4871
4937
  warnings=[],
4872
4938
  ),
4873
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
4874
4952
  if _oauth_config is not None:
4875
4953
  return pending_envelope
4876
4954
  if continuation_id:
@@ -4910,6 +4988,15 @@ class MaxCApp:
4910
4988
  pending_envelope.metadata["continuation_expires_at_unix"] = expires_at
4911
4989
  return pending_envelope
4912
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
+
4913
5000
  resolved_auth = AuthConfig(
4914
5001
  access_id=resolved_access_id,
4915
5002
  secret_access_key=resolved_secret,
@@ -4959,10 +5046,25 @@ class MaxCApp:
4959
5046
  persisted_auth.provider = "oauth"
4960
5047
  persisted_auth.oauth = _oauth_config
4961
5048
  migrate_legacy_session_override(target_path)
4962
- persist_login_config(
4963
- target_path,
4964
- auth=persisted_auth,
4965
- )
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)
4966
5068
 
4967
5069
  continuation_cleanup_warning: str | None = None
4968
5070
  if continuation_id:
@@ -4983,7 +5085,7 @@ class MaxCApp:
4983
5085
  # Always remind callers that AK/SK is stored in plaintext YAML (chmod
4984
5086
  # 0600) — flagged in CLAUDE.md as a known limitation. Skip for STS
4985
5087
  # tokens since those are short-lived and self-expiring.
4986
- if not resolved_auth.security_token:
5088
+ if not reuse_auth and not resolved_auth.security_token:
4987
5089
  warnings.append(
4988
5090
  f"AccessKey saved in plaintext at `{target_path}` (file mode 0600). "
4989
5091
  f"For shared/CI environments prefer `auth login-external` with a credential helper, "
@@ -4993,7 +5095,7 @@ class MaxCApp:
4993
5095
  warnings.append(
4994
5096
  "Credentials were imported from environment variables (--from-env) and saved to config."
4995
5097
  )
4996
- elif any(
5098
+ elif not reuse_auth and any(
4997
5099
  env_settings.get(name)
4998
5100
  for name in ("access_id", "secret_access_key", "security_token", "endpoint", "region_name", "tunnel_endpoint")
4999
5101
  ):
@@ -5031,8 +5133,8 @@ class MaxCApp:
5031
5133
 
5032
5134
  login_metadata = {
5033
5135
  "config_path": str(target_path),
5034
- "written_fields": sorted(persisted_auth.to_mapping().keys()),
5035
- "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",
5036
5138
  }
5037
5139
  envelope = Envelope(
5038
5140
  command="auth.login",
@@ -5726,7 +5828,7 @@ class MaxCApp:
5726
5828
  from . import catalog_bootstrap as _catalog_bootstrap
5727
5829
 
5728
5830
  # 2. Picker not viable (non-TTY or --no-picker).
5729
- if inputs.no_picker or not sys.stdin.isatty():
5831
+ if inputs.no_picker or not inputs.allow_interactive or not sys.stdin.isatty():
5730
5832
  # Non-TTY + picker not disabled: list projects for structured output
5731
5833
  catalog_warning: str | None = None
5732
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",
@@ -5,7 +5,7 @@ from itertools import islice
5
5
  from time import monotonic, sleep
6
6
  from typing import Any
7
7
 
8
- from ..exceptions import BackendConnectionError, JobTimeoutError, ValidationError
8
+ from ..exceptions import BackendConnectionError, JobTimeoutError, MaxCError, ValidationError
9
9
  from ..helpers import (
10
10
  OdpsNoSuchObject,
11
11
  _dt_to_iso,
@@ -301,6 +301,71 @@ class JobMixin(QueryMixin):
301
301
  "task_results": task_results,
302
302
  }
303
303
 
304
+ def inspect_job(
305
+ self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
306
+ task_name: 'str | None' = None, log_id: 'str | None' = None,
307
+ log_type: 'str' = "stdout", size: 'int' = 1048576,
308
+ session_context: 'dict[str, Any] | None' = None,
309
+ ) -> 'dict[str, Any]':
310
+ """Read task diagnostics without hiding service errors or raw metrics.
311
+
312
+ Args:
313
+ job_id: Instance ID resolved by the application.
314
+ section: task-detail, task-summary, workers, or worker-log.
315
+ project: Project owning the instance.
316
+ task_name: Explicit task name, or SDK single-task selection.
317
+ log_id: Worker log ID returned by worker discovery.
318
+ log_type: Supported PyODPS worker log type.
319
+ size: Positive requested log size in bytes.
320
+ session_context: Saved SQLRT routing context.
321
+ """
322
+ if section not in {"task-detail", "task-summary", "workers", "worker-log"}:
323
+ raise ValidationError("Unknown job inspection section.")
324
+ if section == "worker-log":
325
+ from odps.models.worker import LOG_TYPES_MAPPING
326
+
327
+ if not log_id or not log_id.strip():
328
+ raise ValidationError("A non-empty worker log ID is required.")
329
+ if log_type not in LOG_TYPES_MAPPING or not isinstance(size, int) or size <= 0:
330
+ raise ValidationError("Choose a supported log type and a positive log size.")
331
+ instance = self._get_instance(job_id, project=project, session_context=session_context)
332
+ try:
333
+ if section == "worker-log":
334
+ return {"log_id": log_id, "log_type": log_type, "requested_size": size,
335
+ "content": instance.get_worker_log(log_id, log_type, size=size)}
336
+ if section == "task-summary":
337
+ if self._raw_attr(instance, "_subquery_id") is not None:
338
+ from ..exceptions import FeatureUnavailableError
339
+
340
+ raise FeatureUnavailableError(
341
+ "Task summaries are not scoped to SQLRT subqueries by PyODPS.",
342
+ suggestion="Use `job task-detail` for subquery-scoped metrics.",
343
+ )
344
+ summary = instance.get_task_summary(task_name)
345
+ return {"task_name": task_name, "available": summary is not None,
346
+ "summary": dict(summary) if summary is not None else None,
347
+ "summary_text": getattr(summary, "summary_text", None)}
348
+ detail = instance.get_task_detail2(task_name)
349
+ if section == "task-detail":
350
+ return {"task_name": task_name, "detail": detail}
351
+ if not isinstance(detail, (dict, list)):
352
+ from ..exceptions import FeatureUnavailableError
353
+
354
+ raise FeatureUnavailableError(
355
+ "Task detail is not structured JSON; worker discovery is unavailable.",
356
+ suggestion="Read `job task-detail` to inspect the returned detail.",
357
+ )
358
+ workers = instance.get_task_workers(task_name, json_obj=detail)
359
+ fields = ("id", "log_id", "type", "status", "start_time", "end_time",
360
+ "input_bytes", "input_records", "output_bytes", "output_records")
361
+ return {"task_name": task_name, "workers": [
362
+ {field: getattr(worker, field, None) for field in fields} for worker in workers
363
+ ]}
364
+ except MaxCError:
365
+ raise
366
+ except Exception as exc:
367
+ raise translate_odps_error(exc, context="job") from exc
368
+
304
369
  def list_jobs(
305
370
  self, *, project: 'str | None' = None, limit: 'int' = 20
306
371
  ) -> 'tuple[list[JobInfo], bool]':
@@ -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
 
@@ -381,6 +381,24 @@ def build_parser() -> argparse.ArgumentParser:
381
381
  job_diagnose.add_argument("--json", action="store_true", help="Output as JSON envelope")
382
382
  job_diagnose.set_defaults(handler=_handle_job_diagnose)
383
383
 
384
+ for name, description in (
385
+ ("task-detail", "Read raw task detail v2, including operator metrics"),
386
+ ("task-summary", "Read the service task summary"),
387
+ ("workers", "List task workers and their log IDs"),
388
+ ("worker-log", "Read one worker log with a bounded requested size"),
389
+ ):
390
+ inspection = _make_parser(job_subparsers, name, f"job.{name}", help=description)
391
+ inspection.add_argument("job_id", help="Job or instance ID")
392
+ inspection.add_argument("--project", help="Project owning the job (uses stored context when omitted)")
393
+ inspection.add_argument("--json", action="store_true", help="Output as JSON envelope")
394
+ if name == "worker-log":
395
+ inspection.add_argument("log_id", help="Worker log_id returned by job workers")
396
+ inspection.add_argument("--log-type", choices=("stdout", "stderr", "waterfall_summary", "jstack", "pstack", "hs_err_log", "coreinfo"), default="stdout")
397
+ inspection.add_argument("--size", type=positive_int, default=1048576, help="Requested log size in bytes (default: 1048576; must be positive)")
398
+ else:
399
+ inspection.add_argument("--task-name", help="Task name; omit only for a single-task instance")
400
+ inspection.set_defaults(handler=_handle_job_inspect, inspection_section=name)
401
+
384
402
  job_result = _make_parser(job_subparsers, "result", "job.result", help="Fetch job results")
385
403
  job_result.add_argument("job_id", help="Job ID returned by submit")
386
404
  job_result.add_argument("--project", help="Project that owns the job (uses stored submission context when omitted)")
@@ -768,7 +786,11 @@ def build_parser() -> argparse.ArgumentParser:
768
786
  "--project",
769
787
  help="Target MaxCompute project (omit to pop an interactive picker over the catalog)",
770
788
  )
771
- 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
+ )
772
794
  auth_login.add_argument("--region", dest="region_name", help="MaxCompute region name")
773
795
  auth_login.add_argument("--tunnel-endpoint", help="Tunnel endpoint URL for data transfer")
774
796
  auth_login.add_argument(
@@ -1610,11 +1632,14 @@ def run(
1610
1632
  app: MaxCApp | None = None
1611
1633
  try:
1612
1634
  _validate_output_request(args, command_name)
1613
- app = app_type(
1614
- cwd=working_dir,
1615
- config_path=config_path,
1616
- load_backend=_should_load_backend(command_name),
1617
- )
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)
1618
1643
  args.handler(app, args, stdout)
1619
1644
  return getattr(args, "_envelope_exit_code", 0)
1620
1645
  except MaxCError as exc:
@@ -1684,6 +1709,40 @@ def run(
1684
1709
  ),
1685
1710
  }
1686
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])
1687
1746
  # Build schema context for SQL errors to enable agent self-correction
1688
1747
  if app is not None and exc.error_code in (
1689
1748
  "SQL_ERROR", "NOT_FOUND", "SCHEMA_NOT_FOUND", "TABLE_NOT_FOUND", "COLUMN_NOT_FOUND",
@@ -1707,6 +1766,10 @@ def run(
1707
1766
  error=exc.to_payload(),
1708
1767
  agent_hints=_hints,
1709
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
+ ]
1710
1773
  if _is_job_command(args):
1711
1774
  _prepare_job_failure_envelope(payload, args)
1712
1775
  if _is_job_wait_stream(args):
@@ -1992,6 +2055,15 @@ def _prepare_job_failure_envelope(
1992
2055
  envelope.agent_hints = AgentHints(actions=actions, warnings=warnings)
1993
2056
 
1994
2057
 
2058
+ def _handle_job_inspect(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
2059
+ envelope = app.job_inspect(
2060
+ args.job_id, section=args.inspection_section, project=args.project,
2061
+ task_name=getattr(args, "task_name", None), log_id=getattr(args, "log_id", None),
2062
+ log_type=getattr(args, "log_type", "stdout"), size=getattr(args, "size", 1048576),
2063
+ )
2064
+ _emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
2065
+
2066
+
1995
2067
  def _handle_job_diagnose(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -> None:
1996
2068
  envelope = app.job_diagnose(args.job_id, project=args.project)
1997
2069
  _emit_envelope(envelope, args=args, stdout=stdout, default_format="json")
@@ -2413,6 +2485,14 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
2413
2485
  args.security_token,
2414
2486
  )
2415
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
+ )
2416
2496
  if args.oauth:
2417
2497
  if args.from_env or explicit_credentials:
2418
2498
  raise ValidationError(
@@ -2479,6 +2559,8 @@ def _handle_auth_login(app: MaxCApp, args: argparse.Namespace, stdout: TextIO) -
2479
2559
  region_name=args.region_name,
2480
2560
  tunnel_endpoint=args.tunnel_endpoint,
2481
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),
2482
2564
  no_validate=args.no_validate,
2483
2565
  target_config_path=args.requested_config_path,
2484
2566
  catalog_endpoint=args.catalog_endpoint,
@@ -2588,6 +2670,10 @@ _MANIFEST_CONDITIONAL_NETWORK_COMMANDS = frozenset({
2588
2670
  _MANIFEST_JOB_FOLLOWUP_COMMANDS = frozenset({
2589
2671
  "job.cancel",
2590
2672
  "job.diagnose",
2673
+ "job.task-detail",
2674
+ "job.task-summary",
2675
+ "job.workers",
2676
+ "job.worker-log",
2591
2677
  "job.result",
2592
2678
  "job.status",
2593
2679
  "job.wait",
@@ -2690,6 +2776,14 @@ def _manifest_requirements(command: str) -> dict[str, Any]:
2690
2776
  "mode": "required",
2691
2777
  "reason": "A new OAuth authorization and STS exchange use network APIs.",
2692
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
+ },
2693
2787
  {
2694
2788
  "when": _manifest_condition("no_validate", equals=False),
2695
2789
  "mode": "required",
@@ -2937,12 +3031,16 @@ def _manifest_effects(command: str) -> list[dict[str, Any]]:
2937
3031
  "create_or_replace",
2938
3032
  "auth_config",
2939
3033
  when={"runtime": "login_succeeds"},
3034
+ note="--reuse-auth preserves the existing identity and writes connection fields only.",
2940
3035
  ),
2941
3036
  _manifest_effect(
2942
3037
  "local",
2943
3038
  "create",
2944
3039
  "owner_only_access_key_continuation",
2945
- 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
+ ]},
2946
3044
  ),
2947
3045
  _manifest_effect(
2948
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':