maxc-cli 0.6.1__tar.gz → 0.8.0__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 (135) hide show
  1. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/PKG-INFO +16 -1
  2. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/README.md +15 -0
  3. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/_samples.py +20 -0
  5. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/app.py +309 -0
  6. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/auth_providers.py +4 -0
  7. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/job.py +66 -1
  8. maxc_cli-0.8.0/src/maxc_cli/backend/mcp.py +320 -0
  9. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/cli.py +285 -0
  10. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/config.py +45 -0
  11. maxc_cli-0.8.0/src/maxc_cli/enterprise_tls.py +190 -0
  12. maxc_cli-0.8.0/src/maxc_cli/mcp_serve.py +264 -0
  13. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/models.py +16 -1
  14. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/oauth.py +25 -2
  15. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/output.py +37 -0
  16. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/SKILL.md +40 -0
  17. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/command-patterns.md +29 -0
  18. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/PKG-INFO +16 -1
  19. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/SOURCES.txt +8 -0
  20. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_skill_commands_context.py +25 -23
  21. maxc_cli-0.8.0/tests/test_backend_mcp.py +419 -0
  22. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_build_release_archive_compat.py +38 -0
  23. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cache.py +30 -16
  24. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_mock.py +3 -0
  25. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_config_atomic_write.py +2 -0
  26. maxc_cli-0.8.0/tests/test_enterprise_tls.py +247 -0
  27. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_external_auth.py +4 -4
  28. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_installer_contracts.py +135 -8
  29. maxc_cli-0.8.0/tests/test_job_inspection.py +160 -0
  30. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_job_store_durability.py +9 -0
  31. maxc_cli-0.8.0/tests/test_kb_commands.py +497 -0
  32. maxc_cli-0.8.0/tests/test_mcp_serve.py +352 -0
  33. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_oauth.py +113 -1
  34. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_output_format_contract.py +2 -1
  35. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_management.py +1 -1
  36. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/MANIFEST.in +0 -0
  37. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/pyproject.toml +0 -0
  38. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/scripts/pyinstaller_entry.py +0 -0
  39. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/scripts/regression_test.py +0 -0
  40. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/setup.cfg +0 -0
  41. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/setup.py +0 -0
  42. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/__main__.py +0 -0
  43. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/agent_platforms.py +0 -0
  44. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/audit.py +0 -0
  45. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/auth_continuation.py +0 -0
  46. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/__init__.py +0 -0
  47. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/auth.py +0 -0
  48. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/catalog.py +0 -0
  49. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/data.py +0 -0
  50. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/meta.py +0 -0
  51. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/odps.py +0 -0
  52. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/query.py +0 -0
  53. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/backend/semantic.py +0 -0
  54. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/cache.py +0 -0
  55. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/catalog_bootstrap.py +0 -0
  56. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/exceptions.py +0 -0
  57. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/help_format.py +0 -0
  58. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/helpers.py +0 -0
  59. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/job_ids.py +0 -0
  60. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/masking.py +0 -0
  61. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/odps_runtime.py +0 -0
  62. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/proxy_auth.py +0 -0
  63. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/semantic.py +0 -0
  64. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/semantic_management.py +0 -0
  65. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/setting_parser.py +0 -0
  66. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  67. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/bootstrap-auth.md +0 -0
  68. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/bootstrap-flow.md +0 -0
  69. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/json-output-format.md +0 -0
  70. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  71. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +0 -0
  72. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  73. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/red-lines.md +0 -0
  74. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/semantic-packages.md +0 -0
  75. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/setup-install.md +0 -0
  76. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/sql-common-errors.md +0 -0
  77. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  78. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  79. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/state_permissions.py +0 -0
  80. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/store.py +0 -0
  81. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli/utils.py +0 -0
  82. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  83. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  84. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/requires.txt +0 -0
  85. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/src/maxc_cli.egg-info/top_level.txt +0 -0
  86. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_hints_and_cli.py +0 -0
  87. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_platforms.py +0 -0
  88. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_agent_skill_commands.py +0 -0
  89. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_ai_native_contract_regressions.py +0 -0
  90. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_auth_logout.py +0 -0
  91. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_auth.py +0 -0
  92. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_data.py +0 -0
  93. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_data_serialization.py +0 -0
  94. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_backend_meta.py +0 -0
  95. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_build_release_script.py +0 -0
  96. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_catalog.py +0 -0
  97. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_catalog_bootstrap.py +0 -0
  98. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_arg_validation.py +0 -0
  99. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  100. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_compat.py +0 -0
  101. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_e2e_smoke.py +0 -0
  102. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_effective_hints_contract.py +0 -0
  103. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_envelope_shape.py +0 -0
  104. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_error_self_correction.py +0 -0
  105. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_error_translation.py +0 -0
  106. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_exit_codes.py +0 -0
  107. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_flag_hoist.py +0 -0
  108. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_help_format.py +0 -0
  109. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_help_version_e2e.py +0 -0
  110. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_helpers.py +0 -0
  111. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_helpers_csv.py +0 -0
  112. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_integration.py +0 -0
  113. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_integration_real.py +0 -0
  114. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_job_improvements.py +0 -0
  115. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_manifest_runtime_contract.py +0 -0
  116. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_masking.py +0 -0
  117. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_meta_schema_and_partition_cols.py +0 -0
  118. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_odps_runtime.py +0 -0
  119. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_output_action_safety.py +0 -0
  120. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_packaging_metadata.py +0 -0
  121. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_phase1_improvements.py +0 -0
  122. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_proxy_auth.py +0 -0
  123. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_pyinstaller_bundle.py +0 -0
  124. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_python39_compat.py +0 -0
  125. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_query_auto_promote.py +0 -0
  126. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_query_result_csv_fallback.py +0 -0
  127. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_scope.py +0 -0
  128. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_semantic_transport.py +0 -0
  129. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_setting_parser.py +0 -0
  130. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_cli_consistency.py +0 -0
  131. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_eval.py +0 -0
  132. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_skill_renderer.py +0 -0
  133. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_startup_imports.py +0 -0
  134. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/tests/test_state_permissions.py +0 -0
  135. {maxc_cli-0.6.1 → maxc_cli-0.8.0}/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.6.1
3
+ Version: 0.8.0
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.6.1"
5
+ __version__ = "0.8.0"
@@ -59,6 +59,10 @@ SAMPLES: dict[str, str] = {
59
59
  "maxc job wait <job_id>\n"
60
60
  "maxc job wait <job_id> --timeout 600 --stream"
61
61
  ),
62
+ "job.task-detail": "maxc job task-detail <job_id> --task-name AnonymousSQLTask --json",
63
+ "job.task-summary": "maxc job task-summary <job_id> --task-name AnonymousSQLTask --json",
64
+ "job.workers": "maxc job workers <job_id> --task-name AnonymousSQLTask --json",
65
+ "job.worker-log": "maxc job worker-log <job_id> <log_id> --log-type stdout --size 1048576 --json",
62
66
  "job.diagnose": "maxc job diagnose <job_id>\nmaxc job diagnose <job_id> --json",
63
67
  "job.result": (
64
68
  "maxc job result <job_id>\n"
@@ -84,6 +88,22 @@ SAMPLES: dict[str, str] = {
84
88
  "maxc meta search orders\n"
85
89
  "maxc meta search user --project my_proj --json"
86
90
  ),
91
+ # ── kb ─────────────────────────────────────────────────────────────────
92
+ "kb": "maxc kb ask \"How do I set a split size hint?\" --json\nmaxc kb search \"dynamic filter\" --json",
93
+ "kb.ask": (
94
+ "maxc kb ask \"How do I set a split size hint?\"\n"
95
+ "maxc kb ask \"What does ODPS-0123144 mean?\" --max-docs 3 --json"
96
+ ),
97
+ "kb.search": (
98
+ "maxc kb search \"clustered table bucket\"\n"
99
+ "maxc kb search \"dynamic filter\" --limit 5 --context-lines 3 --json"
100
+ ),
101
+ # ── mcp ────────────────────────────────────────────────────────────────
102
+ "mcp": 'maxc mcp serve # point an MCP client at {"command": "maxc", "args": ["mcp", "serve"]}',
103
+ "mcp.serve": (
104
+ 'maxc mcp serve\n'
105
+ '# MCP client config: {"command": "maxc", "args": ["mcp", "serve"]}'
106
+ ),
87
107
  "meta.search-columns": (
88
108
  "maxc meta search-columns user_id\n"
89
109
  "maxc meta search-columns dt --project my_proj --json"
@@ -1811,6 +1811,30 @@ class MaxCApp:
1811
1811
  self.log("job.cancel", envelope.status, envelope.metadata)
1812
1812
  return envelope
1813
1813
 
1814
+ def job_inspect(
1815
+ self, job_id: 'str', *, section: 'str', project: 'str | None' = None,
1816
+ task_name: 'str | None' = None, log_id: 'str | None' = None,
1817
+ log_type: 'str' = "stdout", size: 'int' = 1048576,
1818
+ ) -> 'Envelope':
1819
+ if not self.remote_jobs:
1820
+ raise FeatureUnavailableError("Task diagnostics require a MaxCompute backend.")
1821
+ resolved = self._resolve_remote_job_id(job_id, project=project)
1822
+ payload = self.backend.inspect_job(
1823
+ resolved.instance_id, section=section, project=resolved.project,
1824
+ session_context=resolved.session_context, task_name=task_name,
1825
+ log_id=log_id, log_type=log_type, size=size,
1826
+ )
1827
+ envelope = Envelope(
1828
+ command=f"job.{section}", status="success",
1829
+ data={"job_id": resolved.external_job_id, **payload},
1830
+ metadata={"project": resolved.project, "job_id": resolved.external_job_id},
1831
+ agent_hints=AgentHints(warnings=[
1832
+ "The service may return only part of the log; requested_size does not prove completeness."
1833
+ ] if section == "worker-log" else []),
1834
+ )
1835
+ self.log(envelope.command, envelope.status, envelope.metadata)
1836
+ return envelope
1837
+
1814
1838
  def job_diagnose(self, job_id: 'str', *, project: 'str | None' = None) -> 'Envelope':
1815
1839
  if self.remote_jobs:
1816
1840
  resolved = self._resolve_remote_job_id(job_id, project=project)
@@ -3836,6 +3860,288 @@ class MaxCApp:
3836
3860
  self.log("meta.list-projects", envelope.status, envelope.metadata)
3837
3861
  return envelope
3838
3862
 
3863
+ # --- Knowledge base (public MCP) ------------------------------------
3864
+
3865
+ def _mcp_client(self):
3866
+ """Build a stateless MCP client authorized by the already-resolved credentials.
3867
+
3868
+ Raises ``FeatureUnavailableError`` rather than silently falling back: an agent
3869
+ that reads "no results" and "KB unreachable" as the same signal would conclude
3870
+ the documentation does not cover its question.
3871
+ """
3872
+ from .backend.mcp import (
3873
+ CatalogMcpTokenProvider,
3874
+ McpError,
3875
+ McpHttpClient,
3876
+ build_catalog_mint,
3877
+ default_endpoint,
3878
+ )
3879
+
3880
+ if not self.config.mcp.enabled:
3881
+ raise FeatureUnavailableError(
3882
+ "Knowledge-base commands need the MaxCompute MCP endpoint enabled.",
3883
+ suggestion=(
3884
+ "Set `mcp.enabled: true` in the maxc config. The bearer is minted "
3885
+ "from your existing MaxCompute credentials, so no separate login "
3886
+ "is required."
3887
+ ),
3888
+ )
3889
+ if self.backend is None or not hasattr(self.backend, "_catalog_rest"):
3890
+ raise FeatureUnavailableError(
3891
+ "Knowledge-base commands need an authenticated backend to mint an MCP token.",
3892
+ suggestion="Configure credentials first: `maxc auth whoami --json`.",
3893
+ )
3894
+ catalog_rest = self.backend._catalog_rest
3895
+ if catalog_rest is None:
3896
+ raise BackendConnectionError(
3897
+ "Could not reach CatalogAPI, which mints the MCP access token.",
3898
+ suggestion=(
3899
+ "Verify connectivity and identity: `maxc agent doctor --online --json`."
3900
+ ),
3901
+ )
3902
+ endpoint = (self.config.mcp.endpoint or "").strip() or default_endpoint(
3903
+ self.config.default_region
3904
+ )
3905
+ try:
3906
+ tokens = CatalogMcpTokenProvider(
3907
+ build_catalog_mint(catalog_rest, catalog_rest.endpoint or "")
3908
+ )
3909
+ return McpHttpClient(
3910
+ endpoint,
3911
+ tokens,
3912
+ timeout=self.config.mcp.timeout_seconds,
3913
+ client_version=__version__,
3914
+ )
3915
+ except McpError as exc:
3916
+ raise BackendConnectionError(str(exc)) from exc
3917
+
3918
+ @staticmethod
3919
+ def _kb_structured(result: 'dict[str, Any]') -> 'dict[str, Any]':
3920
+ structured = result.get("structuredContent")
3921
+ if not isinstance(structured, dict):
3922
+ raise BackendConnectionError(
3923
+ "The knowledge-base tool returned no structured content.",
3924
+ suggestion="Retry, or fall back to `maxc meta search` for metadata lookups.",
3925
+ )
3926
+ return structured
3927
+
3928
+ @staticmethod
3929
+ def _kb_payload(
3930
+ structured: 'dict[str, Any]',
3931
+ *,
3932
+ answer_from: 'tuple[str, ...] | None' = None,
3933
+ nested_citation: bool = False,
3934
+ package: bool = False,
3935
+ ) -> 'dict[str, Any]':
3936
+ """Carry the server's fields through rather than rebuilding them.
3937
+
3938
+ Only two transformations happen here: citations are flattened to a stable
3939
+ ``uri`` key (the server nests it under ``source`` for search and puts it
3940
+ top-level for ask), and an answer string is wrapped so its provenance is
3941
+ explicit. Everything else is passed by reference on purpose — a field the
3942
+ service adds starts appearing in maxc output with no CLI change, which is
3943
+ the behaviour worth having when the upstream schema is not ours to pin.
3944
+ """
3945
+ data = structured.get("data") if isinstance(structured.get("data"), dict) else {}
3946
+ payload: dict[str, Any] = {}
3947
+ if answer_from is not None:
3948
+ text = next(
3949
+ (data[key] for key in answer_from if data.get(key) is not None), None
3950
+ )
3951
+ payload["answer"] = {"query": data.get("query"), "text": text}
3952
+ else:
3953
+ search: dict[str, Any] = {
3954
+ "query": data.get("query"),
3955
+ "matches": [],
3956
+ }
3957
+ if package:
3958
+ search["package"] = data.get("package")
3959
+ raw_items = data.get("results") or data.get("matches") or []
3960
+ matches = []
3961
+ for item in raw_items:
3962
+ if not isinstance(item, dict):
3963
+ continue
3964
+ entry = dict(item)
3965
+ uri = entry.get("uri") or entry.get("url")
3966
+ if uri is None and nested_citation:
3967
+ source = entry.get("source")
3968
+ if isinstance(source, dict):
3969
+ uri = source.get("uri") or source.get("url")
3970
+ elif isinstance(source, str):
3971
+ uri = source
3972
+ if uri is not None:
3973
+ entry["uri"] = uri
3974
+ matches.append(entry)
3975
+ search["matches"] = matches
3976
+ payload["search"] = search
3977
+ raw_citations = structured.get("citations")
3978
+ if isinstance(raw_citations, list):
3979
+ flattened = []
3980
+ for item in raw_citations:
3981
+ if not isinstance(item, dict):
3982
+ continue
3983
+ entry = dict(item)
3984
+ uri = entry.get("uri") or entry.get("url")
3985
+ if uri is not None:
3986
+ entry["uri"] = uri
3987
+ flattened.append(entry)
3988
+ payload["citations"] = flattened
3989
+ payload["pagination"] = {
3990
+ "has_more": bool(structured.get("has_more", False)),
3991
+ "next_cursor": structured.get("next_cursor"),
3992
+ }
3993
+ # `request_id` is the only handle for correlating a metered model call with
3994
+ # a service-side incident, so keep it at a predictable location.
3995
+ payload["request_id"] = structured.get("request_id")
3996
+ return payload
3997
+
3998
+ def kb_ask(
3999
+ self,
4000
+ question: 'str',
4001
+ *,
4002
+ max_docs: 'int | None' = None,
4003
+ region: 'str | None' = None,
4004
+ ) -> 'Envelope':
4005
+ """Answer from retrieved documentation, with citations kept as first-class output.
4006
+
4007
+ The two kb tools return different shapes (ask yields ``data.answer`` plus a
4008
+ top-level ``citations[]``; search yields ``data.results[]`` with the URI nested
4009
+ under ``source``), so each is projected explicitly instead of sharing one guess.
4010
+ """
4011
+ started = monotonic()
4012
+ arguments: dict[str, Any] = {"question": question}
4013
+ if max_docs is not None:
4014
+ arguments["max_docs"] = max_docs
4015
+ effective_region = region or self.config.default_region
4016
+ if effective_region:
4017
+ arguments["region"] = effective_region
4018
+ structured = self._call_kb_tool("maxcompute_kb_ask", arguments)
4019
+ payload = self._kb_payload(
4020
+ structured,
4021
+ answer_from=("answer", "text"),
4022
+ )
4023
+ envelope = self._kb_envelope(
4024
+ "kb.ask",
4025
+ payload,
4026
+ started,
4027
+ effective_region,
4028
+ follow_up="kb.search",
4029
+ warnings=self._kb_warnings(structured),
4030
+ insights=[
4031
+ "The answer text is model-generated from the cited documents; attribute "
4032
+ "product claims to `citations[].uri` rather than restating them as fact.",
4033
+ "An empty or thin citation list means retrieval found little, which is "
4034
+ "not evidence that the behaviour is undocumented.",
4035
+ ],
4036
+ )
4037
+ self.log("kb.ask", envelope.status, envelope.metadata)
4038
+ return envelope
4039
+
4040
+ def kb_search(
4041
+ self,
4042
+ query: 'str',
4043
+ *,
4044
+ limit: 'int' = 5,
4045
+ context_lines: 'int | None' = None,
4046
+ region: 'str | None' = None,
4047
+ ) -> 'Envelope':
4048
+ started = monotonic()
4049
+ arguments: dict[str, Any] = {"query": query, "limit": limit}
4050
+ if context_lines is not None:
4051
+ arguments["before_lines"] = context_lines
4052
+ arguments["after_lines"] = context_lines
4053
+ effective_region = region or self.config.default_region
4054
+ if effective_region:
4055
+ arguments["region"] = effective_region
4056
+ structured = self._call_kb_tool("maxcompute_kb_search", arguments)
4057
+ payload = self._kb_payload(
4058
+ structured,
4059
+ nested_citation=True,
4060
+ package=True,
4061
+ )
4062
+ envelope = self._kb_envelope(
4063
+ "kb.search",
4064
+ payload,
4065
+ started,
4066
+ effective_region,
4067
+ follow_up="kb.ask",
4068
+ warnings=self._kb_warnings(structured),
4069
+ insights=[
4070
+ "Snippets are truncated server-side; open the cited `uri` before quoting "
4071
+ "a passage as complete.",
4072
+ ],
4073
+ )
4074
+ self.log("kb.search", envelope.status, envelope.metadata)
4075
+ return envelope
4076
+
4077
+ @staticmethod
4078
+ def _kb_warnings(structured: 'dict[str, Any]') -> 'list[str]':
4079
+ """Surface retrieval degradation that still answered HTTP 200 and ok=true-looking.
4080
+
4081
+ Negative inference is the specific risk here: an agent reading an empty result
4082
+ as "the platform cannot do this" produces a confident, wrong answer.
4083
+ """
4084
+ warnings = [str(item) for item in (structured.get("warnings") or [])]
4085
+ if structured.get("ok") is False:
4086
+ warnings.append(
4087
+ "The knowledge-base tool reported ok=false; treat this response as unverified."
4088
+ )
4089
+ return warnings
4090
+
4091
+ def _kb_envelope(
4092
+ self,
4093
+ command: 'str',
4094
+ payload: 'dict[str, Any]',
4095
+ started: float,
4096
+ region: 'str | None',
4097
+ *,
4098
+ follow_up: 'str',
4099
+ warnings: 'list[str]',
4100
+ insights: 'list[str]',
4101
+ ) -> 'Envelope':
4102
+ metadata = {
4103
+ "elapsed_ms": int((monotonic() - started) * 1000),
4104
+ "region": region or None,
4105
+ "backend": "mcp",
4106
+ }
4107
+ return Envelope(
4108
+ command=command,
4109
+ status="success",
4110
+ data=payload,
4111
+ metadata=metadata,
4112
+ agent_hints=AgentHints(
4113
+ actions=[action(follow_up, data=payload, metadata=metadata)],
4114
+ warnings=warnings,
4115
+ insights=insights,
4116
+ ),
4117
+ )
4118
+
4119
+ def _call_kb_tool(self, tool: 'str', arguments: 'dict[str, Any]') -> 'dict[str, Any]':
4120
+ from .backend.mcp import McpError
4121
+
4122
+ client = self._mcp_client()
4123
+ try:
4124
+ result = client.call_tool(tool, arguments)
4125
+ except McpError as exc:
4126
+ raise BackendConnectionError(
4127
+ str(exc),
4128
+ suggestion=(
4129
+ "Confirm the MCP endpoint is reachable for your region, then retry. "
4130
+ "Do not conclude from this failure that the documentation lacks an answer."
4131
+ ),
4132
+ ) from exc
4133
+ # A tool-level error still answers HTTP 200, so it must not read as success.
4134
+ if result.get("isError"):
4135
+ content = result.get("content") or []
4136
+ detail = ""
4137
+ if content and isinstance(content[0], dict):
4138
+ detail = str(content[0].get("text") or "")
4139
+ raise BackendConnectionError(
4140
+ "The knowledge-base tool failed: "
4141
+ + (detail[:300] or "no detail returned")
4142
+ )
4143
+ return self._kb_structured(result)
4144
+
3839
4145
  def meta_list_schemas(self, *, project: 'str | None' = None) -> 'Envelope':
3840
4146
  """List all schemas in a project."""
3841
4147
  target_project = project or self.config.default_project
@@ -5677,6 +5983,9 @@ class MaxCApp:
5677
5983
  "remote_jobs": getattr(self.backend, "supports_remote_jobs", True) if self.backend else True,
5678
5984
  "cost_check": getattr(self.backend, "supports_cost_check", True) if self.backend else True,
5679
5985
  "lineage": False, # Always false for current ODPS backend
5986
+ # Reported from configuration only; probing the MCP endpoint would make
5987
+ # this local command reach the network.
5988
+ "knowledge_base": bool(self.config.mcp.enabled),
5680
5989
  }
5681
5990
 
5682
5991
  # Keep agent.context strictly local. Report Catalog search capability
@@ -66,8 +66,12 @@ class ResolvedAuthConnection:
66
66
  _MINIMUM_PYODPS = "0.12.0"
67
67
 
68
68
  def create_client(self):
69
+ from .enterprise_tls import configure_enterprise_tls_env
69
70
  from .odps_runtime import configure_user_agent
70
71
 
72
+ # requests resolves SSL_CERT_FILE when a Session is constructed, so
73
+ # this must run before ODPS builds its REST and tunnel clients.
74
+ configure_enterprise_tls_env()
71
75
  configure_user_agent()
72
76
  try:
73
77
  from odps import ODPS
@@ -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]':