maxc-cli 0.5.1__tar.gz → 0.6.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 (129) hide show
  1. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/PKG-INFO +74 -14
  2. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/README.md +73 -13
  3. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/__init__.py +1 -1
  4. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/_samples.py +12 -0
  5. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/app.py +97 -15
  6. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/audit.py +1 -0
  7. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/auth_providers.py +31 -1
  8. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/data.py +7 -4
  9. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/job.py +4 -2
  10. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/odps.py +85 -36
  11. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/query.py +33 -11
  12. maxc_cli-0.6.1/src/maxc_cli/backend/semantic.py +124 -0
  13. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/cli.py +174 -31
  14. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/exceptions.py +47 -22
  15. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/helpers.py +6 -3
  16. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/models.py +131 -16
  17. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/output.py +82 -20
  18. maxc_cli-0.6.1/src/maxc_cli/proxy_auth.py +16 -0
  19. maxc_cli-0.6.1/src/maxc_cli/semantic.py +239 -0
  20. maxc_cli-0.6.1/src/maxc_cli/semantic_management.py +159 -0
  21. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/SKILL.md +60 -22
  22. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/bootstrap-auth.md +32 -9
  23. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/bootstrap-flow.md +4 -3
  24. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/command-patterns.md +39 -7
  25. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/json-output-format.md +78 -15
  26. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/maxcompute-sql-notes.md +33 -11
  27. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/red-lines.md +10 -8
  28. maxc_cli-0.6.1/src/maxc_cli/skills/references/semantic-packages.md +85 -0
  29. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/setup-install.md +12 -7
  30. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/sql-common-errors.md +9 -7
  31. maxc_cli-0.6.1/src/maxc_cli/utils.py +1881 -0
  32. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/PKG-INFO +74 -14
  33. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/SOURCES.txt +9 -0
  34. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_agent_hints_and_cli.py +59 -10
  35. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_agent_skill_commands_context.py +46 -3
  36. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_ai_native_contract_regressions.py +71 -3
  37. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_cli_mock.py +9 -1
  38. maxc_cli-0.6.1/tests/test_effective_hints_contract.py +301 -0
  39. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_exit_codes.py +40 -4
  40. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_job_improvements.py +85 -1
  41. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_manifest_runtime_contract.py +1 -1
  42. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_meta_schema_and_partition_cols.py +24 -12
  43. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_output_action_safety.py +12 -6
  44. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_output_format_contract.py +18 -2
  45. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_phase1_improvements.py +32 -16
  46. maxc_cli-0.6.1/tests/test_proxy_auth.py +123 -0
  47. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_query_auto_promote.py +33 -0
  48. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_query_result_csv_fallback.py +120 -2
  49. maxc_cli-0.6.1/tests/test_semantic_management.py +301 -0
  50. maxc_cli-0.6.1/tests/test_semantic_transport.py +141 -0
  51. maxc_cli-0.6.1/tests/test_setting_parser.py +934 -0
  52. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_state_permissions.py +4 -0
  53. maxc_cli-0.5.1/src/maxc_cli/utils.py +0 -795
  54. maxc_cli-0.5.1/tests/test_setting_parser.py +0 -412
  55. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/MANIFEST.in +0 -0
  56. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/pyproject.toml +0 -0
  57. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/scripts/pyinstaller_entry.py +0 -0
  58. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/scripts/regression_test.py +0 -0
  59. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/setup.cfg +0 -0
  60. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/setup.py +0 -0
  61. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/__main__.py +0 -0
  62. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/agent_platforms.py +0 -0
  63. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/auth_continuation.py +0 -0
  64. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/__init__.py +0 -0
  65. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/auth.py +0 -0
  66. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/catalog.py +0 -0
  67. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/backend/meta.py +0 -0
  68. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/cache.py +0 -0
  69. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/catalog_bootstrap.py +0 -0
  70. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/config.py +0 -0
  71. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/help_format.py +0 -0
  72. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/job_ids.py +0 -0
  73. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/masking.py +0 -0
  74. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/oauth.py +0 -0
  75. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/odps_runtime.py +0 -0
  76. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/setting_parser.py +0 -0
  77. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/agents/openai.yaml +0 -0
  78. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/maxcompute-select-guide.md +0 -0
  79. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/partition-guide.md +0 -0
  80. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/sql-query-patterns.md +0 -0
  81. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/skills/references/text2sql-principles.md +0 -0
  82. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/state_permissions.py +0 -0
  83. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli/store.py +0 -0
  84. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/dependency_links.txt +0 -0
  85. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/entry_points.txt +0 -0
  86. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/requires.txt +0 -0
  87. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/src/maxc_cli.egg-info/top_level.txt +0 -0
  88. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_agent_platforms.py +0 -0
  89. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_agent_skill_commands.py +0 -0
  90. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_auth_logout.py +0 -0
  91. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_backend_auth.py +0 -0
  92. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_backend_data.py +0 -0
  93. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_backend_data_serialization.py +0 -0
  94. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_backend_meta.py +0 -0
  95. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_build_release_archive_compat.py +0 -0
  96. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_build_release_script.py +0 -0
  97. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_cache.py +0 -0
  98. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_catalog.py +0 -0
  99. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_catalog_bootstrap.py +0 -0
  100. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_cli_arg_validation.py +0 -0
  101. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_cli_query_parse_and_sanitize.py +0 -0
  102. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_compat.py +0 -0
  103. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_config_atomic_write.py +0 -0
  104. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_e2e_smoke.py +0 -0
  105. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_envelope_shape.py +0 -0
  106. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_error_self_correction.py +0 -0
  107. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_error_translation.py +0 -0
  108. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_external_auth.py +0 -0
  109. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_flag_hoist.py +0 -0
  110. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_help_format.py +0 -0
  111. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_help_version_e2e.py +0 -0
  112. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_helpers.py +0 -0
  113. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_helpers_csv.py +0 -0
  114. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_installer_contracts.py +0 -0
  115. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_integration.py +0 -0
  116. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_integration_real.py +0 -0
  117. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_job_store_durability.py +0 -0
  118. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_masking.py +0 -0
  119. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_oauth.py +0 -0
  120. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_odps_runtime.py +0 -0
  121. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_packaging_metadata.py +0 -0
  122. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_pyinstaller_bundle.py +0 -0
  123. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_python39_compat.py +0 -0
  124. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_semantic_scope.py +0 -0
  125. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_skill_cli_consistency.py +0 -0
  126. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_skill_eval.py +0 -0
  127. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_skill_renderer.py +0 -0
  128. {maxc_cli-0.5.1 → maxc_cli-0.6.1}/tests/test_startup_imports.py +0 -0
  129. {maxc_cli-0.5.1 → maxc_cli-0.6.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.5.1
3
+ Version: 0.6.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
@@ -31,7 +31,7 @@ MaxCompute 元数据、SQL、作业、权限和数据传输等数据面操作。
31
31
  ### 公共云(推荐)
32
32
 
33
33
  ```bash
34
- # Alibaba Cloud CLI 需要 >= 3.3.3;旧版本先运行 aliyun upgrade
34
+ # aliyun maxc 需要 Alibaba Cloud CLI >= 3.3.19
35
35
  aliyun version
36
36
 
37
37
  # OAuth 是交互式登录的首选方式,不需要把长期 AK/SK 放进命令行
@@ -49,6 +49,9 @@ aliyun maxc query cost "SELECT * FROM schema.table WHERE ds='20260415'" --json
49
49
  aliyun maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
50
50
  ```
51
51
 
52
+ 支持自升级的非 Homebrew Alibaba Cloud CLI 3.3.5+ 可在用户确认后运行
53
+ `aliyun upgrade`;更早版本、Homebrew 安装或缺失 CLI 请按官方安装方式更新。
54
+
52
55
  如果 Alibaba Cloud CLI 扩展不可用,或明确需要 PyPI 版本,可使用独立入口。
53
56
  独立入口要求 Python 3.9 或更高版本:
54
57
 
@@ -110,13 +113,15 @@ Agent 启动时生成一次 32 位小写十六进制 session ID,并在整个
110
113
  UA="AlibabaCloud-Agent-Skills/alibabacloud-maxcompute-cli/<session-id>"
111
114
  ```
112
115
 
113
- 每条 `aliyun maxc` 命令都追加 `--user-agent "$UA"`。然后依次运行:
116
+ 每条调用云 API 的 `aliyun maxc` 命令都追加 `--user-agent "$UA"`;本地
117
+ help、`agent context`、`agent manifest`、`session show` 和 `cache status`
118
+ 可以省略。然后依次运行:
114
119
 
115
120
  ```bash
116
- aliyun maxc agent context --user-agent "$UA" --json # 仅检查本地版本、配置和能力;不访问网络
117
- aliyun maxc agent manifest --user-agent "$UA" --json # 从实时 parser 生成命令、参数和副作用清单
121
+ aliyun maxc agent context --json # 仅检查本地版本、配置和能力;不访问网络
122
+ aliyun maxc agent manifest --json # 从实时 parser 生成命令、参数和副作用清单
118
123
  aliyun maxc agent doctor --online --user-agent "$UA" --json # 验证身份与后端可达性
119
- aliyun maxc agent skill --user-agent "$UA" --json # Skill 路径、名称与 min_cli_version
124
+ aliyun maxc agent skill --json # Skill 路径、名称与 min_cli_version
120
125
  ```
121
126
 
122
127
  只有 `agent doctor --online` 能证明远端已就绪;不要把 `agent context` 中的
@@ -139,10 +144,19 @@ aliyun maxc agent skill --user-agent "$UA" --json # Skill 路径、
139
144
  {
140
145
  "id": "meta.search",
141
146
  "title": "Search tables",
142
- "command": "maxc meta search <keyword> --json",
147
+ "command": "maxc --user-agent <user_agent> meta search <keyword> --json",
143
148
  "executable": false,
144
- "placeholders": {"keyword": "search keyword"},
145
- "args_schema": {},
149
+ "placeholders": {
150
+ "keyword": "<keyword>",
151
+ "user_agent": "<user_agent>"
152
+ },
153
+ "args_schema": {
154
+ "user_agent": {
155
+ "type": "string",
156
+ "description": "Reuse the User-Agent generated once for the current Agent session.",
157
+ "pattern": "^AlibabaCloud-Agent-Skills/alibabacloud-maxcompute-cli/[0-9a-f]{32}$"
158
+ }
159
+ },
146
160
  "effect": "read",
147
161
  "confirmation_required": false,
148
162
  "agent_allowed": true
@@ -178,7 +192,7 @@ maxc meta describe my_table --json
178
192
  "mode": "read_only",
179
193
  "force": false,
180
194
  "allowed_operations": ["SELECT"],
181
- "effective_hints": {"odps.sql.read.only": "true"},
195
+ "effective_hints": {},
182
196
  "policy_decision": "allowed"
183
197
  }
184
198
  ```
@@ -219,10 +233,14 @@ src/maxc_cli/
219
233
 
220
234
  ## 限制
221
235
 
222
- - **查询安全**:公共 Agent Skill 的 SQL 契约只支持 `SELECT`,不通过
223
- `query` 或 `job submit` 执行 DDL/DML。`data upload`、
224
- `data download --overwrite` 和 `job cancel` 是独立的有副作用操作,仍需
225
- 与影响相匹配的明确授权。
236
+ - **查询安全**:SQL 默认按只读请求处理。只有用户明确要求具体 DDL/DML
237
+ 时,才核对完整 statement、project、schema、目标和影响,通过 `query`
238
+ 或 `job submit` 一次提交一条语句并显式增加 `--force`。`data upload`、
239
+ `data download --overwrite` 和 `job cancel` 是独立的有副作用操作,同样需
240
+ 与影响相匹配的明确授权。权限、账号、project、system、resource、package
241
+ 等管理 SQL 和未知语法不在公共 `--force` 正向允许列表中。前置 `SET`
242
+ 同样属于执行上下文:项目安全、访问控制和脱敏参数始终被阻断,强制写入
243
+ 仅接受已审查的语句级执行参数。
226
244
  - **OAuth 优先**:公共云交互式登录优先 OAuth。只有运行环境明确要求时才用
227
245
  AK/SK、STS、环境变量或外部凭证进程。直接 AK/SK 会写入
228
246
  `~/.maxc/config.yaml`(文件权限 0600)。OAuth 需要账号/组织已分配官方
@@ -251,3 +269,45 @@ src/maxc_cli/
251
269
  pip install -e .
252
270
  pytest tests/ -m unit
253
271
  ```
272
+
273
+ ## Trusted egress authentication
274
+
275
+ In a sandbox whose trusted egress injects ODPS credentials, explicitly use:
276
+
277
+ ```bash
278
+ aliyun maxc auth login-proxy --project <project> --odps-endpoint <https-endpoint> --json
279
+ aliyun maxc agent doctor --online --json
280
+ ```
281
+
282
+ This saves `auth.provider: proxy` and routing information only. MaxC does not
283
+ load AK/SK, STS, OAuth or credential helpers, and sends no Authorization or ODPS
284
+ token headers. The egress must authenticate the sandbox and inject credentials.
285
+ TLS verification remains enabled; preserve the egress-provided CA environment.
286
+ Only proceed when `agent doctor --online` reports `data.ready=true`. A saved
287
+ configuration (`--no-validate`) alone does not prove connectivity or identity.
288
+ The egress must cover metadata, SQL polling and result/Tunnel endpoints actually
289
+ used by the workload; ordinary endpoints without injection will reject requests.
290
+ To return to local signing, explicitly run the appropriate `auth login` command.
291
+
292
+ ## Versioned semantic packages
293
+
294
+ `maxc semantic` manages remote Catalog packages using the current connection.
295
+ Specify the verified main-account ID with `--namespace`. Available operations:
296
+ `list`, `get`, `revisions`, `export`, `create`, `diff`, `apply`, `publish`, `delete`.
297
+ `meta semantic` continues to manage local annotations.
298
+
299
+ Export a USER_DRAFT, edit its `object`/`content`, review `diff`, then use its
300
+ `planDigest` with `apply --plan-digest ... --yes`. Publishing is a separate
301
+ `publish --expected-spec-id ... --expected-revision ... --yes` operation.
302
+ Run each command's `--help` for required arguments.
303
+
304
+ The editable JSON format is `maxc.semantic/v1`; its identity and revision fields
305
+ bind edits to one account, endpoint and object. Whole sections replace prior
306
+ content; omitted sections remain unchanged and explicit empty arrays clear them.
307
+ Exports strip server-generated table IDs and preserve unknown nested fields.
308
+ Source files and output paths must not traverse symlinks/reparse points.
309
+
310
+ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
311
+ write must be reconciled before retrying. Publication checks are not SQL or
312
+ business validation. History lists the latest 20 summaries, and an immutable
313
+ revision read does not guarantee historical DataBridge analysis.
@@ -9,7 +9,7 @@ MaxCompute 元数据、SQL、作业、权限和数据传输等数据面操作。
9
9
  ### 公共云(推荐)
10
10
 
11
11
  ```bash
12
- # Alibaba Cloud CLI 需要 >= 3.3.3;旧版本先运行 aliyun upgrade
12
+ # aliyun maxc 需要 Alibaba Cloud CLI >= 3.3.19
13
13
  aliyun version
14
14
 
15
15
  # OAuth 是交互式登录的首选方式,不需要把长期 AK/SK 放进命令行
@@ -27,6 +27,9 @@ aliyun maxc query cost "SELECT * FROM schema.table WHERE ds='20260415'" --json
27
27
  aliyun maxc query "SELECT * FROM schema.table WHERE ds='20260415'" --json
28
28
  ```
29
29
 
30
+ 支持自升级的非 Homebrew Alibaba Cloud CLI 3.3.5+ 可在用户确认后运行
31
+ `aliyun upgrade`;更早版本、Homebrew 安装或缺失 CLI 请按官方安装方式更新。
32
+
30
33
  如果 Alibaba Cloud CLI 扩展不可用,或明确需要 PyPI 版本,可使用独立入口。
31
34
  独立入口要求 Python 3.9 或更高版本:
32
35
 
@@ -88,13 +91,15 @@ Agent 启动时生成一次 32 位小写十六进制 session ID,并在整个
88
91
  UA="AlibabaCloud-Agent-Skills/alibabacloud-maxcompute-cli/<session-id>"
89
92
  ```
90
93
 
91
- 每条 `aliyun maxc` 命令都追加 `--user-agent "$UA"`。然后依次运行:
94
+ 每条调用云 API 的 `aliyun maxc` 命令都追加 `--user-agent "$UA"`;本地
95
+ help、`agent context`、`agent manifest`、`session show` 和 `cache status`
96
+ 可以省略。然后依次运行:
92
97
 
93
98
  ```bash
94
- aliyun maxc agent context --user-agent "$UA" --json # 仅检查本地版本、配置和能力;不访问网络
95
- aliyun maxc agent manifest --user-agent "$UA" --json # 从实时 parser 生成命令、参数和副作用清单
99
+ aliyun maxc agent context --json # 仅检查本地版本、配置和能力;不访问网络
100
+ aliyun maxc agent manifest --json # 从实时 parser 生成命令、参数和副作用清单
96
101
  aliyun maxc agent doctor --online --user-agent "$UA" --json # 验证身份与后端可达性
97
- aliyun maxc agent skill --user-agent "$UA" --json # Skill 路径、名称与 min_cli_version
102
+ aliyun maxc agent skill --json # Skill 路径、名称与 min_cli_version
98
103
  ```
99
104
 
100
105
  只有 `agent doctor --online` 能证明远端已就绪;不要把 `agent context` 中的
@@ -117,10 +122,19 @@ aliyun maxc agent skill --user-agent "$UA" --json # Skill 路径、
117
122
  {
118
123
  "id": "meta.search",
119
124
  "title": "Search tables",
120
- "command": "maxc meta search <keyword> --json",
125
+ "command": "maxc --user-agent <user_agent> meta search <keyword> --json",
121
126
  "executable": false,
122
- "placeholders": {"keyword": "search keyword"},
123
- "args_schema": {},
127
+ "placeholders": {
128
+ "keyword": "<keyword>",
129
+ "user_agent": "<user_agent>"
130
+ },
131
+ "args_schema": {
132
+ "user_agent": {
133
+ "type": "string",
134
+ "description": "Reuse the User-Agent generated once for the current Agent session.",
135
+ "pattern": "^AlibabaCloud-Agent-Skills/alibabacloud-maxcompute-cli/[0-9a-f]{32}$"
136
+ }
137
+ },
124
138
  "effect": "read",
125
139
  "confirmation_required": false,
126
140
  "agent_allowed": true
@@ -156,7 +170,7 @@ maxc meta describe my_table --json
156
170
  "mode": "read_only",
157
171
  "force": false,
158
172
  "allowed_operations": ["SELECT"],
159
- "effective_hints": {"odps.sql.read.only": "true"},
173
+ "effective_hints": {},
160
174
  "policy_decision": "allowed"
161
175
  }
162
176
  ```
@@ -197,10 +211,14 @@ src/maxc_cli/
197
211
 
198
212
  ## 限制
199
213
 
200
- - **查询安全**:公共 Agent Skill 的 SQL 契约只支持 `SELECT`,不通过
201
- `query` 或 `job submit` 执行 DDL/DML。`data upload`、
202
- `data download --overwrite` 和 `job cancel` 是独立的有副作用操作,仍需
203
- 与影响相匹配的明确授权。
214
+ - **查询安全**:SQL 默认按只读请求处理。只有用户明确要求具体 DDL/DML
215
+ 时,才核对完整 statement、project、schema、目标和影响,通过 `query`
216
+ 或 `job submit` 一次提交一条语句并显式增加 `--force`。`data upload`、
217
+ `data download --overwrite` 和 `job cancel` 是独立的有副作用操作,同样需
218
+ 与影响相匹配的明确授权。权限、账号、project、system、resource、package
219
+ 等管理 SQL 和未知语法不在公共 `--force` 正向允许列表中。前置 `SET`
220
+ 同样属于执行上下文:项目安全、访问控制和脱敏参数始终被阻断,强制写入
221
+ 仅接受已审查的语句级执行参数。
204
222
  - **OAuth 优先**:公共云交互式登录优先 OAuth。只有运行环境明确要求时才用
205
223
  AK/SK、STS、环境变量或外部凭证进程。直接 AK/SK 会写入
206
224
  `~/.maxc/config.yaml`(文件权限 0600)。OAuth 需要账号/组织已分配官方
@@ -229,3 +247,45 @@ src/maxc_cli/
229
247
  pip install -e .
230
248
  pytest tests/ -m unit
231
249
  ```
250
+
251
+ ## Trusted egress authentication
252
+
253
+ In a sandbox whose trusted egress injects ODPS credentials, explicitly use:
254
+
255
+ ```bash
256
+ aliyun maxc auth login-proxy --project <project> --odps-endpoint <https-endpoint> --json
257
+ aliyun maxc agent doctor --online --json
258
+ ```
259
+
260
+ This saves `auth.provider: proxy` and routing information only. MaxC does not
261
+ load AK/SK, STS, OAuth or credential helpers, and sends no Authorization or ODPS
262
+ token headers. The egress must authenticate the sandbox and inject credentials.
263
+ TLS verification remains enabled; preserve the egress-provided CA environment.
264
+ Only proceed when `agent doctor --online` reports `data.ready=true`. A saved
265
+ configuration (`--no-validate`) alone does not prove connectivity or identity.
266
+ The egress must cover metadata, SQL polling and result/Tunnel endpoints actually
267
+ used by the workload; ordinary endpoints without injection will reject requests.
268
+ To return to local signing, explicitly run the appropriate `auth login` command.
269
+
270
+ ## Versioned semantic packages
271
+
272
+ `maxc semantic` manages remote Catalog packages using the current connection.
273
+ Specify the verified main-account ID with `--namespace`. Available operations:
274
+ `list`, `get`, `revisions`, `export`, `create`, `diff`, `apply`, `publish`, `delete`.
275
+ `meta semantic` continues to manage local annotations.
276
+
277
+ Export a USER_DRAFT, edit its `object`/`content`, review `diff`, then use its
278
+ `planDigest` with `apply --plan-digest ... --yes`. Publishing is a separate
279
+ `publish --expected-spec-id ... --expected-revision ... --yes` operation.
280
+ Run each command's `--help` for required arguments.
281
+
282
+ The editable JSON format is `maxc.semantic/v1`; its identity and revision fields
283
+ bind edits to one account, endpoint and object. Whole sections replace prior
284
+ content; omitted sections remain unchanged and explicit empty arrays clear them.
285
+ Exports strip server-generated table IDs and preserve unknown nested fields.
286
+ Source files and output paths must not traverse symlinks/reparse points.
287
+
288
+ Metadata edits lack server CAS; publication lacks idempotency keys. An uncertain
289
+ write must be reconciled before retrying. Publication checks are not SQL or
290
+ business validation. History lists the latest 20 summaries, and an immutable
291
+ revision read does not guarantee historical DataBridge analysis.
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.5.1"
5
+ __version__ = "0.6.1"
@@ -12,6 +12,15 @@ SAMPLES: dict[str, str] = {
12
12
  'maxc query "SELECT 1"\n'
13
13
  "maxc meta list-tables --project my_proj"
14
14
  ),
15
+ "semantic.list": "maxc semantic list --namespace <account_id> --json",
16
+ "semantic.get": "maxc semantic get sales --namespace <account_id> --source PUBLISHED --revision <revision> --json",
17
+ "semantic.revisions": "maxc semantic revisions sales --namespace <account_id> --json",
18
+ "semantic.export": "maxc semantic export sales --namespace <account_id> --output sales.json --json",
19
+ "semantic.create": "maxc semantic create sales --namespace <account_id> --file definition.json --json",
20
+ "semantic.diff": "maxc semantic diff sales --namespace <account_id> --file sales.json --json",
21
+ "semantic.apply": "maxc semantic apply sales --namespace <account_id> --file sales.json --plan-digest <reviewed_digest> --json",
22
+ "semantic.publish": "maxc semantic publish sales --namespace <account_id> --expected-spec-id <spec_id> --expected-revision <reviewed_revision> --json",
23
+ "semantic.delete": "maxc semantic delete sales --namespace <account_id> --expected-spec-id <spec_id> --json",
15
24
  # ── query ──────────────────────────────────────────────────────────────
16
25
  "query": (
17
26
  'maxc query "SELECT 1"\n'
@@ -24,6 +33,9 @@ SAMPLES: dict[str, str] = {
24
33
  "maxc auth login --oauth\n"
25
34
  "maxc auth login --from-env --json"
26
35
  ),
36
+ "auth.login-proxy": (
37
+ "maxc auth login-proxy --project <project> --odps-endpoint <endpoint> --json",
38
+ ),
27
39
  "auth.login-external": (
28
40
  "maxc auth login-external --process-command 'credential-helper --format json'"
29
41
  ),
@@ -939,7 +939,11 @@ class MaxCApp:
939
939
  status="pending",
940
940
  data={
941
941
  "job_id": job.job_id,
942
- "safety": build_safety_block(force=force, sql=sql),
942
+ "safety": build_safety_block(
943
+ force=force,
944
+ sql=sql,
945
+ effective_hints=job.effective_hints,
946
+ ),
943
947
  },
944
948
  metadata={
945
949
  "job_id": job.job_id,
@@ -985,7 +989,11 @@ class MaxCApp:
985
989
  status="pending",
986
990
  data={
987
991
  "job_id": job.job_id,
988
- "safety": build_safety_block(force=force, sql=sql),
992
+ "safety": build_safety_block(
993
+ force=force,
994
+ sql=sql,
995
+ effective_hints=job.effective_hints,
996
+ ),
989
997
  },
990
998
  metadata={
991
999
  "job_id": job.job_id,
@@ -1108,6 +1116,7 @@ class MaxCApp:
1108
1116
  self.log(command, envelope.status, envelope.metadata)
1109
1117
  return envelope
1110
1118
  result.extra_metadata.update(execution_metadata)
1119
+ result.effective_hints = dict(job.effective_hints)
1111
1120
  for warning in common_warnings:
1112
1121
  if warning not in result.warnings:
1113
1122
  result.warnings.append(warning)
@@ -1285,7 +1294,14 @@ class MaxCApp:
1285
1294
  envelope = Envelope(
1286
1295
  command="job.submit",
1287
1296
  status="pending",
1288
- data={"job_id": job.job_id, "safety": build_safety_block(force=force, sql=sql)},
1297
+ data={
1298
+ "job_id": job.job_id,
1299
+ "safety": build_safety_block(
1300
+ force=force,
1301
+ sql=sql,
1302
+ effective_hints=job.effective_hints,
1303
+ ),
1304
+ },
1289
1305
  metadata={
1290
1306
  "job_id": job.job_id,
1291
1307
  "project": job.project,
@@ -2761,6 +2777,19 @@ class MaxCApp:
2761
2777
  raise ValidationError("--schema must not be empty.")
2762
2778
  return target_project, target_schema
2763
2779
 
2780
+ def semantic_manage(self, operation: str, *, namespace: str, **options) -> 'Envelope':
2781
+ """Run a remote SemanticSpec workflow, separate from local annotations."""
2782
+ from .semantic_management import SemanticManager
2783
+
2784
+ manager = SemanticManager(self.backend, namespace)
2785
+ data = manager.execute(operation, **options)
2786
+ command = "semantic." + operation
2787
+ metadata = {"namespace": namespace}
2788
+ envelope = Envelope(command=command, status="success", data=data,
2789
+ metadata=metadata, agent_hints=AgentHints(warnings=manager.warnings))
2790
+ self.log(command, envelope.status, metadata)
2791
+ return envelope
2792
+
2764
2793
  def semantic_set(
2765
2794
  self,
2766
2795
  table_name: 'str',
@@ -4864,6 +4893,37 @@ class MaxCApp:
4864
4893
  envelope.metadata["continuation_expires_at_unix"] = expires_at
4865
4894
  return envelope
4866
4895
 
4896
+ def auth_login_proxy(
4897
+ self, *, project: str, endpoint: str,
4898
+ region_name: "str | None" = None, tunnel_endpoint: "str | None" = None,
4899
+ no_validate: bool = False, target_config_path: "Path | None" = None,
4900
+ ) -> Envelope:
4901
+ """Save an explicit, credential-free egress authentication configuration."""
4902
+ target_path = target_config_path or default_global_config_path()
4903
+ auth = AuthConfig(provider="proxy", project=project, endpoint=endpoint,
4904
+ region_name=region_name, tunnel_endpoint=tunnel_endpoint)
4905
+ resolved = resolve_auth_connection(self.config, auth_override=auth)
4906
+ if no_validate:
4907
+ identity = {"authenticated": None, "configured": True,
4908
+ "validation_status": "configuration_only", "auth_type": "proxy",
4909
+ "identity_source": "egress_proxy", "project": resolved.project,
4910
+ "endpoint": resolved.endpoint}
4911
+ warnings = ["Configuration saved without remote validation; run agent doctor --online."]
4912
+ else:
4913
+ identity, warnings = self._validate_auth_config(auth)
4914
+ # A failed identity check must leave the previous login untouched.
4915
+ migrate_legacy_session_override(target_path)
4916
+ persist_login_config(target_path, auth=auth)
4917
+ envelope = Envelope(
4918
+ command="auth.login-proxy", status="success",
4919
+ data={"identity": identity,
4920
+ "persistence": {"saved": True, "validated": not no_validate}},
4921
+ metadata={"config_path": str(target_path)},
4922
+ agent_hints=AgentHints(actions=[action("auth.whoami")], warnings=warnings),
4923
+ )
4924
+ self.log("auth.login-proxy", envelope.status, envelope.metadata)
4925
+ return envelope
4926
+
4867
4927
  def auth_login_external(
4868
4928
  self,
4869
4929
  *,
@@ -5599,6 +5659,8 @@ class MaxCApp:
5599
5659
  has_creds = bool(effective_settings.get("external_process_command"))
5600
5660
  elif provider == "oauth":
5601
5661
  has_creds = auth_cfg.oauth.is_configured()
5662
+ elif provider == "proxy":
5663
+ has_creds = True
5602
5664
  else:
5603
5665
  has_creds = False
5604
5666
 
@@ -5756,13 +5818,8 @@ class MaxCApp:
5756
5818
  failed_check_ids = [
5757
5819
  check["id"] for check in checks if check["status"] == "fail"
5758
5820
  ]
5759
- command = current_cli_entry_point()
5760
5821
  if ready and not online:
5761
- actions = [SuggestedAction(
5762
- id="agent.doctor.online",
5763
- title="Verify credentials and backend",
5764
- command=f"{command} agent doctor --online --json",
5765
- )]
5822
+ actions = [action("agent.doctor", data={"online": True})]
5766
5823
  elif ready:
5767
5824
  actions = [action("meta.list-tables", data=context_data)]
5768
5825
  elif "auth.configured" in failed_check_ids:
@@ -6557,20 +6614,31 @@ class MaxCApp:
6557
6614
 
6558
6615
  # Build actions
6559
6616
  qe_actions: list[SuggestedAction] = []
6617
+ write_operations = known_write_operations(result.sql_executed)
6560
6618
  if result.tables_used and result.extra_metadata.get("result_kind") != "statement":
6561
6619
  qe_actions.append(action("meta.describe", data=data, metadata=metadata))
6562
6620
  if result.has_more and next_cursor:
6563
6621
  qe_actions.append(action("query.paginate", data=data, metadata=metadata))
6564
6622
  elif result.has_more and result.job_id:
6565
6623
  qe_actions.append(action("job.result", data=data, metadata=metadata))
6566
- if dry_run:
6624
+ if dry_run and not write_operations:
6567
6625
  qe_actions.append(action("job.submit", data=data, metadata=metadata))
6626
+ elif dry_run and write_operations:
6627
+ warnings.append(
6628
+ "No replay action was generated for mutating SQL. A dry-run does not "
6629
+ "authorize later execution; rerun the exact statement with `--force` "
6630
+ "only after the user confirms that mutation."
6631
+ )
6568
6632
 
6569
6633
  # Add safety block
6570
6634
  if command in {"job.wait", "job.result"}:
6571
6635
  data["safety"] = build_observational_safety_block(command)
6572
6636
  else:
6573
- data["safety"] = build_safety_block(force=force, sql=result.sql_executed)
6637
+ data["safety"] = build_safety_block(
6638
+ force=force,
6639
+ sql=result.sql_executed,
6640
+ effective_hints=result.effective_hints,
6641
+ )
6574
6642
 
6575
6643
  return Envelope(
6576
6644
  command=command,
@@ -6592,6 +6660,8 @@ class MaxCApp:
6592
6660
  analysis: 'dict[str, Any]',
6593
6661
  force: 'bool' = False,
6594
6662
  ) -> 'Envelope':
6663
+ analysis = dict(analysis)
6664
+ effective_hints = analysis.pop("_effective_hints", {})
6595
6665
  warnings = list(analysis.get("warnings", []))
6596
6666
  insights = []
6597
6667
  if analysis.get("estimated_input_size_bytes") == 0:
@@ -6606,14 +6676,26 @@ class MaxCApp:
6606
6676
 
6607
6677
  # Build actions
6608
6678
  ae_actions: list[SuggestedAction] = []
6609
- if command == "query.cost":
6610
- ae_actions.append(action("query.explain", data=analysis, metadata=metadata))
6611
- ae_actions.append(action("query", data=analysis, metadata=metadata))
6679
+ write_operations = known_write_operations(sql)
6680
+ if not write_operations:
6681
+ if command == "query.cost":
6682
+ ae_actions.append(action("query.explain", data=analysis, metadata=metadata))
6683
+ ae_actions.append(action("query", data=analysis, metadata=metadata))
6684
+ else:
6685
+ warnings.append(
6686
+ "No replay action was generated for mutating SQL. Cost or plan analysis "
6687
+ "does not authorize later execution; rerun the exact statement with "
6688
+ "`--force` only after the user confirms that mutation."
6689
+ )
6612
6690
  if analysis.get("tables_used"):
6613
6691
  ae_actions.append(action("meta.describe", data=analysis, metadata=metadata))
6614
6692
 
6615
6693
  # Add safety block
6616
- analysis["safety"] = build_safety_block(force=force, sql=sql)
6694
+ analysis["safety"] = build_safety_block(
6695
+ force=force,
6696
+ sql=sql,
6697
+ effective_hints=effective_hints,
6698
+ )
6617
6699
 
6618
6700
  return Envelope(
6619
6701
  command=command,
@@ -18,6 +18,7 @@ _SENSITIVE_KEY_FRAGMENTS = (
18
18
  "continuation",
19
19
  "cookie",
20
20
  "credential",
21
+ "logview",
21
22
  "password",
22
23
  "process_command",
23
24
  "refresh_token",
@@ -137,6 +137,8 @@ class ResolvedAuthConnection:
137
137
  pyodps_catalog.Client instance, or None if the SDK is not
138
138
  installed or catalog endpoint is unavailable.
139
139
  """
140
+ if self.provider == "proxy":
141
+ return None
140
142
  try:
141
143
  from maxcompute_tea_openapi import models as open_api_models
142
144
  from pyodps_catalog.client import Client as CatalogClient
@@ -171,6 +173,8 @@ def auth_settings_available(config: 'MaxCConfig') -> 'bool':
171
173
  settings, _, _suppressed = resolve_odps_settings(config)
172
174
  provider = infer_auth_provider(config, settings)
173
175
  try:
176
+ if provider == "proxy":
177
+ return not missing_odps_settings(settings, auth_type="proxy")
174
178
  if provider == "sts_token":
175
179
  return not missing_odps_settings(settings, auth_type="sts_token")
176
180
  if provider == "external":
@@ -193,6 +197,32 @@ def resolve_auth_connection(
193
197
  settings, sources, suppressed_env_vars = resolve_odps_settings(config, auth_override=auth_override)
194
198
  provider = infer_auth_provider(config, settings, auth_override=auth_override)
195
199
 
200
+ if provider == "proxy":
201
+ missing = missing_odps_settings(settings, auth_type="proxy")
202
+ if missing:
203
+ raise ValidationError(
204
+ f"Proxy authentication is missing required fields: {', '.join(missing)}.",
205
+ suggestion="Run `auth login-proxy --project <project> --odps-endpoint <endpoint>`.",
206
+ )
207
+ from .proxy_auth import ProxyAccount
208
+
209
+ # Never retain stale credentials or helper configuration in this mode.
210
+ connection_fields = {"provider", "project", "endpoint", "region_name",
211
+ "tunnel_endpoint", "catalog_endpoint"}
212
+ settings = {key: value if key in connection_fields else None
213
+ for key, value in settings.items()}
214
+ return ResolvedAuthConnection(
215
+ auth_type="proxy", provider="proxy",
216
+ project=settings["project"], endpoint=settings["endpoint"],
217
+ region_name=settings.get("region_name"),
218
+ tunnel_endpoint=settings.get("tunnel_endpoint"),
219
+ catalog_endpoint=settings.get("catalog_endpoint"),
220
+ access_id=None, secret_access_key=None, security_token=None,
221
+ token_expires_at=None, identity_source="egress_proxy",
222
+ settings=settings, setting_sources=sources,
223
+ suppressed_env_vars=suppressed_env_vars, account=ProxyAccount(),
224
+ )
225
+
196
226
  if provider == "external":
197
227
  missing = missing_odps_settings(settings, auth_type="external")
198
228
  if missing:
@@ -329,7 +359,7 @@ def infer_auth_provider(
329
359
  ) -> 'str':
330
360
  auth = auth_override or config.auth
331
361
  explicit = (auth.provider or settings.get("provider") or "").strip().lower()
332
- if explicit in {"access_key", "sts_token", "sts", "external", "oauth"}:
362
+ if explicit in {"access_key", "sts_token", "sts", "external", "oauth", "proxy"}:
333
363
  if explicit == "sts":
334
364
  return "sts_token"
335
365
  return explicit
@@ -326,8 +326,10 @@ class DataMixin:
326
326
  raise ValidationError(
327
327
  f"`{definition.name}` is a view; views are read-only and cannot be loaded via Tunnel.",
328
328
  suggestion=(
329
- "Choose a physical table that supports Tunnel upload, or use "
330
- "an approved data-write workflow outside the public Agent Skill."
329
+ "Choose a physical table that supports Tunnel upload. If the "
330
+ "user separately authorizes one exact supported SQL write and "
331
+ "its target and effect, submit that statement with --force; "
332
+ "otherwise use an approved bulk data-write workflow."
331
333
  ),
332
334
  )
333
335
  partition_columns = {c.name for c in definition.partition_columns}
@@ -352,8 +354,9 @@ class DataMixin:
352
354
  raise ValidationError(
353
355
  f"Columns {unsupported} have complex types not supported by CSV upload.",
354
356
  suggestion=(
355
- "Convert the input through an approved data-write workflow "
356
- "outside the public Agent Skill."
357
+ "Use a format and approved data-write workflow that supports "
358
+ "the exact complex types. Do not auto-convert or submit a SQL "
359
+ "write unless the user separately authorizes its target and effect."
357
360
  ),
358
361
  )
359
362
 
@@ -781,8 +781,10 @@ class JobMixin(QueryMixin):
781
781
  return sql.rstrip(";") if sql else None
782
782
 
783
783
  def _safe_logview(self, instance) -> 'str | None':
784
- """Safely get logview URL from instance."""
784
+ """Get a LogView URL without exposing its signed query or fragment."""
785
+ from ..utils import sanitize_logview_url
786
+
785
787
  try:
786
- return instance.get_logview_address()
788
+ return sanitize_logview_url(instance.get_logview_address())
787
789
  except Exception:
788
790
  return None