@josephyan/qingflow-cli 0.2.0-beta.59 → 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.
|
@@ -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,
|
|
@@ -804,21 +812,53 @@ class AiBuilderFacade:
|
|
|
804
812
|
app_key: str,
|
|
805
813
|
app_title: str = "",
|
|
806
814
|
) -> JSONObject:
|
|
815
|
+
normalized_args = {"tag_id": tag_id, "app_key": app_key, "app_title": app_title}
|
|
807
816
|
if _coerce_positive_int(tag_id) is None:
|
|
808
817
|
return _failed("TAG_ID_REQUIRED", "tag_id must be positive", suggested_next_call=None)
|
|
809
|
-
|
|
818
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
819
|
+
package_permission_outcome = self._guard_package_permission(
|
|
810
820
|
profile=profile,
|
|
811
821
|
tag_id=tag_id,
|
|
812
822
|
required_permission="edit_app",
|
|
813
|
-
normalized_args=
|
|
823
|
+
normalized_args=normalized_args,
|
|
814
824
|
)
|
|
815
|
-
if
|
|
816
|
-
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
|
+
|
|
817
832
|
resolved = self.app_resolve(profile=profile, app_key=app_key)
|
|
818
833
|
if resolved.get("status") == "failed":
|
|
819
|
-
return resolved
|
|
820
|
-
|
|
821
|
-
|
|
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
|
+
)
|
|
822
862
|
already_attached = tag_id in tag_ids_before
|
|
823
863
|
try:
|
|
824
864
|
self._attach_app_to_package(
|
|
@@ -829,33 +869,72 @@ class AiBuilderFacade:
|
|
|
829
869
|
)
|
|
830
870
|
except (QingflowApiError, RuntimeError) as error:
|
|
831
871
|
api_error = _coerce_api_error(error)
|
|
832
|
-
return
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
+
)
|
|
837
884
|
)
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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 = {
|
|
842
913
|
"status": "success" if attached else "partial_success",
|
|
843
|
-
"error_code": None,
|
|
844
|
-
"recoverable":
|
|
845
|
-
"message": "attached app to package" if attached else "app attachment could not be verified",
|
|
846
|
-
"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,
|
|
847
918
|
"missing_fields": [],
|
|
848
919
|
"allowed_values": {},
|
|
849
920
|
"details": {},
|
|
850
|
-
"request_id": None,
|
|
921
|
+
"request_id": verification_error.request_id if verification_error is not None else None,
|
|
851
922
|
"suggested_next_call": None if attached else {"tool_name": "package_attach_app", "arguments": {"profile": profile, "tag_id": tag_id, "app_key": app_key}},
|
|
852
923
|
"noop": already_attached,
|
|
853
|
-
"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
|
+
},
|
|
854
930
|
"app_key": app_key,
|
|
855
931
|
"tag_id": tag_id,
|
|
856
932
|
"tag_ids_after": tag_ids_after,
|
|
857
933
|
"attached": attached,
|
|
858
934
|
}
|
|
935
|
+
if verification_error is not None:
|
|
936
|
+
response["details"]["verification_error"] = _transport_error_payload(verification_error)
|
|
937
|
+
return finalize(response)
|
|
859
938
|
|
|
860
939
|
def app_release_edit_lock_if_mine(
|
|
861
940
|
self,
|
|
@@ -979,6 +1058,42 @@ class AiBuilderFacade:
|
|
|
979
1058
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True)
|
|
980
1059
|
except (QingflowApiError, RuntimeError) as exc:
|
|
981
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
|
+
}
|
|
982
1097
|
return _failed_from_api_error(
|
|
983
1098
|
"APP_NOT_FOUND" if api_error.http_status == 404 else "APP_RESOLVE_FAILED",
|
|
984
1099
|
api_error,
|
|
@@ -1285,67 +1400,73 @@ class AiBuilderFacade:
|
|
|
1285
1400
|
app_key: str,
|
|
1286
1401
|
required_permission: str,
|
|
1287
1402
|
normalized_args: JSONObject,
|
|
1288
|
-
) ->
|
|
1403
|
+
) -> PermissionCheckOutcome:
|
|
1289
1404
|
try:
|
|
1290
1405
|
permission_summary = self._read_app_permission_summary(profile=profile, app_key=app_key)
|
|
1291
1406
|
except (QingflowApiError, RuntimeError) as error:
|
|
1292
1407
|
api_error = _coerce_api_error(error)
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
+
},
|
|
1305
1429
|
},
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
+
)
|
|
1311
1435
|
)
|
|
1312
1436
|
permission_key = {
|
|
1313
1437
|
"edit_app": "can_edit_app",
|
|
1314
1438
|
"data_manage": "can_manage_data",
|
|
1315
1439
|
}.get(required_permission)
|
|
1316
1440
|
if permission_key is None:
|
|
1317
|
-
return
|
|
1441
|
+
return PermissionCheckOutcome()
|
|
1318
1442
|
permission_value = permission_summary.get(permission_key)
|
|
1319
1443
|
if permission_value is None:
|
|
1320
|
-
return
|
|
1321
|
-
"
|
|
1322
|
-
"
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
"app_key": app_key,
|
|
1326
|
-
"required_permission": required_permission,
|
|
1327
|
-
"permission_summary": permission_summary,
|
|
1328
|
-
},
|
|
1329
|
-
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,
|
|
1330
1449
|
)
|
|
1331
1450
|
if permission_value is not False:
|
|
1332
|
-
return
|
|
1451
|
+
return PermissionCheckOutcome()
|
|
1333
1452
|
error_code = "EDIT_APP_UNAUTHORIZED" if required_permission == "edit_app" else "DATA_MANAGE_UNAUTHORIZED"
|
|
1334
1453
|
message = (
|
|
1335
1454
|
"current user does not have builder edit-app permission on this app"
|
|
1336
1455
|
if required_permission == "edit_app"
|
|
1337
1456
|
else "current user does not have data-management permission on this app"
|
|
1338
1457
|
)
|
|
1339
|
-
return
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
+
)
|
|
1349
1470
|
)
|
|
1350
1471
|
|
|
1351
1472
|
def _guard_package_permission(
|
|
@@ -1355,29 +1476,38 @@ class AiBuilderFacade:
|
|
|
1355
1476
|
tag_id: int,
|
|
1356
1477
|
required_permission: str,
|
|
1357
1478
|
normalized_args: JSONObject,
|
|
1358
|
-
) ->
|
|
1479
|
+
) -> PermissionCheckOutcome:
|
|
1359
1480
|
try:
|
|
1360
1481
|
permission_summary = self._read_package_permission_summary(profile=profile, tag_id=tag_id)
|
|
1361
1482
|
except (QingflowApiError, RuntimeError) as error:
|
|
1362
1483
|
api_error = _coerce_api_error(error)
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
+
},
|
|
1375
1505
|
},
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
+
)
|
|
1381
1511
|
)
|
|
1382
1512
|
permission_specs = {
|
|
1383
1513
|
"add_app": (
|
|
@@ -1398,12 +1528,21 @@ class AiBuilderFacade:
|
|
|
1398
1528
|
}
|
|
1399
1529
|
permission_key, error_code, message = permission_specs.get(required_permission, (None, None, None))
|
|
1400
1530
|
if permission_key is None:
|
|
1401
|
-
return
|
|
1531
|
+
return PermissionCheckOutcome()
|
|
1402
1532
|
permission_value = permission_summary.get(permission_key)
|
|
1403
1533
|
if permission_value is None:
|
|
1404
|
-
return
|
|
1405
|
-
"
|
|
1406
|
-
"
|
|
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",
|
|
1407
1546
|
normalized_args=normalized_args,
|
|
1408
1547
|
details={
|
|
1409
1548
|
"tag_id": tag_id,
|
|
@@ -1412,18 +1551,6 @@ class AiBuilderFacade:
|
|
|
1412
1551
|
},
|
|
1413
1552
|
suggested_next_call={"tool_name": "package_get_base", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
1414
1553
|
)
|
|
1415
|
-
if permission_value is not False:
|
|
1416
|
-
return None
|
|
1417
|
-
return _failed(
|
|
1418
|
-
error_code or "PACKAGE_PERMISSION_UNVERIFIED",
|
|
1419
|
-
message or "current user does not have the required package permission",
|
|
1420
|
-
normalized_args=normalized_args,
|
|
1421
|
-
details={
|
|
1422
|
-
"tag_id": tag_id,
|
|
1423
|
-
"required_permission": required_permission,
|
|
1424
|
-
"permission_summary": permission_summary,
|
|
1425
|
-
},
|
|
1426
|
-
suggested_next_call={"tool_name": "package_get_base", "arguments": {"profile": profile, "tag_id": tag_id}},
|
|
1427
1554
|
)
|
|
1428
1555
|
|
|
1429
1556
|
def _guard_portal_permission(
|
|
@@ -1433,25 +1560,26 @@ class AiBuilderFacade:
|
|
|
1433
1560
|
dash_key: str,
|
|
1434
1561
|
normalized_args: JSONObject,
|
|
1435
1562
|
portal_result: dict[str, Any],
|
|
1436
|
-
) ->
|
|
1563
|
+
) -> PermissionCheckOutcome:
|
|
1437
1564
|
permission_summary = self._read_portal_permission_summary(dash_key=dash_key, portal_result=portal_result)
|
|
1438
1565
|
permission_value = permission_summary.get("can_edit_portal")
|
|
1439
1566
|
if permission_value is None:
|
|
1440
|
-
return
|
|
1441
|
-
"
|
|
1442
|
-
"
|
|
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",
|
|
1443
1579
|
normalized_args=normalized_args,
|
|
1444
1580
|
details={"dash_key": dash_key, "permission_summary": permission_summary},
|
|
1445
1581
|
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
1446
1582
|
)
|
|
1447
|
-
if permission_value is not False:
|
|
1448
|
-
return None
|
|
1449
|
-
return _failed(
|
|
1450
|
-
"PORTAL_EDIT_UNAUTHORIZED",
|
|
1451
|
-
"current user does not have builder edit permission on this portal",
|
|
1452
|
-
normalized_args=normalized_args,
|
|
1453
|
-
details={"dash_key": dash_key, "permission_summary": permission_summary},
|
|
1454
|
-
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, "dash_key": dash_key}},
|
|
1455
1583
|
)
|
|
1456
1584
|
|
|
1457
1585
|
def app_read_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
@@ -2226,6 +2354,7 @@ class AiBuilderFacade:
|
|
|
2226
2354
|
"update_fields": [patch.model_dump(mode="json") for patch in update_fields],
|
|
2227
2355
|
"remove_fields": [patch.model_dump(mode="json") for patch in remove_fields],
|
|
2228
2356
|
}
|
|
2357
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
2229
2358
|
requested_field_changes = bool(add_fields or update_fields or remove_fields)
|
|
2230
2359
|
resolved: JSONObject
|
|
2231
2360
|
if app_key:
|
|
@@ -2234,31 +2363,37 @@ class AiBuilderFacade:
|
|
|
2234
2363
|
resolved = self.app_resolve(profile=profile, app_name=app_name, package_tag_id=package_tag_id)
|
|
2235
2364
|
else:
|
|
2236
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
|
+
|
|
2237
2370
|
if resolved.get("status") == "failed":
|
|
2238
2371
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
2239
2372
|
resolved["normalized_args"] = normalized_args
|
|
2240
2373
|
if not create_if_missing or app_key or resolved.get("error_code") != "APP_NOT_FOUND":
|
|
2241
|
-
return resolved
|
|
2374
|
+
return finalize(resolved)
|
|
2242
2375
|
permission_tag_id = _coerce_positive_int(package_tag_id)
|
|
2243
2376
|
if permission_tag_id is None:
|
|
2244
2377
|
permission_tag_id = 0
|
|
2245
|
-
|
|
2378
|
+
add_permission_outcome = self._guard_package_permission(
|
|
2246
2379
|
profile=profile,
|
|
2247
2380
|
tag_id=permission_tag_id,
|
|
2248
2381
|
required_permission="add_app",
|
|
2249
2382
|
normalized_args=normalized_args,
|
|
2250
2383
|
)
|
|
2251
|
-
if
|
|
2252
|
-
return
|
|
2384
|
+
if add_permission_outcome.block is not None:
|
|
2385
|
+
return add_permission_outcome.block
|
|
2386
|
+
permission_outcomes.append(add_permission_outcome)
|
|
2253
2387
|
if requested_field_changes:
|
|
2254
|
-
|
|
2388
|
+
edit_permission_outcome = self._guard_package_permission(
|
|
2255
2389
|
profile=profile,
|
|
2256
2390
|
tag_id=permission_tag_id,
|
|
2257
2391
|
required_permission="edit_app",
|
|
2258
2392
|
normalized_args=normalized_args,
|
|
2259
2393
|
)
|
|
2260
|
-
if
|
|
2261
|
-
return
|
|
2394
|
+
if edit_permission_outcome.block is not None:
|
|
2395
|
+
return edit_permission_outcome.block
|
|
2396
|
+
permission_outcomes.append(edit_permission_outcome)
|
|
2262
2397
|
resolved = self._create_target_app_shell(
|
|
2263
2398
|
profile=profile,
|
|
2264
2399
|
app_name=app_name,
|
|
@@ -2267,23 +2402,27 @@ class AiBuilderFacade:
|
|
|
2267
2402
|
if resolved.get("status") == "failed":
|
|
2268
2403
|
if not isinstance(resolved.get("normalized_args"), dict) or not resolved.get("normalized_args"):
|
|
2269
2404
|
resolved["normalized_args"] = normalized_args
|
|
2270
|
-
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)
|
|
2271
2409
|
target = ResolvedApp(
|
|
2272
2410
|
app_key=str(resolved["app_key"]),
|
|
2273
2411
|
app_name=str(resolved["app_name"]),
|
|
2274
2412
|
tag_ids=_coerce_int_list(resolved.get("tag_ids")),
|
|
2275
2413
|
)
|
|
2276
2414
|
if not bool(resolved.get("created")):
|
|
2277
|
-
|
|
2415
|
+
permission_outcome = self._guard_app_permission(
|
|
2278
2416
|
profile=profile,
|
|
2279
2417
|
app_key=target.app_key,
|
|
2280
2418
|
required_permission="edit_app",
|
|
2281
2419
|
normalized_args=normalized_args,
|
|
2282
2420
|
)
|
|
2283
|
-
if
|
|
2284
|
-
return
|
|
2421
|
+
if permission_outcome.block is not None:
|
|
2422
|
+
return permission_outcome.block
|
|
2423
|
+
permission_outcomes.append(permission_outcome)
|
|
2285
2424
|
if bool(resolved.get("created")) and not requested_field_changes:
|
|
2286
|
-
return {
|
|
2425
|
+
return finalize({
|
|
2287
2426
|
"status": "success",
|
|
2288
2427
|
"error_code": None,
|
|
2289
2428
|
"recoverable": False,
|
|
@@ -2310,21 +2449,21 @@ class AiBuilderFacade:
|
|
|
2310
2449
|
"package_attached": None if package_tag_id is None else package_tag_id in target.tag_ids,
|
|
2311
2450
|
"publish_requested": False,
|
|
2312
2451
|
"published": False,
|
|
2313
|
-
}
|
|
2452
|
+
})
|
|
2314
2453
|
schema_readback_delayed = False
|
|
2315
2454
|
try:
|
|
2316
2455
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=target.app_key)
|
|
2317
2456
|
except (QingflowApiError, RuntimeError) as error:
|
|
2318
2457
|
api_error = _coerce_api_error(error)
|
|
2319
2458
|
if not bool(resolved.get("created")) or api_error.http_status != 404:
|
|
2320
|
-
return _failed_from_api_error(
|
|
2459
|
+
return finalize(_failed_from_api_error(
|
|
2321
2460
|
"SCHEMA_READBACK_FAILED",
|
|
2322
2461
|
api_error,
|
|
2323
2462
|
normalized_args=normalized_args,
|
|
2324
2463
|
allowed_values={"field_types": [item.value for item in PublicFieldType]},
|
|
2325
|
-
details={"app_key": target.app_key},
|
|
2464
|
+
details=_with_state_read_blocked_details({"app_key": target.app_key}, resource="schema", error=api_error),
|
|
2326
2465
|
suggested_next_call={"tool_name": "app_read_summary", "arguments": {"profile": profile, "app_key": target.app_key}},
|
|
2327
|
-
)
|
|
2466
|
+
))
|
|
2328
2467
|
schema_result = _empty_schema_result(target.app_name)
|
|
2329
2468
|
_schema_source = "synthetic_new_app"
|
|
2330
2469
|
schema_readback_delayed = True
|
|
@@ -2442,7 +2581,7 @@ class AiBuilderFacade:
|
|
|
2442
2581
|
"package_attached": package_attached,
|
|
2443
2582
|
}
|
|
2444
2583
|
response["details"]["relation_field_count"] = relation_field_count
|
|
2445
|
-
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))
|
|
2446
2585
|
|
|
2447
2586
|
payload = _build_form_payload_from_fields(
|
|
2448
2587
|
title=schema_result.get("formTitle") or target.app_name,
|
|
@@ -2581,7 +2720,7 @@ class AiBuilderFacade:
|
|
|
2581
2720
|
"http_status": verification_error.http_status,
|
|
2582
2721
|
"backend_code": verification_error.backend_code,
|
|
2583
2722
|
}
|
|
2584
|
-
return response
|
|
2723
|
+
return finalize(response)
|
|
2585
2724
|
|
|
2586
2725
|
def app_layout_apply(
|
|
2587
2726
|
self,
|
|
@@ -2599,25 +2738,31 @@ class AiBuilderFacade:
|
|
|
2599
2738
|
"sections": requested_sections,
|
|
2600
2739
|
"publish": publish,
|
|
2601
2740
|
}
|
|
2602
|
-
|
|
2741
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
2742
|
+
permission_outcome = self._guard_app_permission(
|
|
2603
2743
|
profile=profile,
|
|
2604
2744
|
app_key=app_key,
|
|
2605
2745
|
required_permission="edit_app",
|
|
2606
2746
|
normalized_args=normalized_args,
|
|
2607
2747
|
)
|
|
2608
|
-
if
|
|
2609
|
-
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
|
+
|
|
2610
2755
|
try:
|
|
2611
2756
|
schema_result, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2612
2757
|
except (QingflowApiError, RuntimeError) as error:
|
|
2613
2758
|
api_error = _coerce_api_error(error)
|
|
2614
|
-
return _failed_from_api_error(
|
|
2759
|
+
return finalize(_failed_from_api_error(
|
|
2615
2760
|
"LAYOUT_READ_FAILED",
|
|
2616
2761
|
api_error,
|
|
2617
2762
|
normalized_args=normalized_args,
|
|
2618
|
-
details={"app_key": app_key},
|
|
2763
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="schema", error=api_error),
|
|
2619
2764
|
suggested_next_call={"tool_name": "app_read_layout_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2620
|
-
)
|
|
2765
|
+
))
|
|
2621
2766
|
parsed = _parse_schema(schema_result)
|
|
2622
2767
|
current_fields = parsed["fields"]
|
|
2623
2768
|
requested_sections, missing_selectors = _resolve_layout_sections_to_names(requested_sections, current_fields)
|
|
@@ -2709,7 +2854,7 @@ class AiBuilderFacade:
|
|
|
2709
2854
|
},
|
|
2710
2855
|
"verified": True,
|
|
2711
2856
|
}
|
|
2712
|
-
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))
|
|
2713
2858
|
payload = _build_form_payload_from_existing_schema(
|
|
2714
2859
|
current_schema=schema_result,
|
|
2715
2860
|
layout=target_layout,
|
|
@@ -2742,7 +2887,7 @@ class AiBuilderFacade:
|
|
|
2742
2887
|
fallback_applied = "flatten_sections"
|
|
2743
2888
|
except (QingflowApiError, RuntimeError) as fallback_error:
|
|
2744
2889
|
api_fallback_error = _coerce_api_error(fallback_error)
|
|
2745
|
-
return _failed_from_api_error(
|
|
2890
|
+
return finalize(_failed_from_api_error(
|
|
2746
2891
|
"LAYOUT_APPLY_FAILED",
|
|
2747
2892
|
api_fallback_error,
|
|
2748
2893
|
normalized_args=normalized_args,
|
|
@@ -2755,9 +2900,9 @@ class AiBuilderFacade:
|
|
|
2755
2900
|
"fallback_layout": flattened_layout,
|
|
2756
2901
|
},
|
|
2757
2902
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2758
|
-
)
|
|
2903
|
+
))
|
|
2759
2904
|
else:
|
|
2760
|
-
return _failed_from_api_error(
|
|
2905
|
+
return finalize(_failed_from_api_error(
|
|
2761
2906
|
"LAYOUT_APPLY_FAILED",
|
|
2762
2907
|
api_error,
|
|
2763
2908
|
normalized_args=normalized_args,
|
|
@@ -2768,7 +2913,7 @@ class AiBuilderFacade:
|
|
|
2768
2913
|
"current_field_names": [field["name"] for field in current_fields],
|
|
2769
2914
|
},
|
|
2770
2915
|
suggested_next_call={"tool_name": "app_layout_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2771
|
-
)
|
|
2916
|
+
))
|
|
2772
2917
|
try:
|
|
2773
2918
|
verified_schema, _verified_schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2774
2919
|
except (QingflowApiError, RuntimeError) as error:
|
|
@@ -2798,7 +2943,7 @@ class AiBuilderFacade:
|
|
|
2798
2943
|
"verified": False,
|
|
2799
2944
|
}
|
|
2800
2945
|
response["request_id"] = api_error.request_id
|
|
2801
|
-
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))
|
|
2802
2947
|
verified_layout = _parse_schema(verified_schema)["layout"]
|
|
2803
2948
|
layout_verified = _layouts_equal(verified_layout, applied_layout) or _layouts_semantically_equal(verified_layout, applied_layout)
|
|
2804
2949
|
raw_layout_has_content = _schema_has_layout_content(verified_schema)
|
|
@@ -2848,7 +2993,7 @@ class AiBuilderFacade:
|
|
|
2848
2993
|
},
|
|
2849
2994
|
"verified": layout_verified,
|
|
2850
2995
|
}
|
|
2851
|
-
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))
|
|
2852
2997
|
|
|
2853
2998
|
def app_flow_apply(
|
|
2854
2999
|
self,
|
|
@@ -2867,34 +3012,40 @@ class AiBuilderFacade:
|
|
|
2867
3012
|
"transitions": transitions,
|
|
2868
3013
|
"publish": publish,
|
|
2869
3014
|
}
|
|
2870
|
-
|
|
3015
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3016
|
+
permission_outcome = self._guard_app_permission(
|
|
2871
3017
|
profile=profile,
|
|
2872
3018
|
app_key=app_key,
|
|
2873
3019
|
required_permission="data_manage",
|
|
2874
3020
|
normalized_args=normalized_args,
|
|
2875
3021
|
)
|
|
2876
|
-
if
|
|
2877
|
-
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
|
+
|
|
2878
3029
|
if mode != "replace":
|
|
2879
|
-
return _failed(
|
|
3030
|
+
return finalize(_failed(
|
|
2880
3031
|
"UNSUPPORTED_FLOW_MODE",
|
|
2881
3032
|
"only mode='replace' is supported",
|
|
2882
3033
|
normalized_args=normalized_args,
|
|
2883
3034
|
allowed_values={"modes": ["replace"]},
|
|
2884
3035
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2885
|
-
)
|
|
3036
|
+
))
|
|
2886
3037
|
try:
|
|
2887
3038
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
2888
3039
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2889
3040
|
except (QingflowApiError, RuntimeError) as error:
|
|
2890
3041
|
api_error = _coerce_api_error(error)
|
|
2891
|
-
return _failed_from_api_error(
|
|
3042
|
+
return finalize(_failed_from_api_error(
|
|
2892
3043
|
"FLOW_READ_FAILED",
|
|
2893
3044
|
api_error,
|
|
2894
3045
|
normalized_args=normalized_args,
|
|
2895
|
-
details={"app_key": app_key},
|
|
3046
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
2896
3047
|
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2897
|
-
)
|
|
3048
|
+
))
|
|
2898
3049
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
2899
3050
|
current_fields = _parse_schema(schema)["fields"]
|
|
2900
3051
|
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
@@ -3006,7 +3157,7 @@ class AiBuilderFacade:
|
|
|
3006
3157
|
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
3007
3158
|
suggested_next_call["arguments"] = arguments
|
|
3008
3159
|
failed["suggested_next_call"] = suggested_next_call
|
|
3009
|
-
return failed
|
|
3160
|
+
return finalize(failed)
|
|
3010
3161
|
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
3011
3162
|
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
3012
3163
|
current_workflow=verified_nodes,
|
|
@@ -3065,7 +3216,7 @@ class AiBuilderFacade:
|
|
|
3065
3216
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
3066
3217
|
"verified": workflow_verified,
|
|
3067
3218
|
}
|
|
3068
|
-
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))
|
|
3069
3220
|
|
|
3070
3221
|
def app_views_apply(
|
|
3071
3222
|
self,
|
|
@@ -3102,27 +3253,33 @@ class AiBuilderFacade:
|
|
|
3102
3253
|
"verified": True,
|
|
3103
3254
|
}
|
|
3104
3255
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3105
|
-
|
|
3256
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3257
|
+
permission_outcome = self._guard_app_permission(
|
|
3106
3258
|
profile=profile,
|
|
3107
3259
|
app_key=app_key,
|
|
3108
3260
|
required_permission="data_manage",
|
|
3109
3261
|
normalized_args=normalized_args,
|
|
3110
3262
|
)
|
|
3111
|
-
if
|
|
3112
|
-
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
|
+
|
|
3113
3270
|
try:
|
|
3114
3271
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
3115
3272
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
3116
3273
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
3117
3274
|
except (QingflowApiError, RuntimeError) as error:
|
|
3118
3275
|
api_error = _coerce_api_error(error)
|
|
3119
|
-
return _failed_from_api_error(
|
|
3276
|
+
return finalize(_failed_from_api_error(
|
|
3120
3277
|
"VIEWS_READ_FAILED",
|
|
3121
3278
|
api_error,
|
|
3122
3279
|
normalized_args=normalized_args,
|
|
3123
|
-
details={"app_key": app_key},
|
|
3280
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
3124
3281
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3125
|
-
)
|
|
3282
|
+
))
|
|
3126
3283
|
existing_views = existing_views or []
|
|
3127
3284
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
3128
3285
|
existing_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -3475,13 +3632,13 @@ class AiBuilderFacade:
|
|
|
3475
3632
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
3476
3633
|
except (QingflowApiError, RuntimeError) as error:
|
|
3477
3634
|
api_error = _coerce_api_error(error)
|
|
3478
|
-
return _failed_from_api_error(
|
|
3635
|
+
return finalize(_failed_from_api_error(
|
|
3479
3636
|
"VIEWS_READ_FAILED",
|
|
3480
3637
|
api_error,
|
|
3481
3638
|
normalized_args=normalized_args,
|
|
3482
|
-
details={"app_key": app_key},
|
|
3639
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
3483
3640
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3484
|
-
)
|
|
3641
|
+
))
|
|
3485
3642
|
verified_names = {
|
|
3486
3643
|
_extract_view_name(item)
|
|
3487
3644
|
for item in (verified_view_result or [])
|
|
@@ -3636,7 +3793,7 @@ class AiBuilderFacade:
|
|
|
3636
3793
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
3637
3794
|
"verified": verified and view_filters_verified,
|
|
3638
3795
|
}
|
|
3639
|
-
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))
|
|
3640
3797
|
warnings: list[dict[str, Any]] = []
|
|
3641
3798
|
if filter_readback_pending or filter_mismatches:
|
|
3642
3799
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
@@ -3670,7 +3827,7 @@ class AiBuilderFacade:
|
|
|
3670
3827
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
3671
3828
|
"verified": verified and view_filters_verified,
|
|
3672
3829
|
}
|
|
3673
|
-
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))
|
|
3674
3831
|
|
|
3675
3832
|
def app_publish_verify(
|
|
3676
3833
|
self,
|
|
@@ -3803,18 +3960,27 @@ class AiBuilderFacade:
|
|
|
3803
3960
|
|
|
3804
3961
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
3805
3962
|
normalized_args = request.model_dump(mode="json")
|
|
3963
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3806
3964
|
app_result = self.app_resolve(profile=profile, app_key=request.app_key)
|
|
3807
3965
|
if app_result.get("status") != "success":
|
|
3808
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)
|
|
3809
3970
|
app_key = str(app_result.get("app_key") or request.app_key)
|
|
3810
|
-
|
|
3971
|
+
permission_outcome = self._guard_app_permission(
|
|
3811
3972
|
profile=profile,
|
|
3812
3973
|
app_key=app_key,
|
|
3813
3974
|
required_permission="data_manage",
|
|
3814
3975
|
normalized_args=normalized_args,
|
|
3815
3976
|
)
|
|
3816
|
-
if
|
|
3817
|
-
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
|
+
|
|
3818
3984
|
try:
|
|
3819
3985
|
schema_state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
3820
3986
|
parsed = schema_state.get("parsed") if isinstance(schema_state.get("parsed"), dict) else {}
|
|
@@ -3823,13 +3989,13 @@ class AiBuilderFacade:
|
|
|
3823
3989
|
existing_chart_items, existing_chart_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
3824
3990
|
except (QingflowApiError, RuntimeError) as error:
|
|
3825
3991
|
api_error = _coerce_api_error(error)
|
|
3826
|
-
return _failed_from_api_error(
|
|
3992
|
+
return finalize(_failed_from_api_error(
|
|
3827
3993
|
"CHART_APPLY_FAILED",
|
|
3828
3994
|
api_error,
|
|
3829
3995
|
normalized_args=normalized_args,
|
|
3830
|
-
details={"app_key": app_key},
|
|
3996
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="chart", error=api_error),
|
|
3831
3997
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3832
|
-
)
|
|
3998
|
+
))
|
|
3833
3999
|
|
|
3834
4000
|
field_lookup = _build_public_field_lookup(fields)
|
|
3835
4001
|
qingbi_fields_by_id = {
|
|
@@ -4069,7 +4235,7 @@ class AiBuilderFacade:
|
|
|
4069
4235
|
|
|
4070
4236
|
if failed_items:
|
|
4071
4237
|
successful_changes = bool(created_ids or updated_ids or removed_ids or reordered)
|
|
4072
|
-
return {
|
|
4238
|
+
return finalize({
|
|
4073
4239
|
"status": "partial_success" if successful_changes else "failed",
|
|
4074
4240
|
"error_code": "CHART_APPLY_PARTIAL" if successful_changes else "CHART_APPLY_FAILED",
|
|
4075
4241
|
"recoverable": True,
|
|
@@ -4097,9 +4263,9 @@ class AiBuilderFacade:
|
|
|
4097
4263
|
"app_key": app_key,
|
|
4098
4264
|
"chart_results": chart_results,
|
|
4099
4265
|
"verified": False if failed_items else verified,
|
|
4100
|
-
}
|
|
4266
|
+
})
|
|
4101
4267
|
result_verified = verified or noop
|
|
4102
|
-
return {
|
|
4268
|
+
return finalize({
|
|
4103
4269
|
"status": "success" if result_verified else "partial_success",
|
|
4104
4270
|
"error_code": None if result_verified else "CHART_READBACK_PENDING",
|
|
4105
4271
|
"recoverable": not result_verified,
|
|
@@ -4125,10 +4291,11 @@ class AiBuilderFacade:
|
|
|
4125
4291
|
"app_key": app_key,
|
|
4126
4292
|
"chart_results": chart_results,
|
|
4127
4293
|
"verified": result_verified,
|
|
4128
|
-
}
|
|
4294
|
+
})
|
|
4129
4295
|
|
|
4130
4296
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
4131
4297
|
normalized_args = request.model_dump(mode="json")
|
|
4298
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
4132
4299
|
dash_key = str(request.dash_key or "").strip()
|
|
4133
4300
|
creating = not dash_key
|
|
4134
4301
|
verify_dash_name = creating or request.dash_name is not None
|
|
@@ -4141,24 +4308,31 @@ class AiBuilderFacade:
|
|
|
4141
4308
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
4142
4309
|
except (QingflowApiError, RuntimeError) as error:
|
|
4143
4310
|
api_error = _coerce_api_error(error)
|
|
4144
|
-
return
|
|
4311
|
+
return _failed(
|
|
4145
4312
|
"PORTAL_APPLY_FAILED",
|
|
4146
|
-
api_error,
|
|
4313
|
+
_public_error_message("PORTAL_APPLY_FAILED", api_error),
|
|
4147
4314
|
normalized_args=normalized_args,
|
|
4148
|
-
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),
|
|
4149
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,
|
|
4150
4320
|
)
|
|
4151
4321
|
if not isinstance(base_payload, dict):
|
|
4152
4322
|
base_payload = {}
|
|
4153
4323
|
if not creating:
|
|
4154
|
-
|
|
4324
|
+
portal_permission_outcome = self._guard_portal_permission(
|
|
4155
4325
|
profile=profile,
|
|
4156
4326
|
dash_key=dash_key,
|
|
4157
4327
|
normalized_args=normalized_args,
|
|
4158
4328
|
portal_result=base_payload,
|
|
4159
4329
|
)
|
|
4160
|
-
if
|
|
4161
|
-
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)
|
|
4162
4336
|
target_package_tag_id = request.package_tag_id
|
|
4163
4337
|
if target_package_tag_id is None:
|
|
4164
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"))
|
|
@@ -4170,22 +4344,24 @@ class AiBuilderFacade:
|
|
|
4170
4344
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4171
4345
|
)
|
|
4172
4346
|
if creating:
|
|
4173
|
-
|
|
4347
|
+
package_add_outcome = self._guard_package_permission(
|
|
4174
4348
|
profile=profile,
|
|
4175
4349
|
tag_id=target_package_tag_id,
|
|
4176
4350
|
required_permission="add_app",
|
|
4177
4351
|
normalized_args=normalized_args,
|
|
4178
4352
|
)
|
|
4179
|
-
if
|
|
4180
|
-
return
|
|
4181
|
-
|
|
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(
|
|
4182
4357
|
profile=profile,
|
|
4183
4358
|
tag_id=target_package_tag_id,
|
|
4184
4359
|
required_permission="edit_app",
|
|
4185
4360
|
normalized_args=normalized_args,
|
|
4186
4361
|
)
|
|
4187
|
-
if
|
|
4188
|
-
return
|
|
4362
|
+
if package_edit_outcome.block is not None:
|
|
4363
|
+
return package_edit_outcome.block
|
|
4364
|
+
permission_outcomes.append(package_edit_outcome)
|
|
4189
4365
|
try:
|
|
4190
4366
|
if creating:
|
|
4191
4367
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -4242,7 +4418,7 @@ class AiBuilderFacade:
|
|
|
4242
4418
|
"PORTAL_APPLY_FAILED",
|
|
4243
4419
|
_public_error_message("PORTAL_APPLY_FAILED", api_error) if api_error else str(error),
|
|
4244
4420
|
normalized_args=normalized_args,
|
|
4245
|
-
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},
|
|
4246
4422
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4247
4423
|
request_id=api_error.request_id if api_error else None,
|
|
4248
4424
|
backend_code=api_error.backend_code if api_error else None,
|
|
@@ -4316,7 +4492,7 @@ class AiBuilderFacade:
|
|
|
4316
4492
|
live_meta_verified=live_meta_verified,
|
|
4317
4493
|
publish_requested=request.publish,
|
|
4318
4494
|
)
|
|
4319
|
-
return {
|
|
4495
|
+
return finalize({
|
|
4320
4496
|
"status": status,
|
|
4321
4497
|
"error_code": error_code,
|
|
4322
4498
|
"recoverable": not verified,
|
|
@@ -4347,7 +4523,7 @@ class AiBuilderFacade:
|
|
|
4347
4523
|
"verified": verified,
|
|
4348
4524
|
"draft_result": draft_result,
|
|
4349
4525
|
"live_result": live_result,
|
|
4350
|
-
}
|
|
4526
|
+
})
|
|
4351
4527
|
|
|
4352
4528
|
def _publish_current_edit_version(self, *, profile: str, app_key: str) -> JSONObject:
|
|
4353
4529
|
normalized_args = {"app_key": app_key}
|
|
@@ -4912,6 +5088,177 @@ def _failed_from_api_error(
|
|
|
4912
5088
|
)
|
|
4913
5089
|
|
|
4914
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
|
+
|
|
4915
5262
|
def _from_stage_failure(stage: JSONObject, *, fallback_tool: str) -> JSONObject:
|
|
4916
5263
|
return {
|
|
4917
5264
|
"status": "failed",
|