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.
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/PKG-INFO +3 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/__main__.py +20 -10
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/_lazy.py +4 -2
- comfy_cli-1.22.0/comfy_cli/agent/__init__.py +458 -0
- comfy_cli-1.22.0/comfy_cli/agent/command.py +330 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/builder_api.py +165 -26
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/billing.py +13 -11
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/command.py +1 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cmdline.py +22 -3
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/comfy_client.py +30 -8
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/_cloud_errors.py +87 -5
- comfy_cli-1.22.0/comfy_cli/command/assets_library.py +245 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build.py +623 -47
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_push.py +45 -4
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_spec.py +7 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_targets.py +1 -1
- comfy_cli-1.22.0/comfy_cli/command/build_upload_progress.py +369 -0
- comfy_cli-1.22.0/comfy_cli/command/build_validation.py +562 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/cloud_http.py +7 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy.py +34 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_compute.py +5 -1
- comfy_cli-1.22.0/comfy_cli/command/deploy_progress.py +374 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_read.py +11 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_refs.py +14 -5
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run.py +13 -9
- comfy_cli-1.22.0/comfy_cli/command/deploy_runtime.py +204 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_status.py +165 -17
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_types.py +18 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_up.py +91 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/app.py +104 -11
- comfy_cli-1.22.0/comfy_cli/command/generate/emit.py +668 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/schema.py +1 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/job_watcher.py +23 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/knowledge.py +75 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/launch.py +2 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/nodes.py +5 -5
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/__init__.py +39 -6
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/templates.py +138 -94
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow.py +72 -6
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow_edit.py +119 -4
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/engine.py +711 -81
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/promoted.py +9 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/widget_catalog.py +19 -3
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/credentials.py +73 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_api.py +67 -11
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_api_errors.py +17 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_jobs.py +33 -7
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/discovery.py +12 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/error_codes.py +171 -14
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/help_json.py +13 -4
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/http.py +10 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/knowledge.py +4 -0
- comfy_cli-1.22.0/comfy_cli/layout.py +607 -0
- comfy_cli-1.22.0/comfy_cli/layout_quality.py +219 -0
- comfy_cli-1.22.0/comfy_cli/output/progress.py +68 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/renderer.py +77 -5
- comfy_cli-1.22.0/comfy_cli/schemas/agent.json +300 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_push.json +18 -0
- comfy_cli-1.22.0/comfy_cli/schemas/build_push_event.json +58 -0
- comfy_cli-1.22.0/comfy_cli/schemas/build_release_delete.json +12 -0
- comfy_cli-1.22.0/comfy_cli/schemas/build_validate.json +14 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/cloud_status.json +1 -1
- comfy_cli-1.22.0/comfy_cli/schemas/deploy_progress_event.json +94 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_refs_compute.json +2 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_status.json +33 -2
- comfy_cli-1.22.0/comfy_cli/schemas/deploy_up.json +95 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_list.json +8 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/knowledge.json +7 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/__init__.py +2 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy/SKILL.md +40 -9
- comfy_cli-1.22.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +92 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build/SKILL.md +95 -10
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +11 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +55 -1
- comfy_cli-1.22.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +190 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-debug/SKILL.md +3 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-deploy/SKILL.md +86 -23
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +13 -7
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/tracking.py +95 -3
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_ops.py +493 -46
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_print.py +90 -3
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workflow_to_api.py +259 -108
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/PKG-INFO +3 -2
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/SOURCES.txt +12 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/requires.txt +2 -1
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/pyproject.toml +2 -2
- comfy_cli-1.20.0/comfy_cli/command/assets_library.py +0 -132
- comfy_cli-1.20.0/comfy_cli/command/build_validation.py +0 -261
- comfy_cli-1.20.0/comfy_cli/command/deploy_runtime.py +0 -108
- comfy_cli-1.20.0/comfy_cli/command/generate/emit.py +0 -367
- comfy_cli-1.20.0/comfy_cli/layout.py +0 -180
- comfy_cli-1.20.0/comfy_cli/schemas/build_validate.json +0 -8
- comfy_cli-1.20.0/comfy_cli/schemas/deploy_up.json +0 -60
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/LICENSE +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/README.md +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/_safe_exec.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/command.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/auth/store.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/builder_pagination.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/caller.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cancellation.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cloud/oauth.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_diff.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_digest_cache.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_package.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_paths.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/build_pull.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/code_search.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/custom_nodes/command.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_ls.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_resolve.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run_assets.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_run_watch.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/deploy_workflow.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/adapters.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/client.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/output.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/poll.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/spec/openapi.yml +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/spec.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/generate/upload.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/github/pr_info.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/install.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/jobs.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/models/models.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/models/search.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/node_deps.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/outdated.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/pack_scan.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/pr_command.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/preview.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/project.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/execution.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/loader.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/preflight.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run/watcher.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/run_cli.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/setup.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/stop_port.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/system.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/transfer.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/version_validators.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command/workflow_fragments.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/command_mentions.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/config_manager.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/constants.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/_net.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/annotations_source.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/default_text2img.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/default_workflow.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/errors.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cql/loader.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/cuda_detect.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_assets.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_download.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deploy_events.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/deprecation.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/download_state.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/env_checker.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/execution_errors.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/file_utils.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/fragments.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/git_utils.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/hardware.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/hashing.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/host_port.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/interaction.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/jobs_state.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/local_address.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/locking.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/logging.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/onboarding.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/branding.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/glyphs.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/panels.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/preview.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/rich_compat.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/output/sanitize.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/pr_cache.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/project.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/api.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/config_parser.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/registry/types.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/resolve_python.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/assets.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/assets_library.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/auth.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_delete.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_init.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_ls.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_pull.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_create.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_logs.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_ls.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_release_show.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_show.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_status.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/build_update.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_delete.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_events.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_logs.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_ls.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_run.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_scale.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_show.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_start.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/deploy_stop.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/discover.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/download.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/download_status.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/downloads.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/env.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/envelope.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/error.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/error_codes.md +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/free.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_result.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/generate_schema.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/help.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/jobs.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/jobs_wait.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/knowledge_block.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/launch.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/logs.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/models.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/node_deps.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/nodes.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/outdated.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/preview.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/project.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/run.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/run_event.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/set_default.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/skill.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/stop.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/system_stats.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/templates.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/transfer.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/update.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/version.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/which.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/widget_catalog.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/schemas/workflow.json +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/selector.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy/__init__.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/skills/command.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/standalone.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/target.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/typing.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/ui.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/update.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/utils.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/uv.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/where.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli/workspace_manager.py +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/entry_points.txt +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/comfy_cli.egg-info/top_level.txt +0 -0
- {comfy_cli-1.20.0 → comfy_cli-1.22.0}/setup.cfg +0 -0
- {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.
|
|
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 `
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|