@josephyan/qingflow-cli 0.2.0-beta.58 → 0.2.0-beta.60
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.
- package/README.md +3 -2
- package/docs/local-agent-install.md +9 -0
- package/npm/bin/qingflow.mjs +1 -1
- package/npm/lib/runtime.mjs +156 -21
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +670 -191
- package/src/qingflow_mcp/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +19 -16
- package/src/qingflow_mcp/cli/commands/builder.py +124 -162
- package/src/qingflow_mcp/cli/commands/common.py +21 -95
- package/src/qingflow_mcp/cli/commands/imports.py +42 -34
- package/src/qingflow_mcp/cli/commands/record.py +131 -133
- package/src/qingflow_mcp/cli/commands/task.py +43 -44
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +35 -32
- package/src/qingflow_mcp/cli/formatters.py +124 -121
- package/src/qingflow_mcp/cli/main.py +52 -17
- package/src/qingflow_mcp/server_app_builder.py +122 -190
- package/src/qingflow_mcp/server_app_user.py +63 -662
- package/src/qingflow_mcp/solution/executor.py +63 -4
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/ops/__init__.py +0 -3
- package/src/qingflow_mcp/ops/apps.py +0 -64
- package/src/qingflow_mcp/ops/auth.py +0 -121
- package/src/qingflow_mcp/ops/base.py +0 -290
- package/src/qingflow_mcp/ops/builder.py +0 -357
- package/src/qingflow_mcp/ops/context.py +0 -120
- package/src/qingflow_mcp/ops/directory.py +0 -171
- package/src/qingflow_mcp/ops/feedback.py +0 -49
- package/src/qingflow_mcp/ops/files.py +0 -78
- package/src/qingflow_mcp/ops/imports.py +0 -140
- package/src/qingflow_mcp/ops/records.py +0 -415
- package/src/qingflow_mcp/ops/tasks.py +0 -171
- package/src/qingflow_mcp/ops/workspace.py +0 -76
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from copy import deepcopy
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
@@ -129,6 +129,14 @@ class ResolvedApp:
|
|
|
129
129
|
tag_ids: list[int]
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
@dataclass(slots=True)
|
|
133
|
+
class PermissionCheckOutcome:
|
|
134
|
+
block: JSONObject | None = None
|
|
135
|
+
warnings: list[dict[str, Any]] = field(default_factory=list)
|
|
136
|
+
details: JSONObject = field(default_factory=dict)
|
|
137
|
+
verification: JSONObject = field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
|
|
132
140
|
class AiBuilderFacade:
|
|
133
141
|
def __init__(
|
|
134
142
|
self,
|
|
@@ -161,7 +169,18 @@ class AiBuilderFacade:
|
|
|
161
169
|
"package_name is required",
|
|
162
170
|
suggested_next_call=None,
|
|
163
171
|
)
|
|
164
|
-
|
|
172
|
+
normalized_args = {"package_name": requested}
|
|
173
|
+
try:
|
|
174
|
+
listing = self.packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
175
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
176
|
+
api_error = _coerce_api_error(error)
|
|
177
|
+
return _failed_from_api_error(
|
|
178
|
+
"PACKAGE_RESOLVE_FAILED",
|
|
179
|
+
api_error,
|
|
180
|
+
normalized_args=normalized_args,
|
|
181
|
+
details={"package_name": requested},
|
|
182
|
+
suggested_next_call={"tool_name": "package_list", "arguments": {"profile": profile, "trial_status": "all"}},
|
|
183
|
+
)
|
|
165
184
|
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
166
185
|
matches = [
|
|
167
186
|
{"tag_id": item.get("tagId"), "tag_name": item.get("tagName")}
|
|
@@ -212,6 +231,7 @@ class AiBuilderFacade:
|
|
|
212
231
|
suggested_next_call=None,
|
|
213
232
|
)
|
|
214
233
|
existing = self.package_resolve(profile=profile, package_name=requested)
|
|
234
|
+
lookup_permission_blocked = None
|
|
215
235
|
if existing.get("status") == "success":
|
|
216
236
|
return {
|
|
217
237
|
"status": "success",
|
|
@@ -231,6 +251,16 @@ class AiBuilderFacade:
|
|
|
231
251
|
}
|
|
232
252
|
if existing.get("error_code") == "AMBIGUOUS_PACKAGE":
|
|
233
253
|
return existing
|
|
254
|
+
if existing.get("error_code") == "PACKAGE_RESOLVE_FAILED":
|
|
255
|
+
if existing.get("backend_code") not in {40002, 40027}:
|
|
256
|
+
return existing
|
|
257
|
+
lookup_permission_blocked = {
|
|
258
|
+
"backend_code": existing.get("backend_code"),
|
|
259
|
+
"http_status": existing.get("http_status"),
|
|
260
|
+
"request_id": existing.get("request_id"),
|
|
261
|
+
}
|
|
262
|
+
elif existing.get("error_code") not in {"PACKAGE_NOT_FOUND"}:
|
|
263
|
+
return existing
|
|
234
264
|
try:
|
|
235
265
|
created = self.packages.package_create(profile=profile, payload={"tagName": requested})
|
|
236
266
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -239,7 +269,10 @@ class AiBuilderFacade:
|
|
|
239
269
|
"PACKAGE_CREATE_FAILED",
|
|
240
270
|
api_error,
|
|
241
271
|
normalized_args=normalized_args,
|
|
242
|
-
details={
|
|
272
|
+
details={
|
|
273
|
+
"package_name": requested,
|
|
274
|
+
**({"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {}),
|
|
275
|
+
},
|
|
243
276
|
suggested_next_call={"tool_name": "package_create", "arguments": {"profile": profile, "package_name": requested}},
|
|
244
277
|
)
|
|
245
278
|
result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
@@ -259,7 +292,7 @@ class AiBuilderFacade:
|
|
|
259
292
|
"normalized_args": normalized_args,
|
|
260
293
|
"missing_fields": [],
|
|
261
294
|
"allowed_values": {},
|
|
262
|
-
"details": {},
|
|
295
|
+
"details": {"lookup_permission_blocked": lookup_permission_blocked} if lookup_permission_blocked is not None else {},
|
|
263
296
|
"request_id": None,
|
|
264
297
|
"suggested_next_call": None
|
|
265
298
|
if verified
|
|
@@ -779,21 +812,53 @@ class AiBuilderFacade:
|
|
|
779
812
|
app_key: str,
|
|
780
813
|
app_title: str = "",
|
|
781
814
|
) -> JSONObject:
|
|
815
|
+
normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
|
|
782
816
|
if _coerce_positive_int(tag_id) is None:
|
|
783
817
|
return _failed("TAG_ID_REQUIRED", "tag_id must be positive", suggested_next_call=None)
|
|
784
|
-
|
|
818
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
819
|
+
package_permission_outcome = self._guard_package_permission(
|
|
785
820
|
profile=profile,
|
|
786
821
|
tag_id=tag_id,
|
|
787
822
|
required_permission="edit_app",
|
|
788
|
-
normalized_args=
|
|
823
|
+
normalized_args=normalized_args,
|
|
789
824
|
)
|
|
790
|
-
if
|
|
791
|
-
return
|
|
825
|
+
if package_permission_outcome.block is not None:
|
|
826
|
+
return package_permission_outcome.block
|
|
827
|
+
permission_outcomes.append(package_permission_outcome)
|
|
828
|
+
|
|
829
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
830
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
831
|
+
|
|
792
832
|
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
793
833
|
if resolved.get("status") == "failed":
|
|
794
|
-
return resolved
|
|
795
|
-
|
|
796
|
-
|
|
834
|
+
return finalize(resolved)
|
|
835
|
+
resolved_outcome = _permission_outcome_from_result(resolved)
|
|
836
|
+
if resolved_outcome is not None:
|
|
837
|
+
permission_outcomes.append(resolved_outcome)
|
|
838
|
+
try:
|
|
839
|
+
base_before = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
840
|
+
tag_ids_before = _coerce_int_list(base_before.get("tagIds"))
|
|
841
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
842
|
+
api_error = _coerce_api_error(error)
|
|
843
|
+
tag_ids_before = []
|
|
844
|
+
if _is_permission_restricted_api_error(api_error):
|
|
845
|
+
permission_outcomes.append(
|
|
846
|
+
_verification_read_outcome(
|
|
847
|
+
resource="app",
|
|
848
|
+
target={"app_key": app_key},
|
|
849
|
+
transport_error=api_error,
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
else:
|
|
853
|
+
return finalize(
|
|
854
|
+
_failed_from_api_error(
|
|
855
|
+
"PACKAGE_ATTACH_FAILED",
|
|
856
|
+
api_error,
|
|
857
|
+
normalized_args=normalized_args,
|
|
858
|
+
details={"tag_id": tag_id, "app_key": app_key},
|
|
859
|
+
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": tag_id, "app_key": app_key}},
|
|
860
|
+
)
|
|
861
|
+
)
|
|
797
862
|
already_attached = tag_id in tag_ids_before
|
|
798
863
|
try:
|
|
799
864
|
self._attach_app_to_package(
|
|
@@ -804,33 +869,72 @@ class AiBuilderFacade:
|
|
|
804
869
|
)
|
|
805
870
|
except (QingflowApiError, RuntimeError) as error:
|
|
806
871
|
api_error = _coerce_api_error(error)
|
|
807
|
-
return
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
872
|
+
return finalize(
|
|
873
|
+
_failed_from_api_error(
|
|
874
|
+
"PACKAGE_ATTACH_FAILED",
|
|
875
|
+
api_error,
|
|
876
|
+
normalized_args=normalized_args,
|
|
877
|
+
details=_with_state_read_blocked_details(
|
|
878
|
+
{"tag_id": tag_id, "app_key": app_key},
|
|
879
|
+
resource="package",
|
|
880
|
+
error=api_error,
|
|
881
|
+
),
|
|
882
|
+
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": tag_id, "app_key": app_key}},
|
|
883
|
+
)
|
|
812
884
|
)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
885
|
+
verification_error: QingflowApiError | None = None
|
|
886
|
+
try:
|
|
887
|
+
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
888
|
+
tag_ids_after = _coerce_int_list(base.get("tagIds"))
|
|
889
|
+
attached = tag_id in tag_ids_after
|
|
890
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
891
|
+
verification_error = _coerce_api_error(error)
|
|
892
|
+
tag_ids_after = []
|
|
893
|
+
attached = False
|
|
894
|
+
if _is_permission_restricted_api_error(verification_error):
|
|
895
|
+
permission_outcomes.append(
|
|
896
|
+
_verification_read_outcome(
|
|
897
|
+
resource="app",
|
|
898
|
+
target={"app_key": app_key},
|
|
899
|
+
transport_error=verification_error,
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
else:
|
|
903
|
+
return finalize(
|
|
904
|
+
_failed_from_api_error(
|
|
905
|
+
"PACKAGE_ATTACH_FAILED",
|
|
906
|
+
verification_error,
|
|
907
|
+
normalized_args=normalized_args,
|
|
908
|
+
details={"tag_id": tag_id, "app_key": app_key},
|
|
909
|
+
suggested_next_call={"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": tag_id, "app_key": app_key}},
|
|
910
|
+
)
|
|
911
|
+
)
|
|
912
|
+
response = {
|
|
817
913
|
"status": "success" if attached else "partial_success",
|
|
818
|
-
"error_code": None,
|
|
819
|
-
"recoverable":
|
|
820
|
-
"message": "attached app to package" if attached else "app attachment could not be verified",
|
|
821
|
-
"normalized_args":
|
|
914
|
+
"error_code": None if attached else "PACKAGE_ATTACH_READBACK_PENDING",
|
|
915
|
+
"recoverable": verification_error is not None or not attached,
|
|
916
|
+
"message": "attached app to package" if attached else "app attachment could not be fully verified",
|
|
917
|
+
"normalized_args": normalized_args,
|
|
822
918
|
"missing_fields": [],
|
|
823
919
|
"allowed_values": {},
|
|
824
920
|
"details": {},
|
|
825
|
-
"request_id": None,
|
|
921
|
+
"request_id": verification_error.request_id if verification_error is not None else None,
|
|
826
922
|
"suggested_next_call": None if attached else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": tag_id, "app_key": app_key}},
|
|
827
923
|
"noop": already_attached,
|
|
828
|
-
"verification": {
|
|
924
|
+
"verification": {
|
|
925
|
+
"tag_ids_before": tag_ids_before,
|
|
926
|
+
"tag_ids_after": tag_ids_after,
|
|
927
|
+
"attachment_verified": attached if verification_error is None else None,
|
|
928
|
+
"readback_unavailable": verification_error is not None,
|
|
929
|
+
},
|
|
829
930
|
"app_key": app_key,
|
|
830
931
|
"tag_id": tag_id,
|
|
831
932
|
"tag_ids_after": tag_ids_after,
|
|
832
933
|
"attached": attached,
|
|
833
934
|
}
|
|
935
|
+
if verification_error is not None:
|
|
936
|
+
response["details"]["verification_error"] = _transport_error_payload(verification_error)
|
|
937
|
+
return finalize(response)
|
|
834
938
|
|
|
835
939
|
def app_release_edit_lock_if_mine(
|
|
836
940
|
self,
|
|
@@ -954,6 +1058,42 @@ class AiBuilderFacade:
|
|
|
954
1058
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
955
1059
|
except (QingflowApiError, RuntimeError) as exc:
|
|
956
1060
|
api_error = _coerce_api_error(exc)
|
|
1061
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1062
|
+
return {
|
|
1063
|
+
"status": "success",
|
|
1064
|
+
"error_code": None,
|
|
1065
|
+
"recoverable": False,
|
|
1066
|
+
"message": "resolved app key; metadata unverified",
|
|
1067
|
+
"normalized_args": {"app_key": app_key},
|
|
1068
|
+
"missing_fields": [],
|
|
1069
|
+
"allowed_values": {},
|
|
1070
|
+
"details": {
|
|
1071
|
+
"match_scope": "app_key",
|
|
1072
|
+
"lookup_permission_blocked": {
|
|
1073
|
+
"scope": "app",
|
|
1074
|
+
"target": {"app_key": app_key},
|
|
1075
|
+
"required_permission": None,
|
|
1076
|
+
"transport_error": _transport_error_payload(api_error),
|
|
1077
|
+
},
|
|
1078
|
+
"permission_check_skipped": True,
|
|
1079
|
+
},
|
|
1080
|
+
"request_id": api_error.request_id,
|
|
1081
|
+
"suggested_next_call": {"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1082
|
+
"noop": False,
|
|
1083
|
+
"warnings": [
|
|
1084
|
+
_warning(
|
|
1085
|
+
"PERMISSION_CHECK_SKIPPED",
|
|
1086
|
+
"app metadata lookup was permission-restricted; continuing with explicit app_key",
|
|
1087
|
+
scope="app",
|
|
1088
|
+
app_key=app_key,
|
|
1089
|
+
)
|
|
1090
|
+
],
|
|
1091
|
+
"verification": {"metadata_unverified": True},
|
|
1092
|
+
"app_key": app_key,
|
|
1093
|
+
"app_name": app_key,
|
|
1094
|
+
"tag_ids": [],
|
|
1095
|
+
"publish_status": None,
|
|
1096
|
+
}
|
|
957
1097
|
return _failed_from_api_error(
|
|
958
1098
|
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
959
1099
|
api_error,
|
|
@@ -1012,7 +1152,20 @@ class AiBuilderFacade:
|
|
|
1012
1152
|
details={"app_name": requested, "package_tag_id": package_tag_id, "matches": package_matches},
|
|
1013
1153
|
suggested_next_call=None,
|
|
1014
1154
|
)
|
|
1015
|
-
|
|
1155
|
+
search_error: QingflowApiError | None = None
|
|
1156
|
+
try:
|
|
1157
|
+
search = self.apps.app_search(profile=profile, keyword=requested, page_num=1, page_size=200)
|
|
1158
|
+
except (QingflowApiError, RuntimeError) as exc:
|
|
1159
|
+
api_error = _coerce_api_error(exc)
|
|
1160
|
+
if package_tag_id is None or package_tag_id <= 0 or api_error.backend_code not in {40002, 40027}:
|
|
1161
|
+
return _failed_from_api_error(
|
|
1162
|
+
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
1163
|
+
api_error,
|
|
1164
|
+
details={"app_name": requested, "package_tag_id": package_tag_id},
|
|
1165
|
+
suggested_next_call=None,
|
|
1166
|
+
)
|
|
1167
|
+
search = {}
|
|
1168
|
+
search_error = api_error
|
|
1016
1169
|
apps = search.get("apps") if isinstance(search.get("apps"), list) else []
|
|
1017
1170
|
matches = []
|
|
1018
1171
|
for item in apps:
|
|
@@ -1046,11 +1199,72 @@ class AiBuilderFacade:
|
|
|
1046
1199
|
"tag_ids": tag_ids,
|
|
1047
1200
|
}
|
|
1048
1201
|
)
|
|
1202
|
+
if not matches and package_tag_id is not None and package_tag_id > 0 and search_error is not None:
|
|
1203
|
+
visible_matches = self._resolve_app_matches_in_visible_apps(
|
|
1204
|
+
profile=profile,
|
|
1205
|
+
app_name=requested,
|
|
1206
|
+
package_tag_id=package_tag_id,
|
|
1207
|
+
)
|
|
1208
|
+
if len(visible_matches) == 1:
|
|
1209
|
+
match = visible_matches[0]
|
|
1210
|
+
return {
|
|
1211
|
+
"status": "success",
|
|
1212
|
+
"error_code": None,
|
|
1213
|
+
"recoverable": False,
|
|
1214
|
+
"message": "resolved app",
|
|
1215
|
+
"normalized_args": {"app_name": requested, "package_tag_id": package_tag_id},
|
|
1216
|
+
"missing_fields": [],
|
|
1217
|
+
"allowed_values": {},
|
|
1218
|
+
"details": {
|
|
1219
|
+
"match_scope": "visible_apps",
|
|
1220
|
+
"search_permission_blocked": {
|
|
1221
|
+
"backend_code": search_error.backend_code,
|
|
1222
|
+
"http_status": search_error.http_status,
|
|
1223
|
+
"request_id": search_error.request_id,
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
"request_id": None,
|
|
1227
|
+
"suggested_next_call": None,
|
|
1228
|
+
"noop": False,
|
|
1229
|
+
"verification": {},
|
|
1230
|
+
**match,
|
|
1231
|
+
}
|
|
1232
|
+
if len(visible_matches) > 1:
|
|
1233
|
+
return _failed(
|
|
1234
|
+
"AMBIGUOUS_APP",
|
|
1235
|
+
f"multiple apps matched '{requested}' inside package {package_tag_id}",
|
|
1236
|
+
details={
|
|
1237
|
+
"app_name": requested,
|
|
1238
|
+
"package_tag_id": package_tag_id,
|
|
1239
|
+
"matches": visible_matches,
|
|
1240
|
+
"search_permission_blocked": {
|
|
1241
|
+
"backend_code": search_error.backend_code,
|
|
1242
|
+
"http_status": search_error.http_status,
|
|
1243
|
+
"request_id": search_error.request_id,
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
suggested_next_call=None,
|
|
1247
|
+
)
|
|
1049
1248
|
if not matches:
|
|
1050
1249
|
return _failed(
|
|
1051
1250
|
"APP_NOT_FOUND",
|
|
1052
1251
|
f"app '{requested}' was not found",
|
|
1053
|
-
details={
|
|
1252
|
+
details={
|
|
1253
|
+
"app_name": requested,
|
|
1254
|
+
"package_tag_id": package_tag_id,
|
|
1255
|
+
**(
|
|
1256
|
+
{
|
|
1257
|
+
"search_permission_blocked": {
|
|
1258
|
+
"backend_code": search_error.backend_code,
|
|
1259
|
+
"http_status": search_error.http_status,
|
|
1260
|
+
"request_id": search_error.request_id,
|
|
1261
|
+
},
|
|
1262
|
+
"match_scope": "visible_apps_fallback",
|
|
1263
|
+
}
|
|
1264
|
+
if search_error is not None
|
|
1265
|
+
else {}
|
|
1266
|
+
),
|
|
1267
|
+
},
|
|
1054
1268
|
suggested_next_call=None,
|
|
1055
1269
|
)
|
|
1056
1270
|
if len(matches) > 1:
|
|
@@ -1077,6 +1291,39 @@ class AiBuilderFacade:
|
|
|
1077
1291
|
**match,
|
|
1078
1292
|
}
|
|
1079
1293
|
|
|
1294
|
+
def _resolve_app_matches_in_visible_apps(
|
|
1295
|
+
self,
|
|
1296
|
+
*,
|
|
1297
|
+
profile: str,
|
|
1298
|
+
app_name: str,
|
|
1299
|
+
package_tag_id: int,
|
|
1300
|
+
) -> list[JSONObject]:
|
|
1301
|
+
try:
|
|
1302
|
+
listing = self.apps.app_list(profile=profile, ship_auth=False)
|
|
1303
|
+
except (QingflowApiError, RuntimeError):
|
|
1304
|
+
return []
|
|
1305
|
+
items = listing.get("items") if isinstance(listing.get("items"), list) else []
|
|
1306
|
+
matches: list[JSONObject] = []
|
|
1307
|
+
seen_app_keys: set[str] = set()
|
|
1308
|
+
for item in items:
|
|
1309
|
+
if not isinstance(item, dict):
|
|
1310
|
+
continue
|
|
1311
|
+
title = str(item.get("title") or item.get("app_name") or "").strip()
|
|
1312
|
+
if title != app_name:
|
|
1313
|
+
continue
|
|
1314
|
+
candidate_key = str(item.get("app_key") or item.get("appKey") or "").strip()
|
|
1315
|
+
if not candidate_key or candidate_key in seen_app_keys:
|
|
1316
|
+
continue
|
|
1317
|
+
tag_ids = _coerce_int_list(item.get("tag_ids"))
|
|
1318
|
+
tag_id = _coerce_positive_int(item.get("tag_id"))
|
|
1319
|
+
if tag_id is not None and tag_id not in tag_ids:
|
|
1320
|
+
tag_ids.append(tag_id)
|
|
1321
|
+
if package_tag_id not in tag_ids:
|
|
1322
|
+
continue
|
|
1323
|
+
seen_app_keys.add(candidate_key)
|
|
1324
|
+
matches.append({"app_key": candidate_key, "app_name": title, "tag_ids": tag_ids})
|
|
1325
|
+
return matches
|
|
1326
|
+
|
|
1080
1327
|
def _resolve_app_matches_in_package(
|
|
1081
1328
|
self,
|
|
1082
1329
|
*,
|
|
@@ -1153,67 +1400,73 @@ class AiBuilderFacade:
|
|
|
1153
1400
|
app_key: str,
|
|
1154
1401
|
required_permission: str,
|
|
1155
1402
|
normalized_args: JSONObject,
|
|
1156
|
-
) ->
|
|
1403
|
+
) -> PermissionCheckOutcome:
|
|
1157
1404
|
try:
|
|
1158
1405
|
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
1159
1406
|
except (QingflowApiError, RuntimeError) as error:
|
|
1160
1407
|
api_error = _coerce_api_error(error)
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1408
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1409
|
+
return _permission_skip_outcome(
|
|
1410
|
+
scope="app",
|
|
1411
|
+
target={"app_key": app_key},
|
|
1412
|
+
required_permission=required_permission,
|
|
1413
|
+
transport_error=_transport_error_payload(api_error),
|
|
1414
|
+
)
|
|
1415
|
+
return PermissionCheckOutcome(
|
|
1416
|
+
block=_failed(
|
|
1417
|
+
"APP_PERMISSION_UNVERIFIED",
|
|
1418
|
+
"could not confirm current user's builder permissions for this app",
|
|
1419
|
+
normalized_args=normalized_args,
|
|
1420
|
+
details={
|
|
1421
|
+
"app_key": app_key,
|
|
1422
|
+
"required_permission": required_permission,
|
|
1423
|
+
"permission_read_error": {
|
|
1424
|
+
"message": api_error.message,
|
|
1425
|
+
"http_status": api_error.http_status,
|
|
1426
|
+
"backend_code": api_error.backend_code,
|
|
1427
|
+
"category": api_error.category,
|
|
1428
|
+
},
|
|
1173
1429
|
},
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1430
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1431
|
+
request_id=api_error.request_id,
|
|
1432
|
+
backend_code=api_error.backend_code,
|
|
1433
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
1434
|
+
)
|
|
1179
1435
|
)
|
|
1180
1436
|
permission_key = {
|
|
1181
1437
|
"edit_app": "can_edit_app",
|
|
1182
1438
|
"data_manage": "can_manage_data",
|
|
1183
1439
|
}.get(required_permission)
|
|
1184
1440
|
if permission_key is None:
|
|
1185
|
-
return
|
|
1441
|
+
return PermissionCheckOutcome()
|
|
1186
1442
|
permission_value = permission_summary.get(permission_key)
|
|
1187
1443
|
if permission_value is None:
|
|
1188
|
-
return
|
|
1189
|
-
"
|
|
1190
|
-
"
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
"app_key": app_key,
|
|
1194
|
-
"required_permission": required_permission,
|
|
1195
|
-
"permission_summary": permission_summary,
|
|
1196
|
-
},
|
|
1197
|
-
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1444
|
+
return _permission_skip_outcome(
|
|
1445
|
+
scope="app",
|
|
1446
|
+
target={"app_key": app_key},
|
|
1447
|
+
required_permission=required_permission,
|
|
1448
|
+
permission_summary=permission_summary,
|
|
1198
1449
|
)
|
|
1199
1450
|
if permission_value is not False:
|
|
1200
|
-
return
|
|
1451
|
+
return PermissionCheckOutcome()
|
|
1201
1452
|
error_code = "EDIT_APP_UNAUTHORIZED" if required_permission == "edit_app" else "DATA_MANAGE_UNAUTHORIZED"
|
|
1202
1453
|
message = (
|
|
1203
1454
|
"current user does not have builder edit-app permission on this app"
|
|
1204
1455
|
if required_permission == "edit_app"
|
|
1205
1456
|
else "current user does not have data-management permission on this app"
|
|
1206
1457
|
)
|
|
1207
|
-
return
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1458
|
+
return PermissionCheckOutcome(
|
|
1459
|
+
block=_failed(
|
|
1460
|
+
error_code,
|
|
1461
|
+
message,
|
|
1462
|
+
normalized_args=normalized_args,
|
|
1463
|
+
details={
|
|
1464
|
+
"app_key": app_key,
|
|
1465
|
+
"required_permission": required_permission,
|
|
1466
|
+
"permission_summary": permission_summary,
|
|
1467
|
+
},
|
|
1468
|
+
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
1469
|
+
)
|
|
1217
1470
|
)
|
|
1218
1471
|
|
|
1219
1472
|
def _guard_package_permission(
|
|
@@ -1223,29 +1476,38 @@ class AiBuilderFacade:
|
|
|
1223
1476
|
tag_id: int,
|
|
1224
1477
|
required_permission: str,
|
|
1225
1478
|
normalized_args: JSONObject,
|
|
1226
|
-
) ->
|
|
1479
|
+
) -> PermissionCheckOutcome:
|
|
1227
1480
|
try:
|
|
1228
1481
|
permission_summary = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
|
|
1229
1482
|
except (QingflowApiError, RuntimeError) as error:
|
|
1230
1483
|
api_error = _coerce_api_error(error)
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1484
|
+
if _is_permission_restricted_api_error(api_error):
|
|
1485
|
+
return _permission_skip_outcome(
|
|
1486
|
+
scope="package",
|
|
1487
|
+
target={"tag_id": tag_id},
|
|
1488
|
+
required_permission=required_permission,
|
|
1489
|
+
transport_error=_transport_error_payload(api_error),
|
|
1490
|
+
)
|
|
1491
|
+
return PermissionCheckOutcome(
|
|
1492
|
+
block=_failed(
|
|
1493
|
+
"PACKAGE_PERMISSION_UNVERIFIED",
|
|
1494
|
+
"could not confirm current user's builder permissions for this package",
|
|
1495
|
+
normalized_args=normalized_args,
|
|
1496
|
+
details={
|
|
1497
|
+
"tag_id": tag_id,
|
|
1498
|
+
"required_permission": required_permission,
|
|
1499
|
+
"permission_read_error": {
|
|
1500
|
+
"message": api_error.message,
|
|
1501
|
+
"http_status": api_error.http_status,
|
|
1502
|
+
"backend_code": api_error.backend_code,
|
|
1503
|
+
"category": api_error.category,
|
|
1504
|
+
},
|
|
1243
1505
|
},
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1506
|
+
suggested_next_call={"tool_name": "package_get_base", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
1507
|
+
request_id=api_error.request_id,
|
|
1508
|
+
backend_code=api_error.backend_code,
|
|
1509
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
1510
|
+
)
|
|
1249
1511
|
)
|
|
1250
1512
|
permission_specs = {
|
|
1251
1513
|
"add_app": (
|
|
@@ -1266,12 +1528,21 @@ class AiBuilderFacade:
|
|
|
1266
1528
|
}
|
|
1267
1529
|
permission_key, error_code, message = permission_specs.get(required_permission, (None, None, None))
|
|
1268
1530
|
if permission_key is None:
|
|
1269
|
-
return
|
|
1531
|
+
return PermissionCheckOutcome()
|
|
1270
1532
|
permission_value = permission_summary.get(permission_key)
|
|
1271
1533
|
if permission_value is None:
|
|
1272
|
-
return
|
|
1273
|
-
"
|
|
1274
|
-
"
|
|
1534
|
+
return _permission_skip_outcome(
|
|
1535
|
+
scope="package",
|
|
1536
|
+
target={"tag_id": tag_id},
|
|
1537
|
+
required_permission=required_permission,
|
|
1538
|
+
permission_summary=permission_summary,
|
|
1539
|
+
)
|
|
1540
|
+
if permission_value is not False:
|
|
1541
|
+
return PermissionCheckOutcome()
|
|
1542
|
+
return PermissionCheckOutcome(
|
|
1543
|
+
block=_failed(
|
|
1544
|
+
error_code or "PACKAGE_PERMISSION_UNVERIFIED",
|
|
1545
|
+
message or "current user does not have the required package permission",
|
|
1275
1546
|
normalized_args=normalized_args,
|
|
1276
1547
|
details={
|
|
1277
1548
|
"tag_id": tag_id,
|
|
@@ -1280,18 +1551,6 @@ class AiBuilderFacade:
|
|
|
1280
1551
|
},
|
|
1281
1552
|
suggested_next_call={"tool_name": "package_get_base", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
1282
1553
|
)
|
|
1283
|
-
if permission_value is not False:
|
|
1284
|
-
return None
|
|
1285
|
-
return _failed(
|
|
1286
|
-
error_code or "PACKAGE_PERMISSION_UNVERIFIED",
|
|
1287
|
-
message or "current user does not have the required package permission",
|
|
1288
|
-
normalized_args=normalized_args,
|
|
1289
|
-
details={
|
|
1290
|
-
"tag_id": tag_id,
|
|
1291
|
-
"required_permission": required_permission,
|
|
1292
|
-
"permission_summary": permission_summary,
|
|
1293
|
-
},
|
|
1294
|
-
suggested_next_call={"tool_name": "package_get_base", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
1295
1554
|
)
|
|
1296
1555
|
|
|
1297
1556
|
def _guard_portal_permission(
|
|
@@ -1301,25 +1560,26 @@ class AiBuilderFacade:
|
|
|
1301
1560
|
dash_key: str,
|
|
1302
1561
|
normalized_args: JSONObject,
|
|
1303
1562
|
portal_result: dict[str, Any],
|
|
1304
|
-
) ->
|
|
1563
|
+
) -> PermissionCheckOutcome:
|
|
1305
1564
|
permission_summary = self._read_portal_permission_summary(dash_key=dash_key, portal_result=portal_result)
|
|
1306
1565
|
permission_value = permission_summary.get("can_edit_portal")
|
|
1307
1566
|
if permission_value is None:
|
|
1308
|
-
return
|
|
1309
|
-
"
|
|
1310
|
-
"
|
|
1567
|
+
return _permission_skip_outcome(
|
|
1568
|
+
scope="portal",
|
|
1569
|
+
target={"dash_key": dash_key},
|
|
1570
|
+
required_permission="edit_portal",
|
|
1571
|
+
permission_summary=permission_summary,
|
|
1572
|
+
)
|
|
1573
|
+
if permission_value is not False:
|
|
1574
|
+
return PermissionCheckOutcome()
|
|
1575
|
+
return PermissionCheckOutcome(
|
|
1576
|
+
block=_failed(
|
|
1577
|
+
"PORTAL_EDIT_UNAUTHORIZED",
|
|
1578
|
+
"current user does not have builder edit permission on this portal",
|
|
1311
1579
|
normalized_args=normalized_args,
|
|
1312
1580
|
details={"dash_key": dash_key, "permission_summary": permission_summary},
|
|
1313
1581
|
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
1314
1582
|
)
|
|
1315
|
-
if permission_value is not False:
|
|
1316
|
-
return None
|
|
1317
|
-
return _failed(
|
|
1318
|
-
"PORTAL_EDIT_UNAUTHORIZED",
|
|
1319
|
-
"current user does not have builder edit permission on this portal",
|
|
1320
|
-
normalized_args=normalized_args,
|
|
1321
|
-
details={"dash_key": dash_key, "permission_summary": permission_summary},
|
|
1322
|
-
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
1323
1583
|
)
|
|
1324
1584
|
|
|
1325
1585
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -2094,6 +2354,7 @@ class AiBuilderFacade:
|
|
|
2094
2354
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
2095
2355
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
2096
2356
|
}
|
|
2357
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
2097
2358
|
requested_field_changes = bool(add_fields or update_fields or remove_fields)
|
|
2098
2359
|
resolved: JSONObject
|
|
2099
2360
|
if app_key:
|
|
@@ -2102,31 +2363,37 @@ class AiBuilderFacade:
|
|
|
2102
2363
|
resolved = self.app_resolve(profile=profile, app_name=app_name, package_tag_id=package_tag_id)
|
|
2103
2364
|
else:
|
|
2104
2365
|
return _failed("APP_NAME_REQUIRED", "app_name or app_key is required", normalized_args=normalized_args, suggested_next_call=None)
|
|
2366
|
+
|
|
2367
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
2368
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
2369
|
+
|
|
2105
2370
|
if resolved.get("status") == "failed":
|
|
2106
2371
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
2107
2372
|
resolved["normalized_args"] = normalized_args
|
|
2108
2373
|
if not create_if_missing or app_key or resolved.get("error_code") != "APP_NOT_FOUND":
|
|
2109
|
-
return resolved
|
|
2374
|
+
return finalize(resolved)
|
|
2110
2375
|
permission_tag_id = _coerce_positive_int(package_tag_id)
|
|
2111
2376
|
if permission_tag_id is None:
|
|
2112
2377
|
permission_tag_id = 0
|
|
2113
|
-
|
|
2378
|
+
add_permission_outcome = self._guard_package_permission(
|
|
2114
2379
|
profile=profile,
|
|
2115
2380
|
tag_id=permission_tag_id,
|
|
2116
2381
|
required_permission="add_app",
|
|
2117
2382
|
normalized_args=normalized_args,
|
|
2118
2383
|
)
|
|
2119
|
-
if
|
|
2120
|
-
return
|
|
2384
|
+
if add_permission_outcome.block is not None:
|
|
2385
|
+
return add_permission_outcome.block
|
|
2386
|
+
permission_outcomes.append(add_permission_outcome)
|
|
2121
2387
|
if requested_field_changes:
|
|
2122
|
-
|
|
2388
|
+
edit_permission_outcome = self._guard_package_permission(
|
|
2123
2389
|
profile=profile,
|
|
2124
2390
|
tag_id=permission_tag_id,
|
|
2125
2391
|
required_permission="edit_app",
|
|
2126
2392
|
normalized_args=normalized_args,
|
|
2127
2393
|
)
|
|
2128
|
-
if
|
|
2129
|
-
return
|
|
2394
|
+
if edit_permission_outcome.block is not None:
|
|
2395
|
+
return edit_permission_outcome.block
|
|
2396
|
+
permission_outcomes.append(edit_permission_outcome)
|
|
2130
2397
|
resolved = self._create_target_app_shell(
|
|
2131
2398
|
profile=profile,
|
|
2132
2399
|
app_name=app_name,
|
|
@@ -2135,23 +2402,27 @@ class AiBuilderFacade:
|
|
|
2135
2402
|
if resolved.get("status") == "failed":
|
|
2136
2403
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
2137
2404
|
resolved["normalized_args"] = normalized_args
|
|
2138
|
-
return resolved
|
|
2405
|
+
return finalize(resolved)
|
|
2406
|
+
resolved_outcome = _permission_outcome_from_result(resolved)
|
|
2407
|
+
if resolved_outcome is not None:
|
|
2408
|
+
permission_outcomes.append(resolved_outcome)
|
|
2139
2409
|
target = ResolvedApp(
|
|
2140
2410
|
app_key=str(resolved["app_key"]),
|
|
2141
2411
|
app_name=str(resolved["app_name"]),
|
|
2142
2412
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
2143
2413
|
)
|
|
2144
2414
|
if not bool(resolved.get("created")):
|
|
2145
|
-
|
|
2415
|
+
permission_outcome = self._guard_app_permission(
|
|
2146
2416
|
profile=profile,
|
|
2147
2417
|
app_key=target.app_key,
|
|
2148
2418
|
required_permission="edit_app",
|
|
2149
2419
|
normalized_args=normalized_args,
|
|
2150
2420
|
)
|
|
2151
|
-
if
|
|
2152
|
-
return
|
|
2421
|
+
if permission_outcome.block is not None:
|
|
2422
|
+
return permission_outcome.block
|
|
2423
|
+
permission_outcomes.append(permission_outcome)
|
|
2153
2424
|
if bool(resolved.get("created")) and not requested_field_changes:
|
|
2154
|
-
return {
|
|
2425
|
+
return finalize({
|
|
2155
2426
|
"status": "success",
|
|
2156
2427
|
"error_code": None,
|
|
2157
2428
|
"recoverable": False,
|
|
@@ -2178,21 +2449,21 @@ class AiBuilderFacade:
|
|
|
2178
2449
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
2179
2450
|
"publish_requested": False,
|
|
2180
2451
|
"published": False,
|
|
2181
|
-
}
|
|
2452
|
+
})
|
|
2182
2453
|
schema_readback_delayed = False
|
|
2183
2454
|
try:
|
|
2184
2455
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
|
|
2185
2456
|
except (QingflowApiError, RuntimeError) as error:
|
|
2186
2457
|
api_error = _coerce_api_error(error)
|
|
2187
2458
|
if not bool(resolved.get("created")) or api_error.http_status != 404:
|
|
2188
|
-
return _failed_from_api_error(
|
|
2459
|
+
return finalize(_failed_from_api_error(
|
|
2189
2460
|
"SCHEMA_READBACK_FAILED",
|
|
2190
2461
|
api_error,
|
|
2191
2462
|
normalized_args=normalized_args,
|
|
2192
2463
|
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
2193
|
-
details={"app_key": target.app_key},
|
|
2464
|
+
details=_with_state_read_blocked_details({"app_key": target.app_key}, resource="schema", error=api_error),
|
|
2194
2465
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
2195
|
-
)
|
|
2466
|
+
))
|
|
2196
2467
|
schema_result = _empty_schema_result(target.app_name)
|
|
2197
2468
|
_schema_source = "synthetic_new_app"
|
|
2198
2469
|
schema_readback_delayed = True
|
|
@@ -2310,7 +2581,7 @@ class AiBuilderFacade:
|
|
|
2310
2581
|
"package_attached": package_attached,
|
|
2311
2582
|
}
|
|
2312
2583
|
response["details"]["relation_field_count"] = relation_field_count
|
|
2313
|
-
return self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response)
|
|
2584
|
+
return finalize(self._append_publish_result(profile=profile, app_key=target.app_key, publish=publish, response=response))
|
|
2314
2585
|
|
|
2315
2586
|
payload = _build_form_payload_from_fields(
|
|
2316
2587
|
title=schema_result.get("formTitle") or target.app_name,
|
|
@@ -2449,7 +2720,7 @@ class AiBuilderFacade:
|
|
|
2449
2720
|
"http_status": verification_error.http_status,
|
|
2450
2721
|
"backend_code": verification_error.backend_code,
|
|
2451
2722
|
}
|
|
2452
|
-
return response
|
|
2723
|
+
return finalize(response)
|
|
2453
2724
|
|
|
2454
2725
|
def app_layout_apply(
|
|
2455
2726
|
self,
|
|
@@ -2467,25 +2738,31 @@ class AiBuilderFacade:
|
|
|
2467
2738
|
"sections": requested_sections,
|
|
2468
2739
|
"publish": publish,
|
|
2469
2740
|
}
|
|
2470
|
-
|
|
2741
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
2742
|
+
permission_outcome = self._guard_app_permission(
|
|
2471
2743
|
profile=profile,
|
|
2472
2744
|
app_key=app_key,
|
|
2473
2745
|
required_permission="edit_app",
|
|
2474
2746
|
normalized_args=normalized_args,
|
|
2475
2747
|
)
|
|
2476
|
-
if
|
|
2477
|
-
return
|
|
2748
|
+
if permission_outcome.block is not None:
|
|
2749
|
+
return permission_outcome.block
|
|
2750
|
+
permission_outcomes.append(permission_outcome)
|
|
2751
|
+
|
|
2752
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
2753
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
2754
|
+
|
|
2478
2755
|
try:
|
|
2479
2756
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2480
2757
|
except (QingflowApiError, RuntimeError) as error:
|
|
2481
2758
|
api_error = _coerce_api_error(error)
|
|
2482
|
-
return _failed_from_api_error(
|
|
2759
|
+
return finalize(_failed_from_api_error(
|
|
2483
2760
|
"LAYOUT_READ_FAILED",
|
|
2484
2761
|
api_error,
|
|
2485
2762
|
normalized_args=normalized_args,
|
|
2486
|
-
details={"app_key": app_key},
|
|
2763
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
|
|
2487
2764
|
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2488
|
-
)
|
|
2765
|
+
))
|
|
2489
2766
|
parsed = _parse_schema(schema_result)
|
|
2490
2767
|
current_fields = parsed["fields"]
|
|
2491
2768
|
requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
|
|
@@ -2577,7 +2854,7 @@ class AiBuilderFacade:
|
|
|
2577
2854
|
},
|
|
2578
2855
|
"verified": True,
|
|
2579
2856
|
}
|
|
2580
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
2857
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
2581
2858
|
payload = _build_form_payload_from_existing_schema(
|
|
2582
2859
|
current_schema=schema_result,
|
|
2583
2860
|
layout=target_layout,
|
|
@@ -2610,7 +2887,7 @@ class AiBuilderFacade:
|
|
|
2610
2887
|
fallback_applied = "flatten_sections"
|
|
2611
2888
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
2612
2889
|
api_fallback_error = _coerce_api_error(fallback_error)
|
|
2613
|
-
return _failed_from_api_error(
|
|
2890
|
+
return finalize(_failed_from_api_error(
|
|
2614
2891
|
"LAYOUT_APPLY_FAILED",
|
|
2615
2892
|
api_fallback_error,
|
|
2616
2893
|
normalized_args=normalized_args,
|
|
@@ -2623,9 +2900,9 @@ class AiBuilderFacade:
|
|
|
2623
2900
|
"fallback_layout": flattened_layout,
|
|
2624
2901
|
},
|
|
2625
2902
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2626
|
-
)
|
|
2903
|
+
))
|
|
2627
2904
|
else:
|
|
2628
|
-
return _failed_from_api_error(
|
|
2905
|
+
return finalize(_failed_from_api_error(
|
|
2629
2906
|
"LAYOUT_APPLY_FAILED",
|
|
2630
2907
|
api_error,
|
|
2631
2908
|
normalized_args=normalized_args,
|
|
@@ -2636,7 +2913,7 @@ class AiBuilderFacade:
|
|
|
2636
2913
|
"current_field_names": [field["name"] for field in current_fields],
|
|
2637
2914
|
},
|
|
2638
2915
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2639
|
-
)
|
|
2916
|
+
))
|
|
2640
2917
|
try:
|
|
2641
2918
|
verified_schema, _verified_schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2642
2919
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -2666,7 +2943,7 @@ class AiBuilderFacade:
|
|
|
2666
2943
|
"verified": False,
|
|
2667
2944
|
}
|
|
2668
2945
|
response["request_id"] = api_error.request_id
|
|
2669
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
2946
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
2670
2947
|
verified_layout = _parse_schema(verified_schema)["layout"]
|
|
2671
2948
|
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
2672
2949
|
raw_layout_has_content = _schema_has_layout_content(verified_schema)
|
|
@@ -2716,7 +2993,7 @@ class AiBuilderFacade:
|
|
|
2716
2993
|
},
|
|
2717
2994
|
"verified": layout_verified,
|
|
2718
2995
|
}
|
|
2719
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
2996
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
2720
2997
|
|
|
2721
2998
|
def app_flow_apply(
|
|
2722
2999
|
self,
|
|
@@ -2735,34 +3012,40 @@ class AiBuilderFacade:
|
|
|
2735
3012
|
"transitions": transitions,
|
|
2736
3013
|
"publish": publish,
|
|
2737
3014
|
}
|
|
2738
|
-
|
|
3015
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3016
|
+
permission_outcome = self._guard_app_permission(
|
|
2739
3017
|
profile=profile,
|
|
2740
3018
|
app_key=app_key,
|
|
2741
3019
|
required_permission="data_manage",
|
|
2742
3020
|
normalized_args=normalized_args,
|
|
2743
3021
|
)
|
|
2744
|
-
if
|
|
2745
|
-
return
|
|
3022
|
+
if permission_outcome.block is not None:
|
|
3023
|
+
return permission_outcome.block
|
|
3024
|
+
permission_outcomes.append(permission_outcome)
|
|
3025
|
+
|
|
3026
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3027
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3028
|
+
|
|
2746
3029
|
if mode != "replace":
|
|
2747
|
-
return _failed(
|
|
3030
|
+
return finalize(_failed(
|
|
2748
3031
|
"UNSUPPORTED_FLOW_MODE",
|
|
2749
3032
|
"only mode='replace' is supported",
|
|
2750
3033
|
normalized_args=normalized_args,
|
|
2751
3034
|
allowed_values={"modes": ["replace"]},
|
|
2752
3035
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2753
|
-
)
|
|
3036
|
+
))
|
|
2754
3037
|
try:
|
|
2755
3038
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
2756
3039
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2757
3040
|
except (QingflowApiError, RuntimeError) as error:
|
|
2758
3041
|
api_error = _coerce_api_error(error)
|
|
2759
|
-
return _failed_from_api_error(
|
|
3042
|
+
return finalize(_failed_from_api_error(
|
|
2760
3043
|
"FLOW_READ_FAILED",
|
|
2761
3044
|
api_error,
|
|
2762
3045
|
normalized_args=normalized_args,
|
|
2763
|
-
details={"app_key": app_key},
|
|
3046
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
2764
3047
|
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2765
|
-
)
|
|
3048
|
+
))
|
|
2766
3049
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
2767
3050
|
current_fields = _parse_schema(schema)["fields"]
|
|
2768
3051
|
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
@@ -2874,7 +3157,7 @@ class AiBuilderFacade:
|
|
|
2874
3157
|
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
2875
3158
|
suggested_next_call["arguments"] = arguments
|
|
2876
3159
|
failed["suggested_next_call"] = suggested_next_call
|
|
2877
|
-
return failed
|
|
3160
|
+
return finalize(failed)
|
|
2878
3161
|
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
2879
3162
|
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
2880
3163
|
current_workflow=verified_nodes,
|
|
@@ -2933,7 +3216,7 @@ class AiBuilderFacade:
|
|
|
2933
3216
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
2934
3217
|
"verified": workflow_verified,
|
|
2935
3218
|
}
|
|
2936
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3219
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
2937
3220
|
|
|
2938
3221
|
def app_views_apply(
|
|
2939
3222
|
self,
|
|
@@ -2970,27 +3253,33 @@ class AiBuilderFacade:
|
|
|
2970
3253
|
"verified": True,
|
|
2971
3254
|
}
|
|
2972
3255
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
2973
|
-
|
|
3256
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3257
|
+
permission_outcome = self._guard_app_permission(
|
|
2974
3258
|
profile=profile,
|
|
2975
3259
|
app_key=app_key,
|
|
2976
3260
|
required_permission="data_manage",
|
|
2977
3261
|
normalized_args=normalized_args,
|
|
2978
3262
|
)
|
|
2979
|
-
if
|
|
2980
|
-
return
|
|
3263
|
+
if permission_outcome.block is not None:
|
|
3264
|
+
return permission_outcome.block
|
|
3265
|
+
permission_outcomes.append(permission_outcome)
|
|
3266
|
+
|
|
3267
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3268
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3269
|
+
|
|
2981
3270
|
try:
|
|
2982
3271
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
2983
3272
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2984
3273
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
2985
3274
|
except (QingflowApiError, RuntimeError) as error:
|
|
2986
3275
|
api_error = _coerce_api_error(error)
|
|
2987
|
-
return _failed_from_api_error(
|
|
3276
|
+
return finalize(_failed_from_api_error(
|
|
2988
3277
|
"VIEWS_READ_FAILED",
|
|
2989
3278
|
api_error,
|
|
2990
3279
|
normalized_args=normalized_args,
|
|
2991
|
-
details={"app_key": app_key},
|
|
3280
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
2992
3281
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2993
|
-
)
|
|
3282
|
+
))
|
|
2994
3283
|
existing_views = existing_views or []
|
|
2995
3284
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
2996
3285
|
existing_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -3343,13 +3632,13 @@ class AiBuilderFacade:
|
|
|
3343
3632
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
3344
3633
|
except (QingflowApiError, RuntimeError) as error:
|
|
3345
3634
|
api_error = _coerce_api_error(error)
|
|
3346
|
-
return _failed_from_api_error(
|
|
3635
|
+
return finalize(_failed_from_api_error(
|
|
3347
3636
|
"VIEWS_READ_FAILED",
|
|
3348
3637
|
api_error,
|
|
3349
3638
|
normalized_args=normalized_args,
|
|
3350
|
-
details={"app_key": app_key},
|
|
3639
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
3351
3640
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3352
|
-
)
|
|
3641
|
+
))
|
|
3353
3642
|
verified_names = {
|
|
3354
3643
|
_extract_view_name(item)
|
|
3355
3644
|
for item in (verified_view_result or [])
|
|
@@ -3504,7 +3793,7 @@ class AiBuilderFacade:
|
|
|
3504
3793
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
3505
3794
|
"verified": verified and view_filters_verified,
|
|
3506
3795
|
}
|
|
3507
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3796
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3508
3797
|
warnings: list[dict[str, Any]] = []
|
|
3509
3798
|
if filter_readback_pending or filter_mismatches:
|
|
3510
3799
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
@@ -3538,7 +3827,7 @@ class AiBuilderFacade:
|
|
|
3538
3827
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
3539
3828
|
"verified": verified and view_filters_verified,
|
|
3540
3829
|
}
|
|
3541
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3830
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3542
3831
|
|
|
3543
3832
|
def app_publish_verify(
|
|
3544
3833
|
self,
|
|
@@ -3671,18 +3960,27 @@ class AiBuilderFacade:
|
|
|
3671
3960
|
|
|
3672
3961
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
3673
3962
|
normalized_args = request.model_dump(mode="json")
|
|
3963
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3674
3964
|
app_result = self.app_resolve(profile=profile, app_key=request.app_key)
|
|
3675
3965
|
if app_result.get("status") != "success":
|
|
3676
3966
|
return app_result
|
|
3967
|
+
resolved_outcome = _permission_outcome_from_result(app_result)
|
|
3968
|
+
if resolved_outcome is not None:
|
|
3969
|
+
permission_outcomes.append(resolved_outcome)
|
|
3677
3970
|
app_key = str(app_result.get("app_key") or request.app_key)
|
|
3678
|
-
|
|
3971
|
+
permission_outcome = self._guard_app_permission(
|
|
3679
3972
|
profile=profile,
|
|
3680
3973
|
app_key=app_key,
|
|
3681
3974
|
required_permission="data_manage",
|
|
3682
3975
|
normalized_args=normalized_args,
|
|
3683
3976
|
)
|
|
3684
|
-
if
|
|
3685
|
-
return
|
|
3977
|
+
if permission_outcome.block is not None:
|
|
3978
|
+
return permission_outcome.block
|
|
3979
|
+
permission_outcomes.append(permission_outcome)
|
|
3980
|
+
|
|
3981
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3982
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3983
|
+
|
|
3686
3984
|
try:
|
|
3687
3985
|
schema_state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
3688
3986
|
parsed = schema_state.get("parsed") if isinstance(schema_state.get("parsed"), dict) else {}
|
|
@@ -3691,13 +3989,13 @@ class AiBuilderFacade:
|
|
|
3691
3989
|
existing_chart_items, existing_chart_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
3692
3990
|
except (QingflowApiError, RuntimeError) as error:
|
|
3693
3991
|
api_error = _coerce_api_error(error)
|
|
3694
|
-
return _failed_from_api_error(
|
|
3992
|
+
return finalize(_failed_from_api_error(
|
|
3695
3993
|
"CHART_APPLY_FAILED",
|
|
3696
3994
|
api_error,
|
|
3697
3995
|
normalized_args=normalized_args,
|
|
3698
|
-
details={"app_key": app_key},
|
|
3996
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="chart", error=api_error),
|
|
3699
3997
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3700
|
-
)
|
|
3998
|
+
))
|
|
3701
3999
|
|
|
3702
4000
|
field_lookup = _build_public_field_lookup(fields)
|
|
3703
4001
|
qingbi_fields_by_id = {
|
|
@@ -3937,7 +4235,7 @@ class AiBuilderFacade:
|
|
|
3937
4235
|
|
|
3938
4236
|
if failed_items:
|
|
3939
4237
|
successful_changes = bool(created_ids or updated_ids or removed_ids or reordered)
|
|
3940
|
-
return {
|
|
4238
|
+
return finalize({
|
|
3941
4239
|
"status": "partial_success" if successful_changes else "failed",
|
|
3942
4240
|
"error_code": "CHART_APPLY_PARTIAL" if successful_changes else "CHART_APPLY_FAILED",
|
|
3943
4241
|
"recoverable": True,
|
|
@@ -3965,9 +4263,9 @@ class AiBuilderFacade:
|
|
|
3965
4263
|
"app_key": app_key,
|
|
3966
4264
|
"chart_results": chart_results,
|
|
3967
4265
|
"verified": False if failed_items else verified,
|
|
3968
|
-
}
|
|
4266
|
+
})
|
|
3969
4267
|
result_verified = verified or noop
|
|
3970
|
-
return {
|
|
4268
|
+
return finalize({
|
|
3971
4269
|
"status": "success" if result_verified else "partial_success",
|
|
3972
4270
|
"error_code": None if result_verified else "CHART_READBACK_PENDING",
|
|
3973
4271
|
"recoverable": not result_verified,
|
|
@@ -3993,10 +4291,11 @@ class AiBuilderFacade:
|
|
|
3993
4291
|
"app_key": app_key,
|
|
3994
4292
|
"chart_results": chart_results,
|
|
3995
4293
|
"verified": result_verified,
|
|
3996
|
-
}
|
|
4294
|
+
})
|
|
3997
4295
|
|
|
3998
4296
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
3999
4297
|
normalized_args = request.model_dump(mode="json")
|
|
4298
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
4000
4299
|
dash_key = str(request.dash_key or "").strip()
|
|
4001
4300
|
creating = not dash_key
|
|
4002
4301
|
verify_dash_name = creating or request.dash_name is not None
|
|
@@ -4009,24 +4308,31 @@ class AiBuilderFacade:
|
|
|
4009
4308
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
4010
4309
|
except (QingflowApiError, RuntimeError) as error:
|
|
4011
4310
|
api_error = _coerce_api_error(error)
|
|
4012
|
-
return
|
|
4311
|
+
return _failed(
|
|
4013
4312
|
"PORTAL_APPLY_FAILED",
|
|
4014
|
-
api_error,
|
|
4313
|
+
_public_error_message("PORTAL_APPLY_FAILED", api_error),
|
|
4015
4314
|
normalized_args=normalized_args,
|
|
4016
|
-
details={"dash_key": dash_key or None},
|
|
4315
|
+
details=_with_state_read_blocked_details({"dash_key": dash_key or None}, resource="portal", error=api_error),
|
|
4017
4316
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4317
|
+
request_id=api_error.request_id,
|
|
4318
|
+
backend_code=api_error.backend_code,
|
|
4319
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
4018
4320
|
)
|
|
4019
4321
|
if not isinstance(base_payload, dict):
|
|
4020
4322
|
base_payload = {}
|
|
4021
4323
|
if not creating:
|
|
4022
|
-
|
|
4324
|
+
portal_permission_outcome = self._guard_portal_permission(
|
|
4023
4325
|
profile=profile,
|
|
4024
4326
|
dash_key=dash_key,
|
|
4025
4327
|
normalized_args=normalized_args,
|
|
4026
4328
|
portal_result=base_payload,
|
|
4027
4329
|
)
|
|
4028
|
-
if
|
|
4029
|
-
return
|
|
4330
|
+
if portal_permission_outcome.block is not None:
|
|
4331
|
+
return portal_permission_outcome.block
|
|
4332
|
+
permission_outcomes.append(portal_permission_outcome)
|
|
4333
|
+
|
|
4334
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
4335
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
4030
4336
|
target_package_tag_id = request.package_tag_id
|
|
4031
4337
|
if target_package_tag_id is None:
|
|
4032
4338
|
target_package_tag_id = _coerce_positive_int(((base_payload.get("tags") or [{}])[0] if isinstance(base_payload.get("tags"), list) and base_payload.get("tags") else {}).get("tagId"))
|
|
@@ -4038,22 +4344,24 @@ class AiBuilderFacade:
|
|
|
4038
4344
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4039
4345
|
)
|
|
4040
4346
|
if creating:
|
|
4041
|
-
|
|
4347
|
+
package_add_outcome = self._guard_package_permission(
|
|
4042
4348
|
profile=profile,
|
|
4043
4349
|
tag_id=target_package_tag_id,
|
|
4044
4350
|
required_permission="add_app",
|
|
4045
4351
|
normalized_args=normalized_args,
|
|
4046
4352
|
)
|
|
4047
|
-
if
|
|
4048
|
-
return
|
|
4049
|
-
|
|
4353
|
+
if package_add_outcome.block is not None:
|
|
4354
|
+
return package_add_outcome.block
|
|
4355
|
+
permission_outcomes.append(package_add_outcome)
|
|
4356
|
+
package_edit_outcome = self._guard_package_permission(
|
|
4050
4357
|
profile=profile,
|
|
4051
4358
|
tag_id=target_package_tag_id,
|
|
4052
4359
|
required_permission="edit_app",
|
|
4053
4360
|
normalized_args=normalized_args,
|
|
4054
4361
|
)
|
|
4055
|
-
if
|
|
4056
|
-
return
|
|
4362
|
+
if package_edit_outcome.block is not None:
|
|
4363
|
+
return package_edit_outcome.block
|
|
4364
|
+
permission_outcomes.append(package_edit_outcome)
|
|
4057
4365
|
try:
|
|
4058
4366
|
if creating:
|
|
4059
4367
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -4110,7 +4418,7 @@ class AiBuilderFacade:
|
|
|
4110
4418
|
"PORTAL_APPLY_FAILED",
|
|
4111
4419
|
_public_error_message("PORTAL_APPLY_FAILED", api_error) if api_error else str(error),
|
|
4112
4420
|
normalized_args=normalized_args,
|
|
4113
|
-
details={"dash_key": dash_key or None},
|
|
4421
|
+
details=_with_state_read_blocked_details({"dash_key": dash_key or None}, resource="portal", error=api_error) if api_error is not None else {"dash_key": dash_key or None},
|
|
4114
4422
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4115
4423
|
request_id=api_error.request_id if api_error else None,
|
|
4116
4424
|
backend_code=api_error.backend_code if api_error else None,
|
|
@@ -4184,7 +4492,7 @@ class AiBuilderFacade:
|
|
|
4184
4492
|
live_meta_verified=live_meta_verified,
|
|
4185
4493
|
publish_requested=request.publish,
|
|
4186
4494
|
)
|
|
4187
|
-
return {
|
|
4495
|
+
return finalize({
|
|
4188
4496
|
"status": status,
|
|
4189
4497
|
"error_code": error_code,
|
|
4190
4498
|
"recoverable": not verified,
|
|
@@ -4215,7 +4523,7 @@ class AiBuilderFacade:
|
|
|
4215
4523
|
"verified": verified,
|
|
4216
4524
|
"draft_result": draft_result,
|
|
4217
4525
|
"live_result": live_result,
|
|
4218
|
-
}
|
|
4526
|
+
})
|
|
4219
4527
|
|
|
4220
4528
|
def _publish_current_edit_version(self, *, profile: str, app_key: str) -> JSONObject:
|
|
4221
4529
|
normalized_args = {"app_key": app_key}
|
|
@@ -4780,6 +5088,177 @@ def _failed_from_api_error(
|
|
|
4780
5088
|
)
|
|
4781
5089
|
|
|
4782
5090
|
|
|
5091
|
+
def _transport_error_payload(error: QingflowApiError) -> JSONObject:
|
|
5092
|
+
return {
|
|
5093
|
+
"http_status": error.http_status,
|
|
5094
|
+
"backend_code": error.backend_code,
|
|
5095
|
+
"category": error.category,
|
|
5096
|
+
"request_id": error.request_id,
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
|
|
5100
|
+
def _is_permission_restricted_api_error(error: QingflowApiError) -> bool:
|
|
5101
|
+
return error.backend_code in {40002, 40027}
|
|
5102
|
+
|
|
5103
|
+
|
|
5104
|
+
def _append_response_detail(details: JSONObject, *, key: str, value: Any) -> None:
|
|
5105
|
+
if value is None:
|
|
5106
|
+
return
|
|
5107
|
+
copied_value = deepcopy(value)
|
|
5108
|
+
existing = details.get(key)
|
|
5109
|
+
if existing is None:
|
|
5110
|
+
details[key] = copied_value
|
|
5111
|
+
return
|
|
5112
|
+
if isinstance(existing, list):
|
|
5113
|
+
existing.append(copied_value)
|
|
5114
|
+
return
|
|
5115
|
+
details[key] = [existing, copied_value]
|
|
5116
|
+
|
|
5117
|
+
|
|
5118
|
+
def _permission_skip_outcome(
|
|
5119
|
+
*,
|
|
5120
|
+
scope: str,
|
|
5121
|
+
target: JSONObject,
|
|
5122
|
+
required_permission: str | None,
|
|
5123
|
+
transport_error: JSONObject | None = None,
|
|
5124
|
+
permission_summary: JSONObject | None = None,
|
|
5125
|
+
) -> PermissionCheckOutcome:
|
|
5126
|
+
lookup_payload: JSONObject = {
|
|
5127
|
+
"scope": scope,
|
|
5128
|
+
"target": deepcopy(target),
|
|
5129
|
+
"required_permission": required_permission,
|
|
5130
|
+
}
|
|
5131
|
+
if transport_error:
|
|
5132
|
+
lookup_payload["transport_error"] = deepcopy(transport_error)
|
|
5133
|
+
if permission_summary:
|
|
5134
|
+
lookup_payload["permission_summary"] = deepcopy(permission_summary)
|
|
5135
|
+
warning_message = (
|
|
5136
|
+
"builder permission lookup was permission-restricted; continuing with downstream operations"
|
|
5137
|
+
if transport_error
|
|
5138
|
+
else "builder permission summary was incomplete; continuing with downstream operations"
|
|
5139
|
+
)
|
|
5140
|
+
return PermissionCheckOutcome(
|
|
5141
|
+
warnings=[
|
|
5142
|
+
_warning(
|
|
5143
|
+
"PERMISSION_CHECK_SKIPPED",
|
|
5144
|
+
warning_message,
|
|
5145
|
+
scope=scope,
|
|
5146
|
+
required_permission=required_permission,
|
|
5147
|
+
)
|
|
5148
|
+
],
|
|
5149
|
+
details={
|
|
5150
|
+
"lookup_permission_blocked": lookup_payload,
|
|
5151
|
+
"permission_check_skipped": True,
|
|
5152
|
+
},
|
|
5153
|
+
verification={"metadata_unverified": True},
|
|
5154
|
+
)
|
|
5155
|
+
|
|
5156
|
+
|
|
5157
|
+
def _verification_read_outcome(
|
|
5158
|
+
*,
|
|
5159
|
+
resource: str,
|
|
5160
|
+
target: JSONObject,
|
|
5161
|
+
transport_error: QingflowApiError,
|
|
5162
|
+
) -> PermissionCheckOutcome:
|
|
5163
|
+
return PermissionCheckOutcome(
|
|
5164
|
+
warnings=[
|
|
5165
|
+
_warning(
|
|
5166
|
+
"VERIFICATION_READ_UNAVAILABLE",
|
|
5167
|
+
"post-write verification readback was permission-restricted",
|
|
5168
|
+
resource=resource,
|
|
5169
|
+
)
|
|
5170
|
+
],
|
|
5171
|
+
details={
|
|
5172
|
+
"lookup_permission_blocked": {
|
|
5173
|
+
"scope": resource,
|
|
5174
|
+
"target": deepcopy(target),
|
|
5175
|
+
"phase": "verification",
|
|
5176
|
+
"transport_error": _transport_error_payload(transport_error),
|
|
5177
|
+
},
|
|
5178
|
+
"permission_check_skipped": True,
|
|
5179
|
+
},
|
|
5180
|
+
verification={"metadata_unverified": True},
|
|
5181
|
+
)
|
|
5182
|
+
|
|
5183
|
+
|
|
5184
|
+
def _permission_outcome_from_result(result: JSONObject) -> PermissionCheckOutcome | None:
|
|
5185
|
+
details = result.get("details") if isinstance(result.get("details"), dict) else {}
|
|
5186
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
5187
|
+
warnings = result.get("warnings") if isinstance(result.get("warnings"), list) else []
|
|
5188
|
+
filtered_details: JSONObject = {}
|
|
5189
|
+
if details.get("lookup_permission_blocked") is not None:
|
|
5190
|
+
filtered_details["lookup_permission_blocked"] = deepcopy(details["lookup_permission_blocked"])
|
|
5191
|
+
if details.get("permission_check_skipped") is not None:
|
|
5192
|
+
filtered_details["permission_check_skipped"] = bool(details.get("permission_check_skipped"))
|
|
5193
|
+
filtered_verification: JSONObject = {}
|
|
5194
|
+
if verification.get("metadata_unverified") is not None:
|
|
5195
|
+
filtered_verification["metadata_unverified"] = bool(verification.get("metadata_unverified"))
|
|
5196
|
+
filtered_warnings = [
|
|
5197
|
+
deepcopy(item)
|
|
5198
|
+
for item in warnings
|
|
5199
|
+
if isinstance(item, dict) and str(item.get("code") or "") in {"PERMISSION_CHECK_SKIPPED", "VERIFICATION_READ_UNAVAILABLE"}
|
|
5200
|
+
]
|
|
5201
|
+
if not filtered_details and not filtered_verification and not filtered_warnings:
|
|
5202
|
+
return None
|
|
5203
|
+
return PermissionCheckOutcome(
|
|
5204
|
+
warnings=filtered_warnings,
|
|
5205
|
+
details=filtered_details,
|
|
5206
|
+
verification=filtered_verification,
|
|
5207
|
+
)
|
|
5208
|
+
|
|
5209
|
+
|
|
5210
|
+
def _apply_permission_outcomes(response: JSONObject, *outcomes: PermissionCheckOutcome) -> JSONObject:
|
|
5211
|
+
if not isinstance(response, dict):
|
|
5212
|
+
return response
|
|
5213
|
+
details = response.get("details")
|
|
5214
|
+
if not isinstance(details, dict):
|
|
5215
|
+
details = {}
|
|
5216
|
+
response["details"] = details
|
|
5217
|
+
verification = response.get("verification")
|
|
5218
|
+
if not isinstance(verification, dict):
|
|
5219
|
+
verification = {}
|
|
5220
|
+
response["verification"] = verification
|
|
5221
|
+
warnings = response.get("warnings")
|
|
5222
|
+
if not isinstance(warnings, list):
|
|
5223
|
+
warnings = []
|
|
5224
|
+
response["warnings"] = warnings
|
|
5225
|
+
for outcome in outcomes:
|
|
5226
|
+
if not isinstance(outcome, PermissionCheckOutcome):
|
|
5227
|
+
continue
|
|
5228
|
+
for key, value in outcome.details.items():
|
|
5229
|
+
if key in {"lookup_permission_blocked", "state_read_blocked"}:
|
|
5230
|
+
_append_response_detail(details, key=key, value=value)
|
|
5231
|
+
elif key == "permission_check_skipped":
|
|
5232
|
+
details[key] = bool(details.get(key)) or bool(value)
|
|
5233
|
+
elif key not in details:
|
|
5234
|
+
details[key] = deepcopy(value)
|
|
5235
|
+
for key, value in outcome.verification.items():
|
|
5236
|
+
verification[key] = deepcopy(value)
|
|
5237
|
+
for warning in outcome.warnings:
|
|
5238
|
+
if warning not in warnings:
|
|
5239
|
+
warnings.append(deepcopy(warning))
|
|
5240
|
+
return response
|
|
5241
|
+
|
|
5242
|
+
|
|
5243
|
+
def _with_state_read_blocked_details(
|
|
5244
|
+
details: JSONObject | None,
|
|
5245
|
+
*,
|
|
5246
|
+
resource: str,
|
|
5247
|
+
error: QingflowApiError,
|
|
5248
|
+
) -> JSONObject:
|
|
5249
|
+
merged = deepcopy(details) if isinstance(details, dict) else {}
|
|
5250
|
+
if _is_permission_restricted_api_error(error):
|
|
5251
|
+
_append_response_detail(
|
|
5252
|
+
merged,
|
|
5253
|
+
key="state_read_blocked",
|
|
5254
|
+
value={
|
|
5255
|
+
"resource": resource,
|
|
5256
|
+
"transport_error": _transport_error_payload(error),
|
|
5257
|
+
},
|
|
5258
|
+
)
|
|
5259
|
+
return merged
|
|
5260
|
+
|
|
5261
|
+
|
|
4783
5262
|
def _from_stage_failure(stage: JSONObject, *, fallback_tool: str) -> JSONObject:
|
|
4784
5263
|
return {
|
|
4785
5264
|
"status": "failed",
|