comfy-cli 1.20.0__tar.gz → 1.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/PKG-INFO +3 -2
  2. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/__main__.py +20 -10
  3. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/_lazy.py +4 -2
  4. comfy_cli-1.22.0/comfy_cli/agent/__init__.py +458 -0
  5. comfy_cli-1.22.0/comfy_cli/agent/command.py +330 -0
  6. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/builder_api.py +165 -26
  7. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/billing.py +13 -11
  8. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/command.py +1 -1
  9. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cmdline.py +22 -3
  10. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/comfy_client.py +30 -8
  11. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/_cloud_errors.py +87 -5
  12. comfy_cli-1.22.0/comfy_cli/command/assets_library.py +245 -0
  13. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build.py +623 -47
  14. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_push.py +45 -4
  15. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_spec.py +7 -1
  16. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_targets.py +1 -1
  17. comfy_cli-1.22.0/comfy_cli/command/build_upload_progress.py +369 -0
  18. comfy_cli-1.22.0/comfy_cli/command/build_validation.py +562 -0
  19. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/cloud_http.py +7 -2
  20. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy.py +34 -2
  21. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_compute.py +5 -1
  22. comfy_cli-1.22.0/comfy_cli/command/deploy_progress.py +374 -0
  23. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_read.py +11 -0
  24. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_refs.py +14 -5
  25. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run.py +13 -9
  26. comfy_cli-1.22.0/comfy_cli/command/deploy_runtime.py +204 -0
  27. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_status.py +165 -17
  28. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_types.py +18 -1
  29. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_up.py +91 -2
  30. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/app.py +104 -11
  31. comfy_cli-1.22.0/comfy_cli/command/generate/emit.py +668 -0
  32. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/schema.py +1 -1
  33. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/job_watcher.py +23 -1
  34. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/knowledge.py +75 -2
  35. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/launch.py +2 -2
  36. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/nodes.py +5 -5
  37. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/__init__.py +39 -6
  38. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/templates.py +138 -94
  39. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow.py +72 -6
  40. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow_edit.py +119 -4
  41. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/engine.py +711 -81
  42. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/promoted.py +9 -0
  43. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/widget_catalog.py +19 -3
  44. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/credentials.py +73 -0
  45. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_api.py +67 -11
  46. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_api_errors.py +17 -1
  47. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_jobs.py +33 -7
  48. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/discovery.py +12 -0
  49. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/error_codes.py +171 -14
  50. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/help_json.py +13 -4
  51. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/http.py +10 -1
  52. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/knowledge.py +4 -0
  53. comfy_cli-1.22.0/comfy_cli/layout.py +607 -0
  54. comfy_cli-1.22.0/comfy_cli/layout_quality.py +219 -0
  55. comfy_cli-1.22.0/comfy_cli/output/progress.py +68 -0
  56. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/renderer.py +77 -5
  57. comfy_cli-1.22.0/comfy_cli/schemas/agent.json +300 -0
  58. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_push.json +18 -0
  59. comfy_cli-1.22.0/comfy_cli/schemas/build_push_event.json +58 -0
  60. comfy_cli-1.22.0/comfy_cli/schemas/build_release_delete.json +12 -0
  61. comfy_cli-1.22.0/comfy_cli/schemas/build_validate.json +14 -0
  62. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/cloud_status.json +1 -1
  63. comfy_cli-1.22.0/comfy_cli/schemas/deploy_progress_event.json +94 -0
  64. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_refs_compute.json +2 -0
  65. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_status.json +33 -2
  66. comfy_cli-1.22.0/comfy_cli/schemas/deploy_up.json +95 -0
  67. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_list.json +8 -1
  68. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/knowledge.json +7 -2
  69. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/__init__.py +2 -0
  70. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy/SKILL.md +40 -9
  71. comfy_cli-1.22.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +92 -0
  72. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build/SKILL.md +95 -10
  73. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +11 -2
  74. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +55 -1
  75. comfy_cli-1.22.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +190 -0
  76. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-debug/SKILL.md +3 -0
  77. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-deploy/SKILL.md +86 -23
  78. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +13 -7
  79. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/tracking.py +95 -3
  80. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_ops.py +493 -46
  81. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_print.py +90 -3
  82. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_to_api.py +259 -108
  83. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/PKG-INFO +3 -2
  84. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/SOURCES.txt +12 -0
  85. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/requires.txt +2 -1
  86. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/pyproject.toml +2 -2
  87. comfy_cli-1.20.0/comfy_cli/command/assets_library.py +0 -132
  88. comfy_cli-1.20.0/comfy_cli/command/build_validation.py +0 -261
  89. comfy_cli-1.20.0/comfy_cli/command/deploy_runtime.py +0 -108
  90. comfy_cli-1.20.0/comfy_cli/command/generate/emit.py +0 -367
  91. comfy_cli-1.20.0/comfy_cli/layout.py +0 -180
  92. comfy_cli-1.20.0/comfy_cli/schemas/build_validate.json +0 -8
  93. comfy_cli-1.20.0/comfy_cli/schemas/deploy_up.json +0 -60
  94. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/LICENSE +0 -0
  95. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/README.md +0 -0
  96. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/__init__.py +0 -0
  97. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/_safe_exec.py +0 -0
  98. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/__init__.py +0 -0
  99. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/command.py +0 -0
  100. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/store.py +0 -0
  101. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/builder_pagination.py +0 -0
  102. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/caller.py +0 -0
  103. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cancellation.py +0 -0
  104. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/__init__.py +0 -0
  105. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/oauth.py +0 -0
  106. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/__init__.py +0 -0
  107. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_diff.py +0 -0
  108. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_digest_cache.py +0 -0
  109. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_package.py +0 -0
  110. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_paths.py +0 -0
  111. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_pull.py +0 -0
  112. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/code_search.py +0 -0
  113. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  114. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
  115. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  116. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/command.py +0 -0
  117. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
  118. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_ls.py +0 -0
  119. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_resolve.py +0 -0
  120. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run_assets.py +0 -0
  121. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run_watch.py +0 -0
  122. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_workflow.py +0 -0
  123. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/__init__.py +0 -0
  124. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/adapters.py +0 -0
  125. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/client.py +0 -0
  126. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/output.py +0 -0
  127. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/poll.py +0 -0
  128. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/spec/openapi.yml +0 -0
  129. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/spec.py +0 -0
  130. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/upload.py +0 -0
  131. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/github/pr_info.py +0 -0
  132. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/install.py +0 -0
  133. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/jobs.py +0 -0
  134. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/models/models.py +0 -0
  135. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/models/search.py +0 -0
  136. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/node_deps.py +0 -0
  137. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/outdated.py +0 -0
  138. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/pack_scan.py +0 -0
  139. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/pr_command.py +0 -0
  140. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/preview.py +0 -0
  141. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/project.py +0 -0
  142. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/execution.py +0 -0
  143. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/loader.py +0 -0
  144. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/preflight.py +0 -0
  145. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/watcher.py +0 -0
  146. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run_cli.py +0 -0
  147. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/setup.py +0 -0
  148. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/stop_port.py +0 -0
  149. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/system.py +0 -0
  150. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/transfer.py +0 -0
  151. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/version_validators.py +0 -0
  152. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow_fragments.py +0 -0
  153. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command_mentions.py +0 -0
  154. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/config_manager.py +0 -0
  155. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/constants.py +0 -0
  156. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/__init__.py +0 -0
  157. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/_net.py +0 -0
  158. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/annotations_source.py +0 -0
  159. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/__init__.py +0 -0
  160. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
  161. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/default_text2img.json +0 -0
  162. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
  163. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/default_workflow.py +0 -0
  164. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/errors.py +0 -0
  165. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/loader.py +0 -0
  166. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cuda_detect.py +0 -0
  167. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_assets.py +0 -0
  168. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_download.py +0 -0
  169. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_events.py +0 -0
  170. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deprecation.py +0 -0
  171. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/download_state.py +0 -0
  172. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/env_checker.py +0 -0
  173. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/execution_errors.py +0 -0
  174. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/file_utils.py +0 -0
  175. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/fragments.py +0 -0
  176. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/git_utils.py +0 -0
  177. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/hardware.py +0 -0
  178. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/hashing.py +0 -0
  179. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/host_port.py +0 -0
  180. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/interaction.py +0 -0
  181. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/jobs_state.py +0 -0
  182. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/local_address.py +0 -0
  183. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/locking.py +0 -0
  184. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/logging.py +0 -0
  185. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/onboarding.py +0 -0
  186. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/__init__.py +0 -0
  187. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/branding.py +0 -0
  188. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/glyphs.py +0 -0
  189. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/panels.py +0 -0
  190. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/preview.py +0 -0
  191. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/rich_compat.py +0 -0
  192. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/sanitize.py +0 -0
  193. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/pr_cache.py +0 -0
  194. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/project.py +0 -0
  195. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/__init__.py +0 -0
  196. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/api.py +0 -0
  197. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/config_parser.py +0 -0
  198. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/types.py +0 -0
  199. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/resolve_python.py +0 -0
  200. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/__init__.py +0 -0
  201. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/assets.json +0 -0
  202. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/assets_library.json +0 -0
  203. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/auth.json +0 -0
  204. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
  205. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_delete.json +0 -0
  206. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_init.json +0 -0
  207. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_ls.json +0 -0
  208. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_pull.json +0 -0
  209. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
  210. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
  211. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
  212. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
  213. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_create.json +0 -0
  214. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_logs.json +0 -0
  215. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_ls.json +0 -0
  216. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
  217. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_show.json +0 -0
  218. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_show.json +0 -0
  219. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_status.json +0 -0
  220. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_update.json +0 -0
  221. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_delete.json +0 -0
  222. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_events.json +0 -0
  223. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_logs.json +0 -0
  224. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_ls.json +0 -0
  225. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_run.json +0 -0
  226. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_scale.json +0 -0
  227. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_show.json +0 -0
  228. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_start.json +0 -0
  229. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_stop.json +0 -0
  230. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/discover.json +0 -0
  231. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/download.json +0 -0
  232. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/download_status.json +0 -0
  233. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/downloads.json +0 -0
  234. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/env.json +0 -0
  235. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/envelope.json +0 -0
  236. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/error.json +0 -0
  237. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/error_codes.md +0 -0
  238. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/free.json +0 -0
  239. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_result.json +0 -0
  240. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_schema.json +0 -0
  241. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/help.json +0 -0
  242. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/jobs.json +0 -0
  243. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/jobs_wait.json +0 -0
  244. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/knowledge_block.json +0 -0
  245. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/launch.json +0 -0
  246. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/logs.json +0 -0
  247. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/models.json +0 -0
  248. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/node_deps.json +0 -0
  249. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/nodes.json +0 -0
  250. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/outdated.json +0 -0
  251. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/preview.json +0 -0
  252. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/project.json +0 -0
  253. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/run.json +0 -0
  254. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/run_event.json +0 -0
  255. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/set_default.json +0 -0
  256. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/skill.json +0 -0
  257. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/stop.json +0 -0
  258. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/system_stats.json +0 -0
  259. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/templates.json +0 -0
  260. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/transfer.json +0 -0
  261. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/update.json +0 -0
  262. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/version.json +0 -0
  263. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/which.json +0 -0
  264. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/widget_catalog.json +0 -0
  265. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/workflow.json +0 -0
  266. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/selector.py +0 -0
  267. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy/__init__.py +0 -0
  268. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
  269. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
  270. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
  271. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/command.py +0 -0
  272. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/standalone.py +0 -0
  273. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/target.py +0 -0
  274. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/typing.py +0 -0
  275. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/ui.py +0 -0
  276. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/update.py +0 -0
  277. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/utils.py +0 -0
  278. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/uv.py +0 -0
  279. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/where.py +0 -0
  280. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workspace_manager.py +0 -0
  281. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
  282. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/entry_points.txt +0 -0
  283. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/top_level.txt +0 -0
  284. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/setup.cfg +0 -0
  285. {comfy_cli-1.20.0 → comfy_cli-1.22.0}/tests/test_file_utils_network.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-cli
3
- Version: 1.20.0
3
+ Version: 1.22.0
4
4
  Summary: A CLI tool for installing and using ComfyUI.
5
5
  Maintainer-email: Yoland Yan <yoland@drip.art>, James Kwon <hongilkwon316@gmail.com>, Robin Huang <robin@drip.art>, "Dr.Lt.Data" <dr.lt.data@gmail.com>
6
6
  License: GPL-3.0-only
@@ -45,7 +45,8 @@ Requires-Dist: jsonschema; extra == "dev"
45
45
  Requires-Dist: pre-commit; extra == "dev"
46
46
  Requires-Dist: pytest; extra == "dev"
47
47
  Requires-Dist: pytest-cov; extra == "dev"
48
- Requires-Dist: ruff; extra == "dev"
48
+ Requires-Dist: ruff==0.15.15; extra == "dev"
49
+ Requires-Dist: typer>=0.24; extra == "dev"
49
50
  Provides-Extra: preview
50
51
  Requires-Dist: term-image<0.8,>=0.7; extra == "preview"
51
52
  Dynamic: license-file
@@ -66,17 +66,24 @@ def _broken_pipe_swallowed_by_click() -> bool:
66
66
  """Report whether the `SystemExit` we just caught is click's EPIPE bail-out.
67
67
 
68
68
  click's `BaseCommand.main` handles EPIPE itself: it swaps `sys.stdout` and
69
- `sys.stderr` for `click.utils.PacifyFlushWrapper` and raises
70
- `SystemExit(1)`. So on the common path a broken pipe never reaches us as a
71
- `BrokenPipeError` at all, and the exit 1 is otherwise indistinguishable
72
- from a genuine command failure. That wrapper — which click installs in this
73
- branch and nowhere else — is the one reliable in-process signal.
69
+ `sys.stderr` for a `PacifyFlushWrapper` and raises `SystemExit(1)`. So on
70
+ the common path a broken pipe never reaches us as a `BrokenPipeError` at
71
+ all, and the exit 1 is otherwise indistinguishable from a genuine command
72
+ failure. That wrapper — which click installs in this branch and nowhere
73
+ else — is the one reliable in-process signal.
74
+
75
+ The class is matched by name and owning package because there is no single
76
+ class to point at: typer >= 0.24 runs on a vendored click, so the wrapper the
77
+ CLI actually meets is `typer._click.utils.PacifyFlushWrapper` rather than the
78
+ one in the installed `click`, and click 8.5 renamed its own copy to a private
79
+ name whose public alias warns on access. The package test keeps a same-named
80
+ class from anywhere else — `my_click_adapter.PacifyFlushWrapper` — out, since
81
+ a false positive here reports a failed command as a success.
74
82
  """
75
- try:
76
- from click.utils import PacifyFlushWrapper
77
- except ImportError: # pragma: no cover — click ships as a typer dependency
83
+ cls = type(sys.stdout)
84
+ if cls.__name__.removeprefix("_") != "PacifyFlushWrapper":
78
85
  return False
79
- return isinstance(sys.stdout, PacifyFlushWrapper)
86
+ return cls.__module__.partition(".")[0] in ("click", "typer")
80
87
 
81
88
 
82
89
  def _flush_stdout(*, unwinding: bool) -> bool:
@@ -127,7 +134,10 @@ def main() -> None:
127
134
  # `BrokenPipeError` reaching here is stdout dying outside that guard.
128
135
  _exit_quietly_on_broken_pipe()
129
136
  except SystemExit as exc:
130
- if _broken_pipe_swallowed_by_click():
137
+ # `exc.code == 1` narrows the shortcut to the only code click's EPIPE
138
+ # branch raises, so no other exit status can be laundered into a 0 by a
139
+ # stdout that merely looks pacified.
140
+ if exc.code == 1 and _broken_pipe_swallowed_by_click():
131
141
  _exit_quietly_on_broken_pipe()
132
142
  # stdout is block-buffered when it is a pipe, so the write that actually
133
143
  # lands on the closed reader is frequently the interpreter's shutdown
@@ -28,13 +28,15 @@ import inspect
28
28
  from collections.abc import Callable
29
29
  from dataclasses import dataclass
30
30
  from types import ModuleType
31
- from typing import Any
31
+ from typing import TYPE_CHECKING, Any
32
32
 
33
- import click
34
33
  import typer
35
34
  from typer.core import TyperGroup
36
35
  from typer.models import CommandInfo, TyperInfo
37
36
 
37
+ if TYPE_CHECKING:
38
+ import click
39
+
38
40
 
39
41
  class LazyModule:
40
42
  """Proxy for a module that is imported on first attribute access.
@@ -0,0 +1,458 @@
1
+ """``comfy agent`` — read and extend the local comfy agent's approvals.
2
+
3
+ The local agent (the ``comfy-agent`` binary on the user's own machine) runs
4
+ every command in a sandbox and every file tool behind a fence. What the user
5
+ lets it reach beyond the defaults lives in two files in its data dir:
6
+
7
+ - ``permissions.json`` — folders (``{"paths": [{"path", "reason", "approved_at"}]}``)
8
+ and the agent's own requests waiting for a human
9
+ (``{"pending": [{"id", "kind", "target", "reason", "requested_at"}]}``)
10
+ - ``egress-allow.json`` — hosts (``{"hosts": [{"host", "reason", "approved_at"}]}``)
11
+
12
+ The agent's ``request_path`` / ``request_host`` tools only append to
13
+ ``pending``: the model can ask, and injected content in a workflow can make it
14
+ ask, but nothing it can call moves a target into an approved list. A human
15
+ does that through a channel the model cannot reach — this module from a
16
+ terminal (``comfy agent allow --approve <id>`` / ``comfy agent deny <id>``), or
17
+ a panel button — after vetting the exact target the request names. The same
18
+ module also grants a folder or host outright (``comfy agent allow --path`` /
19
+ ``--host``). A running agent re-reads both files every ~20 s; otherwise they
20
+ apply at its next start. The data dir is ``AGENT_DATA_DIR`` when set, else
21
+ ``~/.comfy-agent`` — the same rule the agent and its launcher use.
22
+
23
+ The checks here are a courtesy, not the fence: the agent's sandbox deny list
24
+ and egress proxy decide what a grant can open, and they win over anything
25
+ written to these files. Vetting here means a grant the agent would never
26
+ honour is refused with a reason instead of recorded and silently ignored.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import ipaddress
32
+ import json
33
+ import os
34
+ import re
35
+ import sys
36
+ from dataclasses import dataclass
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+ from urllib.parse import urlsplit
40
+
41
+ from comfy_cli.file_utils import atomic_write_text
42
+
43
+ PERMISSIONS_FILE = "permissions.json"
44
+ EGRESS_ALLOW_FILE = "egress-allow.json"
45
+ DISCOVERY_FILE = "agent.json"
46
+
47
+
48
+ class StateError(ValueError):
49
+ """A file in the agent's data dir cannot be read, or is not the shape the agent writes."""
50
+
51
+
52
+ class UnknownRequestError(ValueError):
53
+ """No pending request carries the id given (already approved or denied, or never asked)."""
54
+
55
+
56
+ # A request id as the agent mints it: 16 lower-case hex characters.
57
+ _REQUEST_ID = re.compile(r"^[0-9a-f]{16}$")
58
+ _REQUEST_KINDS = ("path", "host")
59
+
60
+
61
+ # Folders a terminal grant may never name, in either direction. This mirrors
62
+ # the agent's own sandbox deny list (``CredentialDenyDirs``) — the credential
63
+ # and browser-profile stores of every platform, denied on every platform so a
64
+ # grant vetted on one OS means the same on another — plus ``.azure`` and the
65
+ # whole of ``.docker``, where the CLI is stricter than the agent. A folder that
66
+ # contains one of these (the home folder, above all) is refused whether or not
67
+ # the store exists yet: ``ssh-keygen`` or ``aws configure`` would otherwise
68
+ # create keys inside a folder the agent already reaches.
69
+ _DENIED_HOME_SUBDIRS = (
70
+ # Every platform: dotfile stores and tokens for services the agent's egress
71
+ # allow list lets a child reach (Hugging Face, GitHub, PyPI, npm, Docker).
72
+ ".ssh",
73
+ ".aws",
74
+ ".gnupg",
75
+ ".azure",
76
+ ".config/gcloud",
77
+ ".kube",
78
+ ".docker",
79
+ ".netrc",
80
+ ".git-credentials",
81
+ ".pypirc",
82
+ ".npmrc",
83
+ ".config/gh",
84
+ ".cache/huggingface/token",
85
+ # The agent's and the CLI's own state (tokens, sessions, the bearer token).
86
+ ".comfy-agent",
87
+ ".comfy-cli",
88
+ ".config/comfy-cli",
89
+ "Library/Application Support/comfy-cli",
90
+ "AppData/Roaming/comfy-cli",
91
+ "AppData/Local/comfy-cli",
92
+ # macOS keychain and browser profiles.
93
+ "Library/Keychains",
94
+ "Library/Application Support/Google/Chrome",
95
+ "Library/Application Support/Firefox",
96
+ "Library/Safari",
97
+ "Library/Containers/com.apple.Safari",
98
+ # Linux browser profiles and keyrings.
99
+ ".mozilla/firefox",
100
+ ".config/google-chrome",
101
+ ".config/chromium",
102
+ ".local/share/keyrings",
103
+ # Windows browser profiles, gcloud and the DPAPI credential vaults.
104
+ "AppData/Roaming/Mozilla/Firefox/Profiles",
105
+ "AppData/Local/Google/Chrome/User Data",
106
+ "AppData/Local/Microsoft/Edge/User Data",
107
+ "AppData/Roaming/gcloud",
108
+ "AppData/Roaming/Microsoft/Credentials",
109
+ "AppData/Local/Microsoft/Credentials",
110
+ )
111
+
112
+ # A name the agent denies wherever it lives (its ``.env`` rule, applied to
113
+ # every path component): a folder called ``.env`` cannot be granted, and a
114
+ # ``.env`` file inside an approved folder stays unreadable regardless.
115
+ _DENIED_NAME = re.compile(r"^\.env(\..*)?$|^\.envrc$", re.IGNORECASE)
116
+
117
+ # Hosts the agent's egress proxy refuses even when they are in the allow file
118
+ # (``sandboxKnownRefusedHosts`` in the cloud repo): Comfy's telemetry endpoints
119
+ # and the shared object-storage roots anyone can create a bucket on. Recording
120
+ # one would change nothing at the proxy, so it is refused here with the reason.
121
+ _REFUSED_HOSTS = {
122
+ "t.comfy.org": "a telemetry endpoint",
123
+ "api.mixpanel.com": "a telemetry endpoint",
124
+ "r2.cloudflarestorage.com": "shared object storage anyone can create a bucket on",
125
+ "storage.googleapis.com": "shared object storage anyone can create a bucket on",
126
+ }
127
+
128
+ # One DNS label: letters, digits and inner hyphens, at most 63 characters.
129
+ _LABEL = re.compile(r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$")
130
+
131
+
132
+ def data_dir(explicit: str | None = None) -> Path:
133
+ """The agent's data dir: ``--data-dir``, else ``AGENT_DATA_DIR``, else ``~/.comfy-agent``."""
134
+ if explicit:
135
+ return Path(explicit).expanduser()
136
+ env = os.environ.get("AGENT_DATA_DIR", "").strip()
137
+ if env:
138
+ return Path(env).expanduser()
139
+ return Path.home() / ".comfy-agent"
140
+
141
+
142
+ def _read_json(path: Path) -> dict:
143
+ """The file as a JSON object, ``{}`` when absent; :class:`StateError` when unreadable or not an object."""
144
+ if not path.is_file():
145
+ return {}
146
+ try:
147
+ data = json.loads(path.read_text(encoding="utf-8"))
148
+ except (OSError, ValueError) as exc:
149
+ raise StateError(f"{path}: cannot read: {exc}") from exc
150
+ if not isinstance(data, dict):
151
+ raise StateError(f"{path}: expected a JSON object, found {type(data).__name__}")
152
+ return data
153
+
154
+
155
+ def _load(path: Path, key: str) -> tuple[dict, list[dict]]:
156
+ """The file's object and its ``key`` list of entries; a non-list or a non-object entry is a broken file."""
157
+ data = _read_json(path)
158
+ raw = data.get(key)
159
+ if raw is None:
160
+ return data, []
161
+ if not isinstance(raw, list) or not all(isinstance(e, dict) for e in raw):
162
+ raise StateError(f"{path}: {key!r} must be a list of objects")
163
+ return data, list(raw)
164
+
165
+
166
+ def _is_request(e: object) -> bool:
167
+ return (
168
+ isinstance(e, dict)
169
+ and isinstance(e.get("id"), str)
170
+ and bool(_REQUEST_ID.match(e["id"]))
171
+ and e.get("kind") in _REQUEST_KINDS
172
+ and isinstance(e.get("target"), str)
173
+ )
174
+
175
+
176
+ def _pending(data: dict) -> list[dict]:
177
+ """The well-formed requests in a permissions object; a malformed entry or a non-list is skipped, not an error.
178
+
179
+ A request the agent wrote badly is nothing a human can act on, and a
180
+ broken ``pending`` must not lock ``comfy agent permissions`` out of the
181
+ approvals the same file holds.
182
+ """
183
+ raw = data.get("pending")
184
+ if not isinstance(raw, list):
185
+ return []
186
+ return [e for e in raw if _is_request(e)]
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class AgentState:
191
+ data_dir: Path
192
+ running: bool
193
+ port: int | None
194
+ paths: list[dict]
195
+ hosts: list[dict]
196
+ pending: list[dict]
197
+
198
+
199
+ def read_state(root: Path) -> AgentState:
200
+ """What the data dir says: whether an agent published itself, the approvals, and the requests waiting.
201
+
202
+ Raises :class:`StateError` when any of the three files is unreadable or
203
+ not the shape the agent writes.
204
+ """
205
+ disc = _read_json(root / DISCOVERY_FILE)
206
+ raw_port = disc.get("port")
207
+ # bool is an int subclass; a JSON true must not become port 1.
208
+ port = raw_port if type(raw_port) is int and 1 <= raw_port <= 65535 else None
209
+ perms, paths = _load(root / PERMISSIONS_FILE, "paths")
210
+ _, hosts = _load(root / EGRESS_ALLOW_FILE, "hosts")
211
+ return AgentState(data_dir=root, running=bool(disc), port=port, paths=paths, hosts=hosts, pending=_pending(perms))
212
+
213
+
214
+ def _now() -> str:
215
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
216
+
217
+
218
+ def _fold(p: Path) -> str:
219
+ """A path in the form its filesystem compares: case folded on Windows and macOS.
220
+
221
+ The default macOS volume (APFS, HFS+) is case-insensitive, and ``resolve``
222
+ does not correct case on POSIX, so ``~/.SSH`` names the same folder as
223
+ ``~/.ssh`` there. Folding on darwin closes that; on a case-sensitive volume
224
+ it only refuses a little more.
225
+ """
226
+ s = os.path.normcase(str(p))
227
+ if sys.platform == "darwin":
228
+ s = s.lower()
229
+ return s
230
+
231
+
232
+ def _same_or_under(child: Path, parent: Path) -> bool:
233
+ c, p = _fold(child), _fold(parent)
234
+ if c == p:
235
+ return True
236
+ return c.startswith(p.rstrip(os.sep) + os.sep)
237
+
238
+
239
+ def vet_path(raw: str, root: Path | None = None) -> Path:
240
+ """An approvable folder: absolute, existing, not the disk, not a credential store or a parent of one.
241
+
242
+ Returns the folder with symlinks resolved: the checks below run on the real
243
+ location, the way the agent's own fence does, so a link named elsewhere
244
+ that points into a credential store is refused and what gets recorded is
245
+ the folder the agent will actually open. ``root`` is the agent's data dir:
246
+ it, anything under it, and any folder that contains it are refused, so a
247
+ grant can never cover the agent's own state (``--data-dir /tmp/x --path /tmp``).
248
+ """
249
+ raw = raw.strip()
250
+ if not raw:
251
+ raise ValueError("a folder is required")
252
+ p = Path(raw).expanduser()
253
+ if not p.is_absolute():
254
+ raise ValueError(f"{raw!r} is not an absolute path")
255
+ p = Path(os.path.normpath(p))
256
+ try:
257
+ # is_dir() raises on EACCES on Python 3.10–3.12 rather than answering False.
258
+ if not p.is_dir():
259
+ raise ValueError(f"{p} is not an existing folder")
260
+ p = p.resolve(strict=True)
261
+ except OSError as exc:
262
+ raise ValueError(f"{p} cannot be opened: {exc}") from exc
263
+ if p.parent == p:
264
+ raise ValueError(f"{p} is the whole filesystem and stays closed")
265
+ for part in p.parts:
266
+ if _DENIED_NAME.match(part):
267
+ raise ValueError(f"{p} stays closed: a .env name is denied wherever it lives")
268
+ home = Path.home()
269
+ for rel in _DENIED_HOME_SUBDIRS:
270
+ # Both spellings: the lexical one and, when the store is a symlink to
271
+ # somewhere else, the folder it really is.
272
+ lexical = Path(os.path.normpath(home / rel))
273
+ for denied in {lexical, lexical.resolve()}:
274
+ if _same_or_under(p, denied):
275
+ raise ValueError(f"{p} stays closed: it is a credential or agent-state folder")
276
+ if _same_or_under(denied, p):
277
+ raise ValueError(f"{p} stays closed: it contains {denied}, a credential or agent-state folder")
278
+ if root is not None:
279
+ agent_root = Path(os.path.normpath(root.expanduser()))
280
+ try:
281
+ agent_root = agent_root.resolve()
282
+ except OSError:
283
+ pass
284
+ if _same_or_under(p, agent_root) or _same_or_under(agent_root, p):
285
+ raise ValueError(f"{p} stays closed: it is, contains, or is inside the agent's own data dir {agent_root}")
286
+ return p
287
+
288
+
289
+ def _as_ip(h: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
290
+ try:
291
+ return ipaddress.ip_address(h)
292
+ except ValueError:
293
+ return None
294
+
295
+
296
+ def vet_host(raw: str) -> str:
297
+ """One host the agent's proxy could match: a full host name or a routable address, lower-case, bare.
298
+
299
+ The proxy matches an approved host exactly (no wildcards, no suffixes), so
300
+ ``*`` and a bare top-level domain would record nothing it could ever use;
301
+ loopback and link-local addresses would open this machine's own services
302
+ and cloud metadata endpoints through the proxy; and the known-refused hosts
303
+ are refused there whatever the file says. Each is refused here with why.
304
+ """
305
+ h = raw.strip()
306
+ if not h:
307
+ raise ValueError("a host is required")
308
+ if "://" in h:
309
+ parts = urlsplit(h)
310
+ if parts.username is not None or parts.password is not None:
311
+ raise ValueError(f"{raw!r} carries credentials; give the host name only")
312
+ h = parts.hostname or ""
313
+ else:
314
+ if "@" in h:
315
+ raise ValueError(f"{raw!r} carries credentials (user@host); give the host name only")
316
+ h = h.split("/", 1)[0]
317
+ if h.startswith("["):
318
+ h = h[1:].split("]", 1)[0]
319
+ elif h.count(":") == 1:
320
+ h = h.split(":", 1)[0]
321
+ h = h.strip().lower().rstrip(".")
322
+ if not h:
323
+ raise ValueError(f"{raw!r} is not a host name")
324
+ if "," in h:
325
+ raise ValueError(f"{raw!r} names more than one host; pass one --host per host")
326
+ if "*" in h:
327
+ raise ValueError(f"{raw!r} is a wildcard; the agent matches an approved host exactly, so name the host")
328
+ ip = _as_ip(h)
329
+ if ip is not None:
330
+ if ip.is_loopback or ip.is_link_local or ip.is_unspecified or ip.is_multicast or ip.is_reserved:
331
+ raise ValueError(
332
+ f"{h} is a loopback, link-local or reserved address: this machine's own services "
333
+ "and cloud metadata endpoints are never opened through the agent's proxy"
334
+ )
335
+ return str(ip)
336
+ try:
337
+ h = h.encode("idna").decode("ascii")
338
+ except UnicodeError as exc:
339
+ raise ValueError(f"{raw!r} is not a host name") from exc
340
+ labels = h.split(".")
341
+ if len(labels) < 2 or h == "localhost":
342
+ raise ValueError(
343
+ f"{h!r} has no domain: a bare name or top-level domain cannot be allowed; give the full host name"
344
+ )
345
+ if len(h) > 253 or not all(_LABEL.match(label) for label in labels):
346
+ raise ValueError(f"{raw!r} is not a host name")
347
+ if h in _REFUSED_HOSTS:
348
+ raise ValueError(f"{h} stays refused: it is {_REFUSED_HOSTS[h]}, and the agent never opens it")
349
+ return h
350
+
351
+
352
+ def allow_path(root: Path, folder: str, reason: str) -> tuple[Path, bool]:
353
+ """Record an approved folder. Returns (folder, added); added is False when it was already there.
354
+
355
+ Raises ``ValueError`` for a folder that cannot be approved,
356
+ :class:`StateError` for a permissions file the agent would not load, and
357
+ ``OSError`` when the data dir cannot be written.
358
+ """
359
+ p = vet_path(folder, root=root)
360
+ file = root / PERMISSIONS_FILE
361
+ data, entries = _load(file, "paths")
362
+ if any(_same_path(str(e.get("path", "")), str(p)) for e in entries):
363
+ return p, False
364
+ entries.append({"path": str(p), "reason": reason, "approved_at": _now()})
365
+ # Keep whatever else the agent put at the top level; only the list changes.
366
+ _write(file, {**data, "paths": entries})
367
+ return p, True
368
+
369
+
370
+ def allow_host(root: Path, host: str, reason: str) -> tuple[str, bool]:
371
+ """Record an approved host. Returns (host, added). Raises as :func:`allow_path` does."""
372
+ h = vet_host(host)
373
+ file = root / EGRESS_ALLOW_FILE
374
+ data, entries = _load(file, "hosts")
375
+ if any(str(e.get("host", "")).lower() == h for e in entries):
376
+ return h, False
377
+ entries.append({"host": h, "reason": reason, "approved_at": _now()})
378
+ _write(file, {**data, "hosts": entries})
379
+ return h, True
380
+
381
+
382
+ def _take_request(root: Path, request_id: str) -> tuple[dict, list[dict], dict, list]:
383
+ """The permissions object, its ``paths``, the request with ``request_id``, and ``pending`` without it.
384
+
385
+ Raises :class:`UnknownRequestError` when no well-formed request carries
386
+ the id, :class:`StateError` for a permissions file the agent would not load.
387
+ """
388
+ data, paths = _load(root / PERMISSIONS_FILE, "paths")
389
+ match = [e for e in _pending(data) if e["id"] == request_id]
390
+ if not match:
391
+ raise UnknownRequestError(f"no pending request {request_id!r} in {root / PERMISSIONS_FILE}")
392
+ # Only the matched entry leaves the list: another request, or an entry
393
+ # the agent wrote in a shape this CLI does not read, stays as it was.
394
+ rest = [e for e in data["pending"] if not (isinstance(e, dict) and e.get("id") == request_id)]
395
+ return data, paths, match[0], rest
396
+
397
+
398
+ def _request_reason(req: dict) -> str:
399
+ reason = req.get("reason")
400
+ return reason if isinstance(reason, str) and reason.strip() else "approved with comfy agent allow --approve"
401
+
402
+
403
+ def approve(root: Path, request_id: str) -> tuple[dict, str]:
404
+ """Approve one pending request: vet its target, record it, drop the request.
405
+
406
+ Returns the entry now in the approvals file (``{"path"|"host", "reason",
407
+ "approved_at"}``) and its kind. Vetting is the one the terminal grant
408
+ uses (:func:`vet_path` with the data-dir rule, :func:`vet_host`); a target
409
+ it refuses raises ``ValueError`` with nothing written and the request
410
+ still pending, so a later ``deny`` can clear it. Raises
411
+ :class:`UnknownRequestError` for an id no request carries,
412
+ :class:`StateError` for a file the agent would not load, and ``OSError``
413
+ when the data dir cannot be written.
414
+ """
415
+ data, paths, req, rest = _take_request(root, request_id)
416
+ kind = req["kind"]
417
+ reason = _request_reason(req)
418
+ perms_file = root / PERMISSIONS_FILE
419
+ if kind == "path":
420
+ p = vet_path(req["target"], root=root)
421
+ record = next((e for e in paths if _same_path(str(e.get("path", "")), str(p))), None)
422
+ if record is None:
423
+ record = {"path": str(p), "reason": reason, "approved_at": _now()}
424
+ paths.append(record)
425
+ _write(perms_file, {**data, "paths": paths, "pending": rest})
426
+ return record, kind
427
+ h = vet_host(req["target"])
428
+ egress_file = root / EGRESS_ALLOW_FILE
429
+ # Read the second file before the first write so a broken egress-allow.json
430
+ # is reported with the request still pending and nothing changed.
431
+ egress, hosts = _load(egress_file, "hosts")
432
+ record = next((e for e in hosts if str(e.get("host", "")).lower() == h), None)
433
+ if record is None:
434
+ record = {"host": h, "reason": reason, "approved_at": _now()}
435
+ hosts.append(record)
436
+ _write(egress_file, {**egress, "hosts": hosts})
437
+ _write(perms_file, {**data, "pending": rest})
438
+ return record, kind
439
+
440
+
441
+ def deny(root: Path, request_id: str) -> dict:
442
+ """Drop one pending request without approving anything. Returns the request. Raises as :func:`approve` does."""
443
+ data, _, req, rest = _take_request(root, request_id)
444
+ _write(root / PERMISSIONS_FILE, {**data, "pending": rest})
445
+ return req
446
+
447
+
448
+ def _same_path(a: str, b: str) -> bool:
449
+ return _fold(Path(os.path.normpath(a))) == _fold(Path(os.path.normpath(b)))
450
+
451
+
452
+ def _write(file: Path, payload: dict) -> None:
453
+ file.parent.mkdir(parents=True, exist_ok=True)
454
+ atomic_write_text(file, json.dumps(payload, indent=2) + "\n")
455
+ try:
456
+ os.chmod(file, 0o600)
457
+ except OSError:
458
+ pass