@josephyan/qingflow-cli 0.2.0-beta.59 → 0.2.0-beta.61
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)
|
|
@@ -2810,19 +2955,24 @@ class AiBuilderFacade:
|
|
|
2810
2955
|
warnings: list[dict[str, Any]] = []
|
|
2811
2956
|
if not layout_summary_verified:
|
|
2812
2957
|
warnings.append(_warning("LAYOUT_SUMMARY_UNVERIFIED", "layout summary is incomplete relative to raw schema readback"))
|
|
2958
|
+
if fallback_applied is not None and layout_verified and layout_summary_verified:
|
|
2959
|
+
warnings.append(_warning("LAYOUT_FALLBACK_APPLIED", "layout readback normalized sectioned layout into flat layout while preserving field placement"))
|
|
2813
2960
|
response = {
|
|
2814
|
-
"status": "success" if layout_verified and layout_summary_verified
|
|
2961
|
+
"status": "success" if layout_verified and layout_summary_verified else "partial_success",
|
|
2815
2962
|
"error_code": (
|
|
2816
2963
|
None
|
|
2817
|
-
if layout_verified and layout_summary_verified
|
|
2964
|
+
if layout_verified and layout_summary_verified
|
|
2818
2965
|
else "LAYOUT_SUMMARY_UNVERIFIED"
|
|
2819
2966
|
if not layout_summary_verified
|
|
2820
2967
|
else "LAYOUT_READBACK_MISMATCH"
|
|
2821
2968
|
),
|
|
2822
|
-
"recoverable": not (layout_verified and layout_summary_verified
|
|
2969
|
+
"recoverable": not (layout_verified and layout_summary_verified),
|
|
2823
2970
|
"message": (
|
|
2971
|
+
"applied app layout with flattened section fallback"
|
|
2972
|
+
if layout_verified and layout_summary_verified and fallback_applied is not None
|
|
2973
|
+
else
|
|
2824
2974
|
"applied app layout"
|
|
2825
|
-
if layout_verified and layout_summary_verified
|
|
2975
|
+
if layout_verified and layout_summary_verified
|
|
2826
2976
|
else "applied app layout; raw layout verified but compact summary is incomplete"
|
|
2827
2977
|
if layout_verified and not layout_summary_verified
|
|
2828
2978
|
else "applied app layout with flattened section fallback"
|
|
@@ -2848,7 +2998,7 @@ class AiBuilderFacade:
|
|
|
2848
2998
|
},
|
|
2849
2999
|
"verified": layout_verified,
|
|
2850
3000
|
}
|
|
2851
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3001
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
2852
3002
|
|
|
2853
3003
|
def app_flow_apply(
|
|
2854
3004
|
self,
|
|
@@ -2867,34 +3017,40 @@ class AiBuilderFacade:
|
|
|
2867
3017
|
"transitions": transitions,
|
|
2868
3018
|
"publish": publish,
|
|
2869
3019
|
}
|
|
2870
|
-
|
|
3020
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3021
|
+
permission_outcome = self._guard_app_permission(
|
|
2871
3022
|
profile=profile,
|
|
2872
3023
|
app_key=app_key,
|
|
2873
3024
|
required_permission="data_manage",
|
|
2874
3025
|
normalized_args=normalized_args,
|
|
2875
3026
|
)
|
|
2876
|
-
if
|
|
2877
|
-
return
|
|
3027
|
+
if permission_outcome.block is not None:
|
|
3028
|
+
return permission_outcome.block
|
|
3029
|
+
permission_outcomes.append(permission_outcome)
|
|
3030
|
+
|
|
3031
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3032
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3033
|
+
|
|
2878
3034
|
if mode != "replace":
|
|
2879
|
-
return _failed(
|
|
3035
|
+
return finalize(_failed(
|
|
2880
3036
|
"UNSUPPORTED_FLOW_MODE",
|
|
2881
3037
|
"only mode='replace' is supported",
|
|
2882
3038
|
normalized_args=normalized_args,
|
|
2883
3039
|
allowed_values={"modes": ["replace"]},
|
|
2884
3040
|
suggested_next_call={"tool_name": "app_flow_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
2885
|
-
)
|
|
3041
|
+
))
|
|
2886
3042
|
try:
|
|
2887
3043
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
2888
3044
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
2889
3045
|
except (QingflowApiError, RuntimeError) as error:
|
|
2890
3046
|
api_error = _coerce_api_error(error)
|
|
2891
|
-
return _failed_from_api_error(
|
|
3047
|
+
return finalize(_failed_from_api_error(
|
|
2892
3048
|
"FLOW_READ_FAILED",
|
|
2893
3049
|
api_error,
|
|
2894
3050
|
normalized_args=normalized_args,
|
|
2895
|
-
details={"app_key": app_key},
|
|
3051
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="workflow", error=api_error),
|
|
2896
3052
|
suggested_next_call={"tool_name": "app_read_flow_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
2897
|
-
)
|
|
3053
|
+
))
|
|
2898
3054
|
entity = _entity_spec_from_app(base_info=base, schema=schema, views=None)
|
|
2899
3055
|
current_fields = _parse_schema(schema)["fields"]
|
|
2900
3056
|
normalized_nodes, resolution_issues = self._normalize_flow_nodes(profile=profile, current_fields=current_fields, nodes=nodes)
|
|
@@ -3006,7 +3162,7 @@ class AiBuilderFacade:
|
|
|
3006
3162
|
suggested_next_call["tool_name"] = "app_flow_apply"
|
|
3007
3163
|
suggested_next_call["arguments"] = arguments
|
|
3008
3164
|
failed["suggested_next_call"] = suggested_next_call
|
|
3009
|
-
return failed
|
|
3165
|
+
return finalize(failed)
|
|
3010
3166
|
verified_nodes, verified_nodes_unavailable = self._load_workflow_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
3011
3167
|
workflow_structure_verified = bool(verified_nodes) and _workflow_nodes_semantically_equal(
|
|
3012
3168
|
current_workflow=verified_nodes,
|
|
@@ -3065,7 +3221,7 @@ class AiBuilderFacade:
|
|
|
3065
3221
|
"flow_diff": {"mode": "replace", "node_count": desired_node_count},
|
|
3066
3222
|
"verified": workflow_verified,
|
|
3067
3223
|
}
|
|
3068
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3224
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3069
3225
|
|
|
3070
3226
|
def app_views_apply(
|
|
3071
3227
|
self,
|
|
@@ -3102,27 +3258,33 @@ class AiBuilderFacade:
|
|
|
3102
3258
|
"verified": True,
|
|
3103
3259
|
}
|
|
3104
3260
|
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3105
|
-
|
|
3261
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3262
|
+
permission_outcome = self._guard_app_permission(
|
|
3106
3263
|
profile=profile,
|
|
3107
3264
|
app_key=app_key,
|
|
3108
3265
|
required_permission="data_manage",
|
|
3109
3266
|
normalized_args=normalized_args,
|
|
3110
3267
|
)
|
|
3111
|
-
if
|
|
3112
|
-
return
|
|
3268
|
+
if permission_outcome.block is not None:
|
|
3269
|
+
return permission_outcome.block
|
|
3270
|
+
permission_outcomes.append(permission_outcome)
|
|
3271
|
+
|
|
3272
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3273
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3274
|
+
|
|
3113
3275
|
try:
|
|
3114
3276
|
base = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
3115
3277
|
schema, _schema_source = self._read_schema_with_fallback(profile=profile, app_key=app_key)
|
|
3116
3278
|
existing_views, _views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=False)
|
|
3117
3279
|
except (QingflowApiError, RuntimeError) as error:
|
|
3118
3280
|
api_error = _coerce_api_error(error)
|
|
3119
|
-
return _failed_from_api_error(
|
|
3281
|
+
return finalize(_failed_from_api_error(
|
|
3120
3282
|
"VIEWS_READ_FAILED",
|
|
3121
3283
|
api_error,
|
|
3122
3284
|
normalized_args=normalized_args,
|
|
3123
|
-
details={"app_key": app_key},
|
|
3285
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
3124
3286
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3125
|
-
)
|
|
3287
|
+
))
|
|
3126
3288
|
existing_views = existing_views or []
|
|
3127
3289
|
existing_by_key: dict[str, dict[str, Any]] = {}
|
|
3128
3290
|
existing_by_name: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -3475,13 +3637,13 @@ class AiBuilderFacade:
|
|
|
3475
3637
|
verified_view_result, verified_views_unavailable = self._load_views_result(profile=profile, app_key=app_key, tolerate_404=True)
|
|
3476
3638
|
except (QingflowApiError, RuntimeError) as error:
|
|
3477
3639
|
api_error = _coerce_api_error(error)
|
|
3478
|
-
return _failed_from_api_error(
|
|
3640
|
+
return finalize(_failed_from_api_error(
|
|
3479
3641
|
"VIEWS_READ_FAILED",
|
|
3480
3642
|
api_error,
|
|
3481
3643
|
normalized_args=normalized_args,
|
|
3482
|
-
details={"app_key": app_key},
|
|
3644
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="views", error=api_error),
|
|
3483
3645
|
suggested_next_call={"tool_name": "app_read_views_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
3484
|
-
)
|
|
3646
|
+
))
|
|
3485
3647
|
verified_names = {
|
|
3486
3648
|
_extract_view_name(item)
|
|
3487
3649
|
for item in (verified_view_result or [])
|
|
@@ -3636,7 +3798,7 @@ class AiBuilderFacade:
|
|
|
3636
3798
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": failed_views},
|
|
3637
3799
|
"verified": verified and view_filters_verified,
|
|
3638
3800
|
}
|
|
3639
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3801
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3640
3802
|
warnings: list[dict[str, Any]] = []
|
|
3641
3803
|
if filter_readback_pending or filter_mismatches:
|
|
3642
3804
|
warnings.append(_warning("VIEW_FILTERS_UNVERIFIED", "view definitions were applied, but saved filter behavior is not fully verified"))
|
|
@@ -3670,7 +3832,7 @@ class AiBuilderFacade:
|
|
|
3670
3832
|
"views_diff": {"created": created, "updated": updated, "removed": removed, "failed": []},
|
|
3671
3833
|
"verified": verified and view_filters_verified,
|
|
3672
3834
|
}
|
|
3673
|
-
return self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response)
|
|
3835
|
+
return finalize(self._append_publish_result(profile=profile, app_key=app_key, publish=publish, response=response))
|
|
3674
3836
|
|
|
3675
3837
|
def app_publish_verify(
|
|
3676
3838
|
self,
|
|
@@ -3803,18 +3965,27 @@ class AiBuilderFacade:
|
|
|
3803
3965
|
|
|
3804
3966
|
def chart_apply(self, *, profile: str, request: ChartApplyRequest) -> JSONObject:
|
|
3805
3967
|
normalized_args = request.model_dump(mode="json")
|
|
3968
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
3806
3969
|
app_result = self.app_resolve(profile=profile, app_key=request.app_key)
|
|
3807
3970
|
if app_result.get("status") != "success":
|
|
3808
3971
|
return app_result
|
|
3972
|
+
resolved_outcome = _permission_outcome_from_result(app_result)
|
|
3973
|
+
if resolved_outcome is not None:
|
|
3974
|
+
permission_outcomes.append(resolved_outcome)
|
|
3809
3975
|
app_key = str(app_result.get("app_key") or request.app_key)
|
|
3810
|
-
|
|
3976
|
+
permission_outcome = self._guard_app_permission(
|
|
3811
3977
|
profile=profile,
|
|
3812
3978
|
app_key=app_key,
|
|
3813
3979
|
required_permission="data_manage",
|
|
3814
3980
|
normalized_args=normalized_args,
|
|
3815
3981
|
)
|
|
3816
|
-
if
|
|
3817
|
-
return
|
|
3982
|
+
if permission_outcome.block is not None:
|
|
3983
|
+
return permission_outcome.block
|
|
3984
|
+
permission_outcomes.append(permission_outcome)
|
|
3985
|
+
|
|
3986
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
3987
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
3988
|
+
|
|
3818
3989
|
try:
|
|
3819
3990
|
schema_state = self._load_base_schema_state(profile=profile, app_key=app_key)
|
|
3820
3991
|
parsed = schema_state.get("parsed") if isinstance(schema_state.get("parsed"), dict) else {}
|
|
@@ -3823,13 +3994,13 @@ class AiBuilderFacade:
|
|
|
3823
3994
|
existing_chart_items, existing_chart_list_source = self._load_chart_list_for_builder(profile=profile, app_key=app_key)
|
|
3824
3995
|
except (QingflowApiError, RuntimeError) as error:
|
|
3825
3996
|
api_error = _coerce_api_error(error)
|
|
3826
|
-
return _failed_from_api_error(
|
|
3997
|
+
return finalize(_failed_from_api_error(
|
|
3827
3998
|
"CHART_APPLY_FAILED",
|
|
3828
3999
|
api_error,
|
|
3829
4000
|
normalized_args=normalized_args,
|
|
3830
|
-
details={"app_key": app_key},
|
|
4001
|
+
details=_with_state_read_blocked_details({"app_key": app_key}, resource="chart", error=api_error),
|
|
3831
4002
|
suggested_next_call={"tool_name": "app_charts_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
3832
|
-
)
|
|
4003
|
+
))
|
|
3833
4004
|
|
|
3834
4005
|
field_lookup = _build_public_field_lookup(fields)
|
|
3835
4006
|
qingbi_fields_by_id = {
|
|
@@ -4069,7 +4240,7 @@ class AiBuilderFacade:
|
|
|
4069
4240
|
|
|
4070
4241
|
if failed_items:
|
|
4071
4242
|
successful_changes = bool(created_ids or updated_ids or removed_ids or reordered)
|
|
4072
|
-
return {
|
|
4243
|
+
return finalize({
|
|
4073
4244
|
"status": "partial_success" if successful_changes else "failed",
|
|
4074
4245
|
"error_code": "CHART_APPLY_PARTIAL" if successful_changes else "CHART_APPLY_FAILED",
|
|
4075
4246
|
"recoverable": True,
|
|
@@ -4097,9 +4268,9 @@ class AiBuilderFacade:
|
|
|
4097
4268
|
"app_key": app_key,
|
|
4098
4269
|
"chart_results": chart_results,
|
|
4099
4270
|
"verified": False if failed_items else verified,
|
|
4100
|
-
}
|
|
4271
|
+
})
|
|
4101
4272
|
result_verified = verified or noop
|
|
4102
|
-
return {
|
|
4273
|
+
return finalize({
|
|
4103
4274
|
"status": "success" if result_verified else "partial_success",
|
|
4104
4275
|
"error_code": None if result_verified else "CHART_READBACK_PENDING",
|
|
4105
4276
|
"recoverable": not result_verified,
|
|
@@ -4125,10 +4296,11 @@ class AiBuilderFacade:
|
|
|
4125
4296
|
"app_key": app_key,
|
|
4126
4297
|
"chart_results": chart_results,
|
|
4127
4298
|
"verified": result_verified,
|
|
4128
|
-
}
|
|
4299
|
+
})
|
|
4129
4300
|
|
|
4130
4301
|
def portal_apply(self, *, profile: str, request: PortalApplyRequest) -> JSONObject:
|
|
4131
4302
|
normalized_args = request.model_dump(mode="json")
|
|
4303
|
+
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
4132
4304
|
dash_key = str(request.dash_key or "").strip()
|
|
4133
4305
|
creating = not dash_key
|
|
4134
4306
|
verify_dash_name = creating or request.dash_name is not None
|
|
@@ -4141,24 +4313,31 @@ class AiBuilderFacade:
|
|
|
4141
4313
|
base_payload = self.portals.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") if dash_key else {}
|
|
4142
4314
|
except (QingflowApiError, RuntimeError) as error:
|
|
4143
4315
|
api_error = _coerce_api_error(error)
|
|
4144
|
-
return
|
|
4316
|
+
return _failed(
|
|
4145
4317
|
"PORTAL_APPLY_FAILED",
|
|
4146
|
-
api_error,
|
|
4318
|
+
_public_error_message("PORTAL_APPLY_FAILED", api_error),
|
|
4147
4319
|
normalized_args=normalized_args,
|
|
4148
|
-
details={"dash_key": dash_key or None},
|
|
4320
|
+
details=_with_state_read_blocked_details({"dash_key": dash_key or None}, resource="portal", error=api_error),
|
|
4149
4321
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4322
|
+
request_id=api_error.request_id,
|
|
4323
|
+
backend_code=api_error.backend_code,
|
|
4324
|
+
http_status=None if api_error.http_status == 404 else api_error.http_status,
|
|
4150
4325
|
)
|
|
4151
4326
|
if not isinstance(base_payload, dict):
|
|
4152
4327
|
base_payload = {}
|
|
4153
4328
|
if not creating:
|
|
4154
|
-
|
|
4329
|
+
portal_permission_outcome = self._guard_portal_permission(
|
|
4155
4330
|
profile=profile,
|
|
4156
4331
|
dash_key=dash_key,
|
|
4157
4332
|
normalized_args=normalized_args,
|
|
4158
4333
|
portal_result=base_payload,
|
|
4159
4334
|
)
|
|
4160
|
-
if
|
|
4161
|
-
return
|
|
4335
|
+
if portal_permission_outcome.block is not None:
|
|
4336
|
+
return portal_permission_outcome.block
|
|
4337
|
+
permission_outcomes.append(portal_permission_outcome)
|
|
4338
|
+
|
|
4339
|
+
def finalize(response: JSONObject) -> JSONObject:
|
|
4340
|
+
return _apply_permission_outcomes(response, *permission_outcomes)
|
|
4162
4341
|
target_package_tag_id = request.package_tag_id
|
|
4163
4342
|
if target_package_tag_id is None:
|
|
4164
4343
|
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 +4349,24 @@ class AiBuilderFacade:
|
|
|
4170
4349
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4171
4350
|
)
|
|
4172
4351
|
if creating:
|
|
4173
|
-
|
|
4352
|
+
package_add_outcome = self._guard_package_permission(
|
|
4174
4353
|
profile=profile,
|
|
4175
4354
|
tag_id=target_package_tag_id,
|
|
4176
4355
|
required_permission="add_app",
|
|
4177
4356
|
normalized_args=normalized_args,
|
|
4178
4357
|
)
|
|
4179
|
-
if
|
|
4180
|
-
return
|
|
4181
|
-
|
|
4358
|
+
if package_add_outcome.block is not None:
|
|
4359
|
+
return package_add_outcome.block
|
|
4360
|
+
permission_outcomes.append(package_add_outcome)
|
|
4361
|
+
package_edit_outcome = self._guard_package_permission(
|
|
4182
4362
|
profile=profile,
|
|
4183
4363
|
tag_id=target_package_tag_id,
|
|
4184
4364
|
required_permission="edit_app",
|
|
4185
4365
|
normalized_args=normalized_args,
|
|
4186
4366
|
)
|
|
4187
|
-
if
|
|
4188
|
-
return
|
|
4367
|
+
if package_edit_outcome.block is not None:
|
|
4368
|
+
return package_edit_outcome.block
|
|
4369
|
+
permission_outcomes.append(package_edit_outcome)
|
|
4189
4370
|
try:
|
|
4190
4371
|
if creating:
|
|
4191
4372
|
create_payload = _build_public_portal_base_payload(
|
|
@@ -4242,7 +4423,7 @@ class AiBuilderFacade:
|
|
|
4242
4423
|
"PORTAL_APPLY_FAILED",
|
|
4243
4424
|
_public_error_message("PORTAL_APPLY_FAILED", api_error) if api_error else str(error),
|
|
4244
4425
|
normalized_args=normalized_args,
|
|
4245
|
-
details={"dash_key": dash_key or None},
|
|
4426
|
+
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
4427
|
suggested_next_call={"tool_name": "portal_apply", "arguments": {"profile": profile, **normalized_args}},
|
|
4247
4428
|
request_id=api_error.request_id if api_error else None,
|
|
4248
4429
|
backend_code=api_error.backend_code if api_error else None,
|
|
@@ -4316,7 +4497,7 @@ class AiBuilderFacade:
|
|
|
4316
4497
|
live_meta_verified=live_meta_verified,
|
|
4317
4498
|
publish_requested=request.publish,
|
|
4318
4499
|
)
|
|
4319
|
-
return {
|
|
4500
|
+
return finalize({
|
|
4320
4501
|
"status": status,
|
|
4321
4502
|
"error_code": error_code,
|
|
4322
4503
|
"recoverable": not verified,
|
|
@@ -4347,7 +4528,7 @@ class AiBuilderFacade:
|
|
|
4347
4528
|
"verified": verified,
|
|
4348
4529
|
"draft_result": draft_result,
|
|
4349
4530
|
"live_result": live_result,
|
|
4350
|
-
}
|
|
4531
|
+
})
|
|
4351
4532
|
|
|
4352
4533
|
def _publish_current_edit_version(self, *, profile: str, app_key: str) -> JSONObject:
|
|
4353
4534
|
normalized_args = {"app_key": app_key}
|
|
@@ -4912,6 +5093,177 @@ def _failed_from_api_error(
|
|
|
4912
5093
|
)
|
|
4913
5094
|
|
|
4914
5095
|
|
|
5096
|
+
def _transport_error_payload(error: QingflowApiError) -> JSONObject:
|
|
5097
|
+
return {
|
|
5098
|
+
"http_status": error.http_status,
|
|
5099
|
+
"backend_code": error.backend_code,
|
|
5100
|
+
"category": error.category,
|
|
5101
|
+
"request_id": error.request_id,
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
|
|
5105
|
+
def _is_permission_restricted_api_error(error: QingflowApiError) -> bool:
|
|
5106
|
+
return error.backend_code in {40002, 40027}
|
|
5107
|
+
|
|
5108
|
+
|
|
5109
|
+
def _append_response_detail(details: JSONObject, *, key: str, value: Any) -> None:
|
|
5110
|
+
if value is None:
|
|
5111
|
+
return
|
|
5112
|
+
copied_value = deepcopy(value)
|
|
5113
|
+
existing = details.get(key)
|
|
5114
|
+
if existing is None:
|
|
5115
|
+
details[key] = copied_value
|
|
5116
|
+
return
|
|
5117
|
+
if isinstance(existing, list):
|
|
5118
|
+
existing.append(copied_value)
|
|
5119
|
+
return
|
|
5120
|
+
details[key] = [existing, copied_value]
|
|
5121
|
+
|
|
5122
|
+
|
|
5123
|
+
def _permission_skip_outcome(
|
|
5124
|
+
*,
|
|
5125
|
+
scope: str,
|
|
5126
|
+
target: JSONObject,
|
|
5127
|
+
required_permission: str | None,
|
|
5128
|
+
transport_error: JSONObject | None = None,
|
|
5129
|
+
permission_summary: JSONObject | None = None,
|
|
5130
|
+
) -> PermissionCheckOutcome:
|
|
5131
|
+
lookup_payload: JSONObject = {
|
|
5132
|
+
"scope": scope,
|
|
5133
|
+
"target": deepcopy(target),
|
|
5134
|
+
"required_permission": required_permission,
|
|
5135
|
+
}
|
|
5136
|
+
if transport_error:
|
|
5137
|
+
lookup_payload["transport_error"] = deepcopy(transport_error)
|
|
5138
|
+
if permission_summary:
|
|
5139
|
+
lookup_payload["permission_summary"] = deepcopy(permission_summary)
|
|
5140
|
+
warning_message = (
|
|
5141
|
+
"builder permission lookup was permission-restricted; continuing with downstream operations"
|
|
5142
|
+
if transport_error
|
|
5143
|
+
else "builder permission summary was incomplete; continuing with downstream operations"
|
|
5144
|
+
)
|
|
5145
|
+
return PermissionCheckOutcome(
|
|
5146
|
+
warnings=[
|
|
5147
|
+
_warning(
|
|
5148
|
+
"PERMISSION_CHECK_SKIPPED",
|
|
5149
|
+
warning_message,
|
|
5150
|
+
scope=scope,
|
|
5151
|
+
required_permission=required_permission,
|
|
5152
|
+
)
|
|
5153
|
+
],
|
|
5154
|
+
details={
|
|
5155
|
+
"lookup_permission_blocked": lookup_payload,
|
|
5156
|
+
"permission_check_skipped": True,
|
|
5157
|
+
},
|
|
5158
|
+
verification={"metadata_unverified": True},
|
|
5159
|
+
)
|
|
5160
|
+
|
|
5161
|
+
|
|
5162
|
+
def _verification_read_outcome(
|
|
5163
|
+
*,
|
|
5164
|
+
resource: str,
|
|
5165
|
+
target: JSONObject,
|
|
5166
|
+
transport_error: QingflowApiError,
|
|
5167
|
+
) -> PermissionCheckOutcome:
|
|
5168
|
+
return PermissionCheckOutcome(
|
|
5169
|
+
warnings=[
|
|
5170
|
+
_warning(
|
|
5171
|
+
"VERIFICATION_READ_UNAVAILABLE",
|
|
5172
|
+
"post-write verification readback was permission-restricted",
|
|
5173
|
+
resource=resource,
|
|
5174
|
+
)
|
|
5175
|
+
],
|
|
5176
|
+
details={
|
|
5177
|
+
"lookup_permission_blocked": {
|
|
5178
|
+
"scope": resource,
|
|
5179
|
+
"target": deepcopy(target),
|
|
5180
|
+
"phase": "verification",
|
|
5181
|
+
"transport_error": _transport_error_payload(transport_error),
|
|
5182
|
+
},
|
|
5183
|
+
"permission_check_skipped": True,
|
|
5184
|
+
},
|
|
5185
|
+
verification={"metadata_unverified": True},
|
|
5186
|
+
)
|
|
5187
|
+
|
|
5188
|
+
|
|
5189
|
+
def _permission_outcome_from_result(result: JSONObject) -> PermissionCheckOutcome | None:
|
|
5190
|
+
details = result.get("details") if isinstance(result.get("details"), dict) else {}
|
|
5191
|
+
verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
|
|
5192
|
+
warnings = result.get("warnings") if isinstance(result.get("warnings"), list) else []
|
|
5193
|
+
filtered_details: JSONObject = {}
|
|
5194
|
+
if details.get("lookup_permission_blocked") is not None:
|
|
5195
|
+
filtered_details["lookup_permission_blocked"] = deepcopy(details["lookup_permission_blocked"])
|
|
5196
|
+
if details.get("permission_check_skipped") is not None:
|
|
5197
|
+
filtered_details["permission_check_skipped"] = bool(details.get("permission_check_skipped"))
|
|
5198
|
+
filtered_verification: JSONObject = {}
|
|
5199
|
+
if verification.get("metadata_unverified") is not None:
|
|
5200
|
+
filtered_verification["metadata_unverified"] = bool(verification.get("metadata_unverified"))
|
|
5201
|
+
filtered_warnings = [
|
|
5202
|
+
deepcopy(item)
|
|
5203
|
+
for item in warnings
|
|
5204
|
+
if isinstance(item, dict) and str(item.get("code") or "") in {"PERMISSION_CHECK_SKIPPED", "VERIFICATION_READ_UNAVAILABLE"}
|
|
5205
|
+
]
|
|
5206
|
+
if not filtered_details and not filtered_verification and not filtered_warnings:
|
|
5207
|
+
return None
|
|
5208
|
+
return PermissionCheckOutcome(
|
|
5209
|
+
warnings=filtered_warnings,
|
|
5210
|
+
details=filtered_details,
|
|
5211
|
+
verification=filtered_verification,
|
|
5212
|
+
)
|
|
5213
|
+
|
|
5214
|
+
|
|
5215
|
+
def _apply_permission_outcomes(response: JSONObject, *outcomes: PermissionCheckOutcome) -> JSONObject:
|
|
5216
|
+
if not isinstance(response, dict):
|
|
5217
|
+
return response
|
|
5218
|
+
details = response.get("details")
|
|
5219
|
+
if not isinstance(details, dict):
|
|
5220
|
+
details = {}
|
|
5221
|
+
response["details"] = details
|
|
5222
|
+
verification = response.get("verification")
|
|
5223
|
+
if not isinstance(verification, dict):
|
|
5224
|
+
verification = {}
|
|
5225
|
+
response["verification"] = verification
|
|
5226
|
+
warnings = response.get("warnings")
|
|
5227
|
+
if not isinstance(warnings, list):
|
|
5228
|
+
warnings = []
|
|
5229
|
+
response["warnings"] = warnings
|
|
5230
|
+
for outcome in outcomes:
|
|
5231
|
+
if not isinstance(outcome, PermissionCheckOutcome):
|
|
5232
|
+
continue
|
|
5233
|
+
for key, value in outcome.details.items():
|
|
5234
|
+
if key in {"lookup_permission_blocked", "state_read_blocked"}:
|
|
5235
|
+
_append_response_detail(details, key=key, value=value)
|
|
5236
|
+
elif key == "permission_check_skipped":
|
|
5237
|
+
details[key] = bool(details.get(key)) or bool(value)
|
|
5238
|
+
elif key not in details:
|
|
5239
|
+
details[key] = deepcopy(value)
|
|
5240
|
+
for key, value in outcome.verification.items():
|
|
5241
|
+
verification[key] = deepcopy(value)
|
|
5242
|
+
for warning in outcome.warnings:
|
|
5243
|
+
if warning not in warnings:
|
|
5244
|
+
warnings.append(deepcopy(warning))
|
|
5245
|
+
return response
|
|
5246
|
+
|
|
5247
|
+
|
|
5248
|
+
def _with_state_read_blocked_details(
|
|
5249
|
+
details: JSONObject | None,
|
|
5250
|
+
*,
|
|
5251
|
+
resource: str,
|
|
5252
|
+
error: QingflowApiError,
|
|
5253
|
+
) -> JSONObject:
|
|
5254
|
+
merged = deepcopy(details) if isinstance(details, dict) else {}
|
|
5255
|
+
if _is_permission_restricted_api_error(error):
|
|
5256
|
+
_append_response_detail(
|
|
5257
|
+
merged,
|
|
5258
|
+
key="state_read_blocked",
|
|
5259
|
+
value={
|
|
5260
|
+
"resource": resource,
|
|
5261
|
+
"transport_error": _transport_error_payload(error),
|
|
5262
|
+
},
|
|
5263
|
+
)
|
|
5264
|
+
return merged
|
|
5265
|
+
|
|
5266
|
+
|
|
4915
5267
|
def _from_stage_failure(stage: JSONObject, *, fallback_tool: str) -> JSONObject:
|
|
4916
5268
|
return {
|
|
4917
5269
|
"status": "failed",
|
|
@@ -6944,17 +7296,22 @@ def _workflow_branch_structure_verified(*, current_workflow: Any, requested_node
|
|
|
6944
7296
|
|
|
6945
7297
|
def _workflow_nodes_semantically_equal(*, current_workflow: Any, requested_nodes: list[dict[str, Any]]) -> bool:
|
|
6946
7298
|
current_nodes = _summarize_workflow_nodes(current_workflow)
|
|
7299
|
+
current_effective = [
|
|
7300
|
+
node
|
|
7301
|
+
for node in current_nodes
|
|
7302
|
+
if _normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")) not in {"start", "end"}
|
|
7303
|
+
]
|
|
6947
7304
|
requested_effective = [
|
|
6948
7305
|
node for node in requested_nodes if str(node.get("type") or "") not in {"start", "end"}
|
|
6949
7306
|
]
|
|
6950
|
-
if not
|
|
7307
|
+
if not current_effective or not requested_effective or len(current_effective) != len(requested_effective):
|
|
6951
7308
|
return False
|
|
6952
7309
|
current_signatures = sorted(
|
|
6953
7310
|
(
|
|
6954
7311
|
_normalize_existing_workflow_node_type(node.get("type"), node.get("deal_type")),
|
|
6955
7312
|
str(node.get("name") or "").strip(),
|
|
6956
7313
|
)
|
|
6957
|
-
for node in
|
|
7314
|
+
for node in current_effective
|
|
6958
7315
|
)
|
|
6959
7316
|
requested_signatures = sorted(
|
|
6960
7317
|
(
|
|
@@ -6967,7 +7324,7 @@ def _workflow_nodes_semantically_equal(*, current_workflow: Any, requested_nodes
|
|
|
6967
7324
|
|
|
6968
7325
|
|
|
6969
7326
|
def _normalize_existing_workflow_node_type(raw_type: Any, deal_type: Any) -> str:
|
|
6970
|
-
normalized = str(raw_type
|
|
7327
|
+
normalized = "" if raw_type is None else str(raw_type).strip().lower()
|
|
6971
7328
|
if normalized in {"audit", "approve"}:
|
|
6972
7329
|
return "approve"
|
|
6973
7330
|
if normalized in {"fill", "copy", "branch", "condition", "webhook", "start", "end"}:
|