@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +41 -2
  50. package/src/qingflow_mcp/builder_facade/service.py +2743 -423
  51. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  52. package/src/qingflow_mcp/cli/commands/builder.py +30 -4
  53. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  54. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  55. package/src/qingflow_mcp/cli/commands/record.py +54 -11
  56. package/src/qingflow_mcp/cli/context.py +0 -3
  57. package/src/qingflow_mcp/cli/formatters.py +238 -8
  58. package/src/qingflow_mcp/cli/main.py +47 -3
  59. package/src/qingflow_mcp/errors.py +43 -2
  60. package/src/qingflow_mcp/public_surface.py +24 -16
  61. package/src/qingflow_mcp/response_trim.py +119 -12
  62. package/src/qingflow_mcp/server.py +17 -14
  63. package/src/qingflow_mcp/server_app_builder.py +29 -7
  64. package/src/qingflow_mcp/server_app_user.py +23 -24
  65. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  66. package/src/qingflow_mcp/solution/executor.py +112 -15
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
  68. package/src/qingflow_mcp/tools/app_tools.py +237 -51
  69. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  70. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  71. package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
  72. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  73. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  74. package/src/qingflow_mcp/tools/export_tools.py +230 -33
  75. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  76. package/src/qingflow_mcp/tools/import_tools.py +293 -40
  77. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  78. package/src/qingflow_mcp/tools/package_tools.py +134 -8
  79. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  80. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  81. package/src/qingflow_mcp/tools/record_tools.py +2305 -442
  82. package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
  83. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  84. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  85. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  86. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  87. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  88. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  89. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
@@ -5,6 +5,215 @@ import re
5
5
 
6
6
 
7
7
  DEFAULT_ICON_COLOR = "qing-orange"
8
+ WORKSPACE_ICON_COLORS: tuple[str, ...] = (
9
+ "qing-orange",
10
+ "yellow",
11
+ "green",
12
+ "emerald",
13
+ "blue",
14
+ "azure",
15
+ "indigo",
16
+ "qing-purple",
17
+ "purple",
18
+ "pink",
19
+ "red",
20
+ "orange",
21
+ )
22
+ WORKSPACE_ICON_NAMES: tuple[str, ...] = (
23
+ "user",
24
+ "user-group",
25
+ "user-remove",
26
+ "user-add",
27
+ "user-circle",
28
+ "base-camera",
29
+ "view-grid",
30
+ "inbox",
31
+ "inbox-in",
32
+ "share",
33
+ "sitemap",
34
+ "airplane",
35
+ "template",
36
+ "music-note",
37
+ "movie-play",
38
+ "clock",
39
+ "document",
40
+ "document-search",
41
+ "clipboard-check",
42
+ "document-download",
43
+ "document-text",
44
+ "clipboard-copy",
45
+ "presentation-chart-bar",
46
+ "chart-square-bar",
47
+ "database",
48
+ "server",
49
+ "calendar",
50
+ "mail",
51
+ "annotation",
52
+ "chat",
53
+ "bell",
54
+ "key",
55
+ "shopping-bag",
56
+ "download",
57
+ "eye",
58
+ "eye-off",
59
+ "emoji-happy",
60
+ "emoji-sad",
61
+ "sun",
62
+ "moon",
63
+ "cloud",
64
+ "lightning-bolt",
65
+ "fire",
66
+ "star",
67
+ "sparkles",
68
+ "heart",
69
+ "cake",
70
+ "gift",
71
+ "light-bulb",
72
+ "exclamation",
73
+ "cog",
74
+ "thumb-up",
75
+ "thumb-down",
76
+ "cloud-download",
77
+ "cloud-upload",
78
+ "printer",
79
+ "phone-incoming",
80
+ "phone-missed-call",
81
+ "terminal",
82
+ "search-circle",
83
+ "x-circle",
84
+ "check-circle",
85
+ "exclamation-circle",
86
+ "question-mark-circle",
87
+ "information-circle",
88
+ "academic-cap",
89
+ "briefcase",
90
+ "home",
91
+ "phone",
92
+ "photograph",
93
+ "puzzle",
94
+ "color-swatch",
95
+ "lock-open",
96
+ "lock-closed",
97
+ "shield-check",
98
+ "shield-exclamation",
99
+ "currency-dollar",
100
+ "currency-yen",
101
+ "globe",
102
+ "at-symbol",
103
+ "slack",
104
+ "microphone",
105
+ "speakerphone",
106
+ "trash",
107
+ "book-open",
108
+ "truck",
109
+ "filter",
110
+ "essetional-filter-search",
111
+ "essetional-filter-tick",
112
+ "table",
113
+ "calculator",
114
+ "location-radar",
115
+ "essetional-weight",
116
+ "school-award",
117
+ "comp-cloud-connection",
118
+ "comp-cloud-remove",
119
+ "comp-cpu-charge",
120
+ "comp-cpu-setting",
121
+ "comp-cpu",
122
+ "comp-devices",
123
+ "comp-driver-2",
124
+ "comp-driver-refresh",
125
+ "location-global",
126
+ "location-location",
127
+ "location-map",
128
+ "location-gps",
129
+ "essetional-ranking",
130
+ "chart-bar",
131
+ "business-graph",
132
+ "business-status-up",
133
+ "business-trend-down",
134
+ "business-trend-up",
135
+ "business-presention-chart",
136
+ "business-favorite-chart",
137
+ "business-health",
138
+ "receipt-refund",
139
+ "receipt-tax",
140
+ "money-receipt-2-1",
141
+ "money-transaction-minus",
142
+ "action-hourglass-full",
143
+ "action-work",
144
+ "bug-f",
145
+ "essetional-pet",
146
+ "files-folder",
147
+ "badge-check",
148
+ "money-wallet-1",
149
+ "money-ticket",
150
+ "money-money",
151
+ "money-tag",
152
+ "money-wallet-2",
153
+ "business-personalcard",
154
+ "car-airplane",
155
+ "car-bus",
156
+ "car-car",
157
+ "car-driving",
158
+ "car-gas-station",
159
+ "car-smart-car",
160
+ "car-ship",
161
+ "location-map-1",
162
+ "location-route-square",
163
+ "cone",
164
+ "design-brush-4",
165
+ "paint-roll",
166
+ "wrench-f",
167
+ "essetional-reserve",
168
+ "essetional-broom",
169
+ "design-brush-2",
170
+ "essetional-judge",
171
+ "design-bucket",
172
+ "palette",
173
+ "comp-electricity",
174
+ "vial",
175
+ "beaker",
176
+ "leaf-f",
177
+ "cursor-click",
178
+ "solid-search-alt-2",
179
+ "md-library",
180
+ "building-3",
181
+ "office-building",
182
+ "building-hospital",
183
+ "school",
184
+ "store",
185
+ "video-camera-vintage-f",
186
+ "comp-monitor",
187
+ "delivery-truck",
188
+ "delivery-box-1",
189
+ "delivery-box-add",
190
+ "delivery-box-remove",
191
+ "settings-setting-3",
192
+ "document-duplicate",
193
+ "essetional-flag-2",
194
+ "flag",
195
+ "icon-currency-dollar",
196
+ "clipboard-list",
197
+ "save-as",
198
+ "wifi",
199
+ "status-online",
200
+ "scissors",
201
+ "globe-alt",
202
+ "ban",
203
+ "finger-print",
204
+ "qrcode",
205
+ "paper-clip",
206
+ "translate",
207
+ "cube-transparent",
208
+ "variable",
209
+ "switch-vertical",
210
+ "sports-baseball",
211
+ "sports-basketball",
212
+ "sports-soccer",
213
+ "sports-football",
214
+ "sports-volleyball",
215
+ )
216
+ GENERIC_WORKSPACE_ICON_NAMES: tuple[str, ...] = ("template",)
8
217
  DEFAULT_ICON_STYLE_POOL: tuple[tuple[str, str], ...] = (
9
218
  ("briefcase", "qing-orange"),
10
219
  ("calendar", "emerald"),
@@ -115,6 +324,91 @@ def parse_workspace_icon(value: str | None) -> tuple[str | None, str | None, str
115
324
  return normalize_workspace_icon_name(stripped), None, None
116
325
 
117
326
 
327
+ def workspace_icon_config(value: str | None) -> dict[str, str | None]:
328
+ icon_name, icon_color, icon_text = parse_workspace_icon(value)
329
+ raw = str(value).strip() if value not in (None, "") else None
330
+ return {
331
+ "icon_name": icon_name,
332
+ "icon_color": icon_color,
333
+ "icon_text": icon_text,
334
+ "raw": raw,
335
+ }
336
+
337
+
338
+ def workspace_icon_catalog_payload() -> dict[str, object]:
339
+ return {
340
+ "icon_names": list(WORKSPACE_ICON_NAMES),
341
+ "icon_colors": list(WORKSPACE_ICON_COLORS),
342
+ "generic_icon_names": list(GENERIC_WORKSPACE_ICON_NAMES),
343
+ "notes": [
344
+ "Use explicit icon + color for app/package/portal creation.",
345
+ "Do not use template for newly created workspace resources.",
346
+ "The CLI validates candidates only; it does not infer an icon from business names.",
347
+ ],
348
+ "common_examples": {
349
+ "employee": ["business-personalcard", "user-group", "user"],
350
+ "task": ["clipboard-check", "action-work"],
351
+ "worklog": ["clock", "action-hourglass-full"],
352
+ "order": ["delivery-box-1", "shopping-bag"],
353
+ "payment": ["money-receipt-2-1", "money-wallet-1"],
354
+ "opportunity": ["business-graph", "business-trend-up"],
355
+ "dashboard": ["view-grid", "chart-square-bar", "presentation-chart-bar"],
356
+ },
357
+ }
358
+
359
+
360
+ def validate_workspace_icon_choice(
361
+ *,
362
+ icon: str | None,
363
+ color: str | None,
364
+ require_explicit: bool,
365
+ disallow_generic: bool,
366
+ ) -> tuple[bool, str | None, str | None, dict[str, object]]:
367
+ normalized_icon = _normalize_workspace_icon_candidate(icon)
368
+ normalized_color = str(color or "").strip() or None
369
+ details: dict[str, object] = {
370
+ "icon": icon,
371
+ "normalized_icon": normalized_icon,
372
+ "color": color,
373
+ "icon_catalog_command": "qingflow --json builder icon catalog",
374
+ }
375
+ if require_explicit and not normalized_icon:
376
+ return False, "WORKSPACE_ICON_REQUIRED", "icon is required when creating a workspace resource", details
377
+ if require_explicit and not normalized_color:
378
+ return False, "WORKSPACE_ICON_COLOR_REQUIRED", "color is required when creating a workspace resource", details
379
+ if normalized_icon and normalized_icon not in WORKSPACE_ICON_NAMES:
380
+ details["allowed_icon_names"] = list(WORKSPACE_ICON_NAMES)
381
+ return False, "WORKSPACE_ICON_NOT_FOUND", "icon is not in the workspace icon catalog", details
382
+ if normalized_color and normalized_color not in WORKSPACE_ICON_COLORS:
383
+ details["allowed_icon_colors"] = list(WORKSPACE_ICON_COLORS)
384
+ return False, "WORKSPACE_ICON_COLOR_NOT_FOUND", "color is not in the workspace icon color catalog", details
385
+ if disallow_generic and normalized_icon in GENERIC_WORKSPACE_ICON_NAMES:
386
+ details["generic_icon_names"] = list(GENERIC_WORKSPACE_ICON_NAMES)
387
+ return False, "GENERIC_WORKSPACE_ICON_NOT_ALLOWED", "template is a generic icon and is not allowed for new workspace resources", details
388
+ return True, None, None, details
389
+
390
+
391
+ def _normalize_workspace_icon_candidate(icon: str | None) -> str | None:
392
+ if not icon:
393
+ return None
394
+ raw = str(icon).strip()
395
+ if not raw:
396
+ return None
397
+ if _looks_like_icon_json(raw):
398
+ try:
399
+ payload = json.loads(raw)
400
+ except Exception:
401
+ return None
402
+ return _normalize_workspace_icon_candidate(payload.get("iconName"))
403
+ normalized = raw.lower()
404
+ if normalized in WORKSPACE_ICON_NAMES:
405
+ return normalized
406
+ legacy = LEGACY_EX_ICON_MAP.get(normalized)
407
+ if legacy in WORKSPACE_ICON_NAMES:
408
+ return legacy
409
+ return normalized
410
+
411
+
118
412
  def encode_workspace_icon_with_defaults(
119
413
  *,
120
414
  icon: str | None,
@@ -5,7 +5,7 @@ from copy import deepcopy
5
5
  from typing import Any
6
6
  from uuid import uuid4
7
7
 
8
- from ..errors import QingflowApiError
8
+ from ..errors import QingflowApiError, backend_code_int, is_auth_like_error
9
9
  from ..tools.app_tools import AppTools
10
10
  from ..tools.navigation_tools import NavigationTools
11
11
  from ..tools.package_tools import PackageTools
@@ -76,7 +76,8 @@ class SolutionExecutor:
76
76
  except Exception as exc: # noqa: BLE001
77
77
  store.record_step_failed(step.step_name, str(exc), debug_context=debug_context)
78
78
  return store.summary()
79
- store.mark_finished(status="success")
79
+ final_status = "partial_success" if _artifacts_have_post_write_readback_pending(store.data.get("artifacts", {})) else "success"
80
+ store.mark_finished(status=final_status)
80
81
  return store.summary()
81
82
 
82
83
  def _repair_start_index(self, compiled: CompiledSolution, store: RunArtifactStore) -> int:
@@ -203,6 +204,11 @@ class SolutionExecutor:
203
204
  dash_key = store.get_artifact("portal", "dash_key")
204
205
  if dash_key:
205
206
  self.portal_tools.portal_publish(profile=profile, dash_key=dash_key)
207
+ store.set_artifact(
208
+ "portal",
209
+ "publish",
210
+ {"published": True, "write_executed": True, "safe_to_retry": False},
211
+ )
206
212
  self._refresh_portal_artifact(profile=profile, store=store, being_draft=False, artifact_key="published_result")
207
213
  return
208
214
  if step_name == "publish.navigation" and publish and compiled.normalized_spec.publish_policy.navigation:
@@ -347,7 +353,32 @@ class SolutionExecutor:
347
353
  updated_items.insert(insert_at, item)
348
354
  self.package_tools.package_sort_items(profile=profile, tag_id=tag_id, tag_items=updated_items)
349
355
 
350
- verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
356
+ try:
357
+ verified_detail = _verify_package_attachment(self.package_tools, profile=profile, tag_id=tag_id, app_key=app_key)
358
+ except Exception as exc: # noqa: BLE001
359
+ api_error = _coerce_qingflow_error(exc)
360
+ if api_error is None or not _is_permission_restricted_error(api_error):
361
+ raise
362
+ store.set_artifact(
363
+ "package",
364
+ "attachment_readback",
365
+ _post_write_readback_artifact(
366
+ resource="package_attach",
367
+ target={"tag_id": tag_id, "app_key": app_key},
368
+ error=api_error,
369
+ ),
370
+ )
371
+ self._record_package_attachment(
372
+ store,
373
+ entity.entity_id,
374
+ app_artifact,
375
+ tag_id=tag_id,
376
+ attached=True,
377
+ reused=False,
378
+ readback_status="unavailable",
379
+ readback_verified=False,
380
+ )
381
+ return
351
382
  verified_result = verified_detail.get("result") if isinstance(verified_detail.get("result"), dict) else {}
352
383
  verified_items = [deepcopy(existing) for existing in verified_result.get("tagItems", []) if isinstance(existing, dict)]
353
384
  if not any(_package_item_app_key(existing) == app_key for existing in verified_items):
@@ -369,12 +400,18 @@ class SolutionExecutor:
369
400
  tag_id: int,
370
401
  attached: bool,
371
402
  reused: bool,
403
+ readback_status: str = "verified",
404
+ readback_verified: bool = True,
372
405
  ) -> None:
373
406
  next_artifact = deepcopy(app_artifact)
374
407
  next_artifact["package_attachment"] = {
375
408
  "tag_id": tag_id,
376
409
  "attached": attached,
377
410
  "reused": reused,
411
+ "readback_status": readback_status,
412
+ "readback_verified": readback_verified,
413
+ "write_executed": not reused,
414
+ "safe_to_retry": reused or not attached,
378
415
  }
379
416
  store.set_artifact("apps", entity_id, next_artifact)
380
417
 
@@ -698,12 +735,23 @@ class SolutionExecutor:
698
735
  api_error = _coerce_qingflow_error(exc)
699
736
  if api_error is None or not _is_permission_restricted_error(api_error):
700
737
  raise
701
- raise _required_state_read_blocked_error(
702
- resource="portal",
703
- message=f"portal update requires readable draft state for dash '{dash_key}'",
704
- error=api_error,
705
- details={"dash_key": dash_key},
706
- ) from exc
738
+ if not result:
739
+ raise _required_state_read_blocked_error(
740
+ resource="portal",
741
+ message=f"portal update requires readable draft state for dash '{dash_key}'",
742
+ error=api_error,
743
+ details={"dash_key": dash_key},
744
+ ) from exc
745
+ base_payload = {}
746
+ store.set_artifact(
747
+ "portal",
748
+ "draft_readback_before_update",
749
+ _post_write_readback_artifact(
750
+ resource="portal",
751
+ target={"dash_key": dash_key, "phase": "created_portal_draft_readback"},
752
+ error=api_error,
753
+ ),
754
+ )
707
755
  update_payload = self._resolve_portal_payload(compiled.portal_plan["update_payload"], store, base_payload=base_payload)
708
756
  self.portal_tools.portal_update(profile=profile, dash_key=dash_key, payload=update_payload)
709
757
  self._refresh_portal_artifact(profile=profile, store=store, being_draft=True, artifact_key="draft_result")
@@ -744,7 +792,19 @@ class SolutionExecutor:
744
792
  return
745
793
  try:
746
794
  result = self.portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
747
- except Exception: # noqa: BLE001
795
+ except Exception as exc: # noqa: BLE001
796
+ api_error = _coerce_qingflow_error(exc)
797
+ if api_error is None:
798
+ raise
799
+ store.set_artifact(
800
+ "portal",
801
+ f"{artifact_key}_readback",
802
+ _post_write_readback_artifact(
803
+ resource="portal",
804
+ target={"dash_key": dash_key, "being_draft": being_draft, "artifact_key": artifact_key},
805
+ error=api_error,
806
+ ),
807
+ )
748
808
  return
749
809
  store.set_artifact("portal", artifact_key, result)
750
810
  store.set_artifact("portal", "result", result)
@@ -2112,10 +2172,9 @@ def _has_explicit_workflow_global_settings(global_settings: dict[str, Any] | Non
2112
2172
 
2113
2173
 
2114
2174
  def _is_navigation_plugin_unavailable(error: QingflowApiError) -> bool:
2115
- try:
2116
- backend_code = int(error.backend_code)
2117
- except (TypeError, ValueError):
2118
- backend_code = None
2175
+ if is_auth_like_error(error):
2176
+ return False
2177
+ backend_code = backend_code_int(error)
2119
2178
  if backend_code != 50004:
2120
2179
  return False
2121
2180
  message = error.message or ""
@@ -2152,7 +2211,9 @@ def _coerce_qingflow_error(error: Exception) -> QingflowApiError | None:
2152
2211
 
2153
2212
 
2154
2213
  def _is_permission_restricted_error(error: QingflowApiError) -> bool:
2155
- return error.backend_code in {40002, 40027}
2214
+ if is_auth_like_error(error):
2215
+ return False
2216
+ return backend_code_int(error) in {40002, 40027}
2156
2217
 
2157
2218
 
2158
2219
  def _required_state_read_blocked_error(
@@ -2182,6 +2243,42 @@ def _required_state_read_blocked_error(
2182
2243
  )
2183
2244
 
2184
2245
 
2246
+ def _post_write_readback_artifact(
2247
+ *,
2248
+ resource: str,
2249
+ target: dict[str, Any],
2250
+ error: QingflowApiError,
2251
+ ) -> dict[str, Any]:
2252
+ return {
2253
+ "resource": resource,
2254
+ "target": deepcopy(target),
2255
+ "readback_status": "unavailable",
2256
+ "readback_verified": False,
2257
+ "write_executed": True,
2258
+ "safe_to_retry": False,
2259
+ "transport_error": {
2260
+ "http_status": error.http_status,
2261
+ "backend_code": error.backend_code,
2262
+ "category": error.category,
2263
+ "request_id": error.request_id,
2264
+ },
2265
+ }
2266
+
2267
+
2268
+ def _artifacts_have_post_write_readback_pending(value: Any) -> bool:
2269
+ if isinstance(value, dict):
2270
+ if (
2271
+ value.get("write_executed") is True
2272
+ and value.get("safe_to_retry") is False
2273
+ and value.get("readback_status") == "unavailable"
2274
+ ):
2275
+ return True
2276
+ return any(_artifacts_have_post_write_readback_pending(item) for item in value.values())
2277
+ if isinstance(value, list):
2278
+ return any(_artifacts_have_post_write_readback_pending(item) for item in value)
2279
+ return False
2280
+
2281
+
2185
2282
  def _portal_component_position(
2186
2283
  source_type: Any,
2187
2284
  *,