comfy-cli 1.22.0__tar.gz → 1.24.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.22.0 → comfy_cli-1.24.0}/PKG-INFO +4 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/README.md +3 -3
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/_lazy.py +4 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/agent/__init__.py +11 -6
- comfy_cli-1.24.0/comfy_cli/agent/command.py +569 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/builder_api.py +24 -5
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cloud/command.py +44 -16
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cloud/oauth.py +115 -17
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cmdline.py +990 -8
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/comfy_client.py +12 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/_cloud_errors.py +79 -3
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/assets_library.py +114 -21
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build.py +233 -19
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/cloud_http.py +42 -4
- comfy_cli-1.24.0/comfy_cli/command/code_search.py +770 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy.py +308 -26
- comfy_cli-1.24.0/comfy_cli/command/deploy_cancel.py +195 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_ls.py +43 -7
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_progress.py +25 -11
- comfy_cli-1.24.0/comfy_cli/command/deploy_promote.py +55 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_read.py +127 -13
- comfy_cli-1.24.0/comfy_cli/command/deploy_rename.py +110 -0
- comfy_cli-1.24.0/comfy_cli/command/deploy_resolve.py +545 -0
- comfy_cli-1.24.0/comfy_cli/command/deploy_rollback.py +291 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run.py +53 -8
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_runtime.py +29 -6
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_status.py +94 -9
- comfy_cli-1.24.0/comfy_cli/command/deploy_types.py +427 -0
- comfy_cli-1.24.0/comfy_cli/command/deploy_up.py +886 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/app.py +2 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/client.py +70 -19
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/emit.py +6 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/output.py +1 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/schema.py +1 -1
- comfy_cli-1.24.0/comfy_cli/command/generate/spec/openapi.yml +1 -0
- comfy_cli-1.24.0/comfy_cli/command/generate/spec.py +1285 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/jobs.py +555 -106
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/knowledge.py +111 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/models/models.py +456 -182
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/node_deps.py +71 -16
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/nodes.py +88 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run/__init__.py +447 -147
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run/execution.py +117 -6
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run/preflight.py +18 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run/watcher.py +40 -5
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/templates.py +62 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/transfer.py +77 -7
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow.py +298 -14
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow_edit.py +110 -15
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/engine.py +1527 -152
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/loader.py +13 -159
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/promoted.py +932 -131
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/widget_catalog.py +42 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/credentials.py +193 -64
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_api.py +82 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_api_errors.py +40 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_assets.py +12 -3
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_download.py +9 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_events.py +24 -7
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/discovery.py +12 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/download_state.py +406 -127
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/error_codes.py +278 -39
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/jobs_state.py +19 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/knowledge.py +191 -17
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/layout.py +55 -4
- comfy_cli-1.24.0/comfy_cli/link_integrity.py +204 -0
- comfy_cli-1.24.0/comfy_cli/model_variants.py +299 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/registry/config_parser.py +259 -25
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/agent.json +6 -8
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/assets_library.json +2 -0
- comfy_cli-1.24.0/comfy_cli/schemas/deploy_cancel.json +116 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_events.json +16 -1
- comfy_cli-1.24.0/comfy_cli/schemas/deploy_history.json +68 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_ls.json +15 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_progress_event.json +5 -1
- comfy_cli-1.24.0/comfy_cli/schemas/deploy_promote.json +101 -0
- comfy_cli-1.24.0/comfy_cli/schemas/deploy_rename.json +39 -0
- comfy_cli-1.24.0/comfy_cli/schemas/deploy_rollback.json +86 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_run.json +6 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_status.json +28 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_up.json +24 -3
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/error_codes.md +4 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_list.json +1 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/jobs.json +8 -3
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/knowledge.json +8 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/node_deps.json +2 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/run.json +6 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/widget_catalog.json +16 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/workflow.json +11 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/selector.py +195 -8
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/__init__.py +45 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy/SKILL.md +51 -9
- comfy_cli-1.24.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +159 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build/SKILL.md +9 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-failures/SKILL.md +9 -4
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +59 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-api-schema/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-caching/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-dependencies/SKILL.md +11 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-frontend/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-invocation/SKILL.md +16 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-migration/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-packaging/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-search/SKILL.md +12 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-troubleshooting/SKILL.md +14 -0
- comfy_cli-1.24.0/comfy_cli/skills/comfy-custom-nodes-validation/SKILL.md +15 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-debug/SKILL.md +13 -1
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-deploy/SKILL.md +149 -19
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-deploy-failures/SKILL.md +11 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/command.py +21 -8
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/target.py +3 -2
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/tracking.py +13 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/workflow_ops.py +1865 -107
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/workflow_print.py +735 -63
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/workflow_to_api.py +992 -119
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/PKG-INFO +4 -4
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/SOURCES.txt +21 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/pyproject.toml +1 -1
- comfy_cli-1.22.0/comfy_cli/agent/command.py +0 -330
- comfy_cli-1.22.0/comfy_cli/command/code_search.py +0 -194
- comfy_cli-1.22.0/comfy_cli/command/deploy_resolve.py +0 -236
- comfy_cli-1.22.0/comfy_cli/command/deploy_types.py +0 -133
- comfy_cli-1.22.0/comfy_cli/command/deploy_up.py +0 -309
- comfy_cli-1.22.0/comfy_cli/command/generate/spec/openapi.yml +0 -31636
- comfy_cli-1.22.0/comfy_cli/command/generate/spec.py +0 -547
- comfy_cli-1.22.0/comfy_cli/skills/comfy-agent-permissions/SKILL.md +0 -92
- comfy_cli-1.22.0/comfy_cli/skills/comfy-custom-nodes/SKILL.md +0 -190
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/LICENSE +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/__main__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/_safe_exec.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/auth/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/auth/command.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/auth/store.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/builder_pagination.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/caller.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cancellation.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cloud/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cloud/billing.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_diff.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_digest_cache.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_package.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_paths.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_pull.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_push.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_spec.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_targets.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_upload_progress.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/build_validation.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/custom_nodes/command.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_compute.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_lifecycle.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_refs.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run_assets.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_run_watch.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/deploy_workflow.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/adapters.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/poll.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/generate/upload.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/github/pr_info.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/install.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/job_watcher.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/launch.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/models/search.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/outdated.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/pack_scan.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/pr_command.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/preview.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/project.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run/loader.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/run_cli.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/setup.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/stop_port.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/system.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/version_validators.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command/workflow_fragments.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/command_mentions.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/config_manager.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/constants.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/_net.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/annotations_source.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/cloud_disable_config.yaml +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/default_text2img.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/data/supported_nodes.yaml +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/default_workflow.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cql/errors.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/cuda_detect.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deploy_jobs.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/deprecation.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/env_checker.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/execution_errors.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/file_utils.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/fragments.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/git_utils.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/hardware.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/hashing.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/help_json.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/host_port.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/http.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/interaction.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/layout_quality.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/local_address.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/locking.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/logging.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/onboarding.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/branding.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/glyphs.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/panels.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/preview.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/progress.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/renderer.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/rich_compat.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/output/sanitize.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/pr_cache.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/project.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/registry/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/registry/api.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/registry/types.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/resolve_python.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/assets.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/auth.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_blob_ls.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_delete.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_init.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_ls.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_pull.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_push.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_push_event.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_base_images.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_build_targets.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_model_dirs.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_refs_resolve.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_create.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_delete.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_logs.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_ls.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_manifest.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_release_show.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_show.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_status.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_update.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/build_validate.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/cloud_status.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_delete.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_logs.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_refs_compute.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_scale.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_show.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_start.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/deploy_stop.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/discover.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/download.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/download_status.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/downloads.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/env.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/envelope.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/error.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/free.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_result.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/generate_schema.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/help.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/jobs_wait.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/knowledge_block.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/launch.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/logs.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/models.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/nodes.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/outdated.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/preview.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/project.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/run_event.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/set_default.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/skill.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/stop.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/system_stats.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/templates.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/transfer.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/update.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/version.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/schemas/which.json +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy/__init__.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-authoring/SKILL.md +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-build-pins/SKILL.md +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-director/SKILL.md +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/skills/comfy-relay/SKILL.md +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/standalone.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/typing.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/ui.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/update.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/utils.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/uv.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/where.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli/workspace_manager.py +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/entry_points.txt +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/requires.txt +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/comfy_cli.egg-info/top_level.txt +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.0}/setup.cfg +0 -0
- {comfy_cli-1.22.0 → comfy_cli-1.24.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.24.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
|
|
@@ -72,7 +72,7 @@ workflows, and call hosted partner image models, all from your terminal.
|
|
|
72
72
|
## Features
|
|
73
73
|
|
|
74
74
|
- 🚀 One-command ComfyUI install and launch
|
|
75
|
-
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft,
|
|
75
|
+
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Reve, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
|
|
76
76
|
- 🔧 Custom node management — install, update, snapshot, bisect
|
|
77
77
|
- 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
|
|
78
78
|
- 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
|
|
@@ -529,8 +529,8 @@ Notes:
|
|
|
529
529
|
`comfy generate` calls Comfy's partner nodes directly from the terminal — no
|
|
530
530
|
local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
|
|
531
531
|
you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
|
|
532
|
-
models (Flux, Ideogram, DALL·E, Recraft,
|
|
533
|
-
|
|
532
|
+
models (Flux, Ideogram, DALL·E, Recraft, Runway, Reve, xAI Grok, Google
|
|
533
|
+
Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
|
|
534
534
|
Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
|
|
535
535
|
**Seedance**) are all covered; video jobs run async and the CLI polls until
|
|
536
536
|
the result is ready.
|
|
@@ -19,7 +19,7 @@ workflows, and call hosted partner image models, all from your terminal.
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
21
|
- 🚀 One-command ComfyUI install and launch
|
|
22
|
-
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft,
|
|
22
|
+
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Reve, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
|
|
23
23
|
- 🔧 Custom node management — install, update, snapshot, bisect
|
|
24
24
|
- 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
|
|
25
25
|
- 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
|
|
@@ -476,8 +476,8 @@ Notes:
|
|
|
476
476
|
`comfy generate` calls Comfy's partner nodes directly from the terminal — no
|
|
477
477
|
local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
|
|
478
478
|
you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
|
|
479
|
-
models (Flux, Ideogram, DALL·E, Recraft,
|
|
480
|
-
|
|
479
|
+
models (Flux, Ideogram, DALL·E, Recraft, Runway, Reve, xAI Grok, Google
|
|
480
|
+
Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
|
|
481
481
|
Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
|
|
482
482
|
**Seedance**) are all covered; video jobs run async and the CLI polls until
|
|
483
483
|
the result is ready.
|
|
@@ -102,6 +102,7 @@ class LazyCommand:
|
|
|
102
102
|
attr: str | None = None
|
|
103
103
|
register: str | None = None
|
|
104
104
|
help: str | None = None
|
|
105
|
+
hidden: bool = False
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
class LazyTyperGroup(TyperGroup):
|
|
@@ -164,9 +165,11 @@ class LazyTyperGroup(TyperGroup):
|
|
|
164
165
|
infos = [info for info in scratch.registered_commands if info.name == name]
|
|
165
166
|
assert len(infos) == 1, f"{spec.module}.{spec.register} did not register exactly one {name!r} command"
|
|
166
167
|
info = infos[0]
|
|
168
|
+
if spec.hidden:
|
|
169
|
+
info.hidden = True
|
|
167
170
|
else:
|
|
168
171
|
assert spec.attr, "LazyCommand needs attr or register"
|
|
169
|
-
info = CommandInfo(name=name, callback=getattr(module, spec.attr), help=spec.help)
|
|
172
|
+
info = CommandInfo(name=name, callback=getattr(module, spec.attr), help=spec.help, hidden=spec.hidden)
|
|
170
173
|
return typer.main.get_command_from_info(info, **self._factory_kwargs(typer.main.get_command_from_info))
|
|
171
174
|
|
|
172
175
|
def _build_group(self, name: str, spec: LazySubcommand) -> click.Command:
|
|
@@ -12,12 +12,17 @@ lets it reach beyond the defaults lives in two files in its data dir:
|
|
|
12
12
|
The agent's ``request_path`` / ``request_host`` tools only append to
|
|
13
13
|
``pending``: the model can ask, and injected content in a workflow can make it
|
|
14
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 —
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
does that through a channel the model cannot reach — the agent panel or the
|
|
16
|
+
in-chat card while an agent is running, or this module from a terminal
|
|
17
|
+
(``comfy agent allow --approve <id>`` / ``comfy agent deny <id>``) while none
|
|
18
|
+
is — after vetting the exact target the request names. The same module also
|
|
19
|
+
grants a folder or host outright (``comfy agent allow --path`` / ``--host``).
|
|
20
|
+
|
|
21
|
+
**A running agent is the only writer of these files.** It loads them at start
|
|
22
|
+
and saves its whole state on every change, so an edit written here while it
|
|
23
|
+
runs is never read and is overwritten by its next save; the commands in this
|
|
24
|
+
module refuse while an agent answers on its port, and otherwise apply at the
|
|
25
|
+
agent's next start. The data dir is ``AGENT_DATA_DIR`` when set, else
|
|
21
26
|
``~/.comfy-agent`` — the same rule the agent and its launcher use.
|
|
22
27
|
|
|
23
28
|
The checks here are a courtesy, not the fence: the agent's sandbox deny list
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
"""``comfy agent`` — the Typer surface over :mod:`comfy_cli.agent`.
|
|
2
|
+
|
|
3
|
+
``permissions`` shows the agent's requests waiting for a human, what the local
|
|
4
|
+
agent may reach beyond its defaults, and whether one is running; ``allow
|
|
5
|
+
--approve <id>`` / ``deny <id>`` answer a request the agent's ``request_path``
|
|
6
|
+
/ ``request_host`` tools recorded (the model cannot answer its own request);
|
|
7
|
+
``allow --path`` / ``--host`` grants a folder or a host outright.
|
|
8
|
+
|
|
9
|
+
Every one of those writes refuses while an agent is answering on its port: a
|
|
10
|
+
running agent is the only writer of its permission files, so the write would
|
|
11
|
+
be ignored and then overwritten. While one runs, requests are answered in the
|
|
12
|
+
agent panel or on the in-chat card, which call the agent's own API.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import http.client
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.request
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Annotated
|
|
25
|
+
|
|
26
|
+
import typer
|
|
27
|
+
|
|
28
|
+
from comfy_cli import tracking
|
|
29
|
+
from comfy_cli.agent import (
|
|
30
|
+
DISCOVERY_FILE,
|
|
31
|
+
StateError,
|
|
32
|
+
UnknownRequestError,
|
|
33
|
+
allow_host,
|
|
34
|
+
allow_path,
|
|
35
|
+
approve,
|
|
36
|
+
data_dir,
|
|
37
|
+
deny,
|
|
38
|
+
read_state,
|
|
39
|
+
vet_host,
|
|
40
|
+
vet_path,
|
|
41
|
+
)
|
|
42
|
+
from comfy_cli.http import build_http_only_opener
|
|
43
|
+
from comfy_cli.output import get_renderer, rprint
|
|
44
|
+
from comfy_cli.output.sanitize import sanitize_markup as esc
|
|
45
|
+
|
|
46
|
+
app = typer.Typer(
|
|
47
|
+
no_args_is_help=True,
|
|
48
|
+
help="The local comfy agent: what it may reach, and how to let it reach more.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_DATA_DIR_OPT = Annotated[
|
|
52
|
+
str | None,
|
|
53
|
+
typer.Option(
|
|
54
|
+
"--data-dir", show_default=False, help="The agent's data dir (default: AGENT_DATA_DIR, else ~/.comfy-agent)."
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
# A running agent is the only writer of its permission files: it loads them at
|
|
59
|
+
# start and saves its whole state on every change, so an edit made here while
|
|
60
|
+
# it runs is never read and is then overwritten by the agent's next save.
|
|
61
|
+
# Hence NEXT_START_NOTE, and the refusals below. The rule is the agent's, and
|
|
62
|
+
# is stated in its own `services/agent/local/README.md`.
|
|
63
|
+
NEXT_START_NOTE = "this applies at the agent's next start; a running agent does not read it"
|
|
64
|
+
AGENT_RUNNING_MESSAGE = (
|
|
65
|
+
"an agent is answering on port {port} from {root}: a running agent is the only writer of its "
|
|
66
|
+
"permission files, so this edit would be ignored and then overwritten"
|
|
67
|
+
)
|
|
68
|
+
ANSWER_IN_PANEL = (
|
|
69
|
+
"a running agent is the only writer of these files: answer a waiting request in the agent panel or "
|
|
70
|
+
"on the card in chat, or stop the agent to edit them from here"
|
|
71
|
+
)
|
|
72
|
+
AGENT_RUNNING_HINT = (
|
|
73
|
+
"answer a waiting request in the agent panel or on the card in chat, which call the running agent's "
|
|
74
|
+
"own API; to write these files from here instead, stop the agent first"
|
|
75
|
+
)
|
|
76
|
+
AGENT_UNCONFIRMED_MESSAGE = (
|
|
77
|
+
"something is answering on port {port}, which {root} names as its agent's, but it did not answer as that "
|
|
78
|
+
"agent: a write here is lost without a trace if an agent is in fact running, so none was made"
|
|
79
|
+
)
|
|
80
|
+
AGENT_UNCONFIRMED_HINT = (
|
|
81
|
+
"if an agent is running, answer the request in the agent panel or on the card in chat; if none is, delete "
|
|
82
|
+
"the stale agent.json in that data dir and run this again"
|
|
83
|
+
)
|
|
84
|
+
REFUSED_HINT = (
|
|
85
|
+
"a narrower folder that holds only what is needed may be allowed; credential stores, the home folder "
|
|
86
|
+
"and the whole disk never are. A host must be a full name or a routable address"
|
|
87
|
+
)
|
|
88
|
+
REFUSED_REQUEST_HINT = (
|
|
89
|
+
"the request stays pending and nothing was approved: `comfy agent deny <id>` clears it, or "
|
|
90
|
+
"`comfy agent allow --path <narrower folder>` grants what is actually needed. " + REFUSED_HINT
|
|
91
|
+
)
|
|
92
|
+
UNKNOWN_REQUEST_HINT = "run `comfy agent permissions` to see the requests waiting; each shows its id"
|
|
93
|
+
GRANT_HINTS = {
|
|
94
|
+
"approve": "{cli} agent allow{dir} --approve <id>",
|
|
95
|
+
"deny": "{cli} agent deny{dir} <id>",
|
|
96
|
+
"path": '{cli} agent allow{dir} --path "<folder>" --reason "<why>"',
|
|
97
|
+
"host": "{cli} agent allow{dir} --host <host> --reason <why>",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# The entry points pyproject installs. Anything else in argv[0] -- a test
|
|
101
|
+
# runner, an embedding host -- is not a way to call this CLI, so the hints name
|
|
102
|
+
# the installed command rather than whatever started the process.
|
|
103
|
+
_CLI_NAMES = frozenset({"comfy", "comfy-cli", "comfycli"})
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _cli_invocation() -> str:
|
|
107
|
+
"""How to call this CLI, as this process was started.
|
|
108
|
+
|
|
109
|
+
``comfy`` for an installed entry point (``comfy.exe`` folded to the same),
|
|
110
|
+
``<python> -m comfy_cli`` when started as a module, and ``comfy`` for
|
|
111
|
+
anything else -- a hint is for the user's terminal, and a path that only
|
|
112
|
+
works inside the agent's sandbox is exactly what sent users to paste a venv
|
|
113
|
+
path that answered "Access is denied".
|
|
114
|
+
"""
|
|
115
|
+
argv0 = Path(sys.argv[0] or "")
|
|
116
|
+
if argv0.name == "__main__.py":
|
|
117
|
+
return f"{Path(sys.executable).name} -m comfy_cli"
|
|
118
|
+
name = argv0.name[:-4] if argv0.name.lower().endswith(".exe") else argv0.name
|
|
119
|
+
return name if name in _CLI_NAMES else "comfy"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def grant_hints(root: Path) -> dict[str, str]:
|
|
123
|
+
"""The exact commands that approve or grant against ``root``.
|
|
124
|
+
|
|
125
|
+
``--data-dir`` rides along whenever the agent's dir is not the one a bare
|
|
126
|
+
terminal resolves (``AGENT_DATA_DIR``, else ``~/.comfy-agent``): the user's
|
|
127
|
+
shell is a different process, and without it ``--approve`` answers
|
|
128
|
+
``agent_unknown_request`` while the request is still waiting. A relative
|
|
129
|
+
dir is emitted absolute: the user's shell need not share this process's cwd.
|
|
130
|
+
"""
|
|
131
|
+
flag = "" if root.is_absolute() and root == data_dir(None) else f' --data-dir "{root.absolute()}"'
|
|
132
|
+
cli = _cli_invocation()
|
|
133
|
+
return {k: v.format(cli=cli, dir=flag) for k, v in GRANT_HINTS.items()}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _age(requested_at: object, now: datetime | None = None) -> str:
|
|
137
|
+
"""How long ago an RFC 3339 timestamp was, for a human; ``?`` when it does not parse."""
|
|
138
|
+
if not isinstance(requested_at, str):
|
|
139
|
+
return "?"
|
|
140
|
+
try:
|
|
141
|
+
then = datetime.fromisoformat(requested_at.replace("Z", "+00:00"))
|
|
142
|
+
except ValueError:
|
|
143
|
+
return "?"
|
|
144
|
+
if then.tzinfo is None:
|
|
145
|
+
then = then.replace(tzinfo=timezone.utc)
|
|
146
|
+
seconds = max(0, int(((now or datetime.now(timezone.utc)) - then).total_seconds()))
|
|
147
|
+
if seconds < 60:
|
|
148
|
+
return f"{seconds}s"
|
|
149
|
+
if seconds < 3600:
|
|
150
|
+
return f"{seconds // 60}m"
|
|
151
|
+
if seconds < 86400:
|
|
152
|
+
return f"{seconds // 3600}h"
|
|
153
|
+
return f"{seconds // 86400}d"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _no_agent_warning(root) -> str:
|
|
157
|
+
# No agent has ever published itself here. A running agent started with
|
|
158
|
+
# another AGENT_DATA_DIR (the launcher's, or a typo in --data-dir) will
|
|
159
|
+
# never read this file; say so rather than promise a pick-up.
|
|
160
|
+
return (
|
|
161
|
+
f"no {DISCOVERY_FILE} in {root}: no agent has run from this data dir. A running agent that uses "
|
|
162
|
+
"another data dir will not see this approval; pass --data-dir or set AGENT_DATA_DIR to match it"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _refuse_while_live(root, state) -> None:
|
|
167
|
+
"""Refuse a write to the permission files unless the agent is known gone.
|
|
168
|
+
|
|
169
|
+
``state.running`` only says a discovery file exists; a crashed agent
|
|
170
|
+
leaves one behind, so this probes ``/health`` rather than trusting the
|
|
171
|
+
file — otherwise a stale file would lock the user out of the only way to
|
|
172
|
+
edit these files.
|
|
173
|
+
|
|
174
|
+
It refuses on ``unconfirmed`` as well as ``live``, which is the whole
|
|
175
|
+
reason :func:`_probe_agent` has three outcomes rather than two. The two
|
|
176
|
+
mistakes are not symmetric: writing while an agent runs is overwritten by
|
|
177
|
+
its next save and the user never learns the approval was lost, while a
|
|
178
|
+
refusal names the data dir and what to do about it. Only what the probe
|
|
179
|
+
*establishes* is not this agent permits the write.
|
|
180
|
+
"""
|
|
181
|
+
if not state.running:
|
|
182
|
+
return
|
|
183
|
+
verdict, _ = _probe_agent(state.port)
|
|
184
|
+
if verdict == _AGENT_GONE:
|
|
185
|
+
return
|
|
186
|
+
renderer = get_renderer()
|
|
187
|
+
if verdict == _AGENT_LIVE:
|
|
188
|
+
renderer.error(
|
|
189
|
+
code="agent_running",
|
|
190
|
+
message=AGENT_RUNNING_MESSAGE.format(port=state.port, root=root),
|
|
191
|
+
hint=AGENT_RUNNING_HINT,
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
renderer.error(
|
|
195
|
+
code="agent_unconfirmed",
|
|
196
|
+
message=AGENT_UNCONFIRMED_MESSAGE.format(port=state.port, root=root),
|
|
197
|
+
hint=AGENT_UNCONFIRMED_HINT,
|
|
198
|
+
)
|
|
199
|
+
raise typer.Exit(code=1)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _open_health(url: str):
|
|
203
|
+
"""Open the agent's /health on loopback, ignoring any HTTP(S)_PROXY in the environment.
|
|
204
|
+
|
|
205
|
+
The global ``urlopen`` honours ``HTTP_PROXY``, which a shell behind the
|
|
206
|
+
agent's own egress proxy exports; routing a loopback probe through it
|
|
207
|
+
made a live agent look absent. An empty ``ProxyHandler`` skips the
|
|
208
|
+
environment.
|
|
209
|
+
"""
|
|
210
|
+
return build_http_only_opener(urllib.request.ProxyHandler({})).open(url, timeout=2)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# The keys only the agent's own /health carries. The standalone binary serves
|
|
214
|
+
# the plain string "ok" until it has something to report and a JSON object
|
|
215
|
+
# once it has, and its startup reports sandbox and cli together, so a JSON
|
|
216
|
+
# object carrying none of these is some other service on a reused port (cloud
|
|
217
|
+
# services/agent/local/internal/server/server.go, healthPayload). `status` is
|
|
218
|
+
# deliberately not in the list: every other health endpoint has one too.
|
|
219
|
+
# Checked against that struct's JSON tags at cloud commit 6c54e206, and pinned
|
|
220
|
+
# by test_health_markers_are_fields_the_agent_actually_sends so a rename on the
|
|
221
|
+
# agent's side fails here instead of reading a live agent as gone.
|
|
222
|
+
_AGENT_HEALTH_KEYS = (
|
|
223
|
+
"sandbox",
|
|
224
|
+
"skills",
|
|
225
|
+
"crdt",
|
|
226
|
+
"doc_host",
|
|
227
|
+
"prompt",
|
|
228
|
+
"cli",
|
|
229
|
+
"assets",
|
|
230
|
+
"memory",
|
|
231
|
+
"node_trees",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# What a /health probe on the discovery port established. Three outcomes, not
|
|
236
|
+
# two, because the write guard needs the distinction: GONE is only for what the
|
|
237
|
+
# probe *proved* is not this agent, and everything it could not settle is
|
|
238
|
+
# UNCONFIRMED. `_sandbox_status` collapses both onto None, which is right for
|
|
239
|
+
# display — either way there is no sandbox to show — and wrong for a refusal.
|
|
240
|
+
_AGENT_LIVE = "live"
|
|
241
|
+
_AGENT_GONE = "gone"
|
|
242
|
+
_AGENT_UNCONFIRMED = "unconfirmed"
|
|
243
|
+
|
|
244
|
+
# The bare answer the agent's health handler falls through to while every
|
|
245
|
+
# reporter is still nil, in both forms it could arrive in: the server sends the
|
|
246
|
+
# two bytes as text/plain, a JSON encoder would quote them.
|
|
247
|
+
#
|
|
248
|
+
# No agent can serve it on the port this probe reaches. `app.go` calls
|
|
249
|
+
# ReportSandbox/ReportCLI/ReportNodeTrees/ReportAssets/ReportMemory/
|
|
250
|
+
# ReportPrompt/ReportSkills/ReportCRDT (lines 233-255 on cloud main) BEFORE
|
|
251
|
+
# net.Listen (257) and before WriteDiscoveryWith puts the port in agent.json
|
|
252
|
+
# (263), so by the time anything can connect, or even learn the port, eight
|
|
253
|
+
# reporters are non-nil and the handler serves the JSON payload. The
|
|
254
|
+
# fall-through is live code on a path nothing can observe from here.
|
|
255
|
+
#
|
|
256
|
+
# So this body means "something else holds the port", not "the agent, early".
|
|
257
|
+
_AGENT_EARLY_OK = ("ok", '"ok"')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _probe_agent(port: int | None) -> tuple[str, dict | None]:
|
|
261
|
+
"""Ask the discovery port whether this agent is answering on it.
|
|
262
|
+
|
|
263
|
+
Returns the verdict and, when the answer carried them, the sandbox and
|
|
264
|
+
workspace fields `permissions` displays.
|
|
265
|
+
"""
|
|
266
|
+
if not port:
|
|
267
|
+
return _AGENT_GONE, None
|
|
268
|
+
try:
|
|
269
|
+
with _open_health(f"http://127.0.0.1:{port}/health") as resp:
|
|
270
|
+
raw = resp.read().decode("utf-8", "replace")
|
|
271
|
+
except urllib.error.HTTPError:
|
|
272
|
+
# Something serves HTTP here and refused the probe. Whether it is the
|
|
273
|
+
# agent is exactly what this failed to establish.
|
|
274
|
+
return _AGENT_UNCONFIRMED, None
|
|
275
|
+
except urllib.error.URLError as exc:
|
|
276
|
+
# urllib wraps the transport error; a refused connection is the one
|
|
277
|
+
# that proves nothing is listening, so agent.json is stale.
|
|
278
|
+
return (_AGENT_GONE if isinstance(exc.reason, ConnectionRefusedError) else _AGENT_UNCONFIRMED), None
|
|
279
|
+
except ConnectionRefusedError:
|
|
280
|
+
return _AGENT_GONE, None
|
|
281
|
+
except (http.client.RemoteDisconnected, http.client.IncompleteRead):
|
|
282
|
+
# Both reach the HTTPException clause below and must not: they come
|
|
283
|
+
# from a peer that accepted the connection and then failed mid-exchange
|
|
284
|
+
# -- a live agent shutting down, crashing or overloaded does exactly
|
|
285
|
+
# this. urllib wraps only h.request() in URLError, not h.getresponse(),
|
|
286
|
+
# so a RemoteDisconnected (a BadStatusLine as well as a
|
|
287
|
+
# ConnectionResetError) arrives here unwrapped; an IncompleteRead comes
|
|
288
|
+
# from resp.read() on a short body. Neither establishes that the
|
|
289
|
+
# listener is not this agent.
|
|
290
|
+
return _AGENT_UNCONFIRMED, None
|
|
291
|
+
except http.client.HTTPException:
|
|
292
|
+
# HTTPException is not an OSError: a stale port taken by something
|
|
293
|
+
# that is not HTTP (an SSH banner) raises BadStatusLine here. Proof
|
|
294
|
+
# enough that this is not the agent.
|
|
295
|
+
return _AGENT_GONE, None
|
|
296
|
+
except (OSError, ValueError):
|
|
297
|
+
# A timeout (the 2 s budget in _open_health) or a reset mid-read.
|
|
298
|
+
# Something is there; what it is is unsettled.
|
|
299
|
+
return _AGENT_UNCONFIRMED, None
|
|
300
|
+
if raw.strip() in _AGENT_EARLY_OK:
|
|
301
|
+
# Unsettled, and deliberately not either certainty. It cannot be this
|
|
302
|
+
# agent answering early (see `_AGENT_EARLY_OK`: the port does not exist
|
|
303
|
+
# until eight reporters do), so "live" asserts something false and
|
|
304
|
+
# sends a user with no agent to stop to the agent panel. But it is not
|
|
305
|
+
# proof of absence either — something is listening on the port
|
|
306
|
+
# agent.json names, and if an agent is in fact running from another
|
|
307
|
+
# data dir, a write made here is lost without a trace. Refusing while
|
|
308
|
+
# naming the stale-agent.json way out is the only honest answer.
|
|
309
|
+
#
|
|
310
|
+
# What must not come back is `_AGENT_GONE`: parsing this body as JSON
|
|
311
|
+
# before looking at it raised ValueError and fell through to a "gone"
|
|
312
|
+
# verdict, which let the write through. That is the silent-loss hole,
|
|
313
|
+
# and it stays closed. The JSON-quoted form is matched too, so a later
|
|
314
|
+
# switch to `c.JSON` does not reopen it.
|
|
315
|
+
return _AGENT_UNCONFIRMED, None
|
|
316
|
+
try:
|
|
317
|
+
body = json.loads(raw)
|
|
318
|
+
except ValueError:
|
|
319
|
+
# Not the early answer, and not JSON either. Something is there; what
|
|
320
|
+
# it is is unsettled.
|
|
321
|
+
return _AGENT_UNCONFIRMED, None
|
|
322
|
+
if not isinstance(body, dict):
|
|
323
|
+
# Neither the agent's own early answer nor anything judgeable.
|
|
324
|
+
return _AGENT_UNCONFIRMED, None
|
|
325
|
+
if not any(key in body for key in _AGENT_HEALTH_KEYS):
|
|
326
|
+
# A reused port held by an unrelated local service whose /health
|
|
327
|
+
# answers `{"status": "ok"}`. Established: not this agent, so the
|
|
328
|
+
# user is not locked out of editing the files at all.
|
|
329
|
+
return _AGENT_GONE, None
|
|
330
|
+
# The envelope schema says sandbox is an object or null and comfy_path a
|
|
331
|
+
# string or null; a stale or foreign server's shapes are not passed on.
|
|
332
|
+
sandbox = body.get("sandbox")
|
|
333
|
+
out: dict = {"sandbox": sandbox if isinstance(sandbox, dict) else None}
|
|
334
|
+
cli = body.get("cli")
|
|
335
|
+
if isinstance(cli, dict) and isinstance(cli.get("workspace"), str) and cli["workspace"]:
|
|
336
|
+
out["comfy_path"] = cli["workspace"]
|
|
337
|
+
return _AGENT_LIVE, out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _sandbox_status(port: int | None) -> dict | None:
|
|
341
|
+
"""The sandbox and workspace to display, or ``None`` when there are none.
|
|
342
|
+
|
|
343
|
+
Two-valued on purpose: a caller that only renders state has nothing to do
|
|
344
|
+
differently for a port it could not reach and one it reached and dismissed.
|
|
345
|
+
A caller that *writes* must use :func:`_probe_agent` directly.
|
|
346
|
+
"""
|
|
347
|
+
verdict, detail = _probe_agent(port)
|
|
348
|
+
return detail if verdict == _AGENT_LIVE else None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@app.command(
|
|
352
|
+
"permissions",
|
|
353
|
+
help="Show the agent's requests waiting for approval, the approved folders and hosts, and a running agent's sandbox.",
|
|
354
|
+
)
|
|
355
|
+
@tracking.track_command("agent")
|
|
356
|
+
def permissions_cmd(data_dir_opt: _DATA_DIR_OPT = None):
|
|
357
|
+
renderer = get_renderer()
|
|
358
|
+
root = data_dir(data_dir_opt)
|
|
359
|
+
try:
|
|
360
|
+
state = read_state(root)
|
|
361
|
+
except StateError as exc:
|
|
362
|
+
renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
|
|
363
|
+
raise typer.Exit(code=1) from exc
|
|
364
|
+
# One probe serves both readings: whether to show the agent as running,
|
|
365
|
+
# and whether a `grant` command offered here would be refused by
|
|
366
|
+
# `_refuse_while_live`. Offering one it would refuse was the contradiction.
|
|
367
|
+
verdict, health = _probe_agent(state.port) if state.running else (_AGENT_GONE, None)
|
|
368
|
+
live = verdict == _AGENT_LIVE
|
|
369
|
+
writable = verdict == _AGENT_GONE
|
|
370
|
+
payload = {
|
|
371
|
+
"data_dir": str(root),
|
|
372
|
+
"agent": {"running": live, "port": state.port},
|
|
373
|
+
"sandbox": (health or {}).get("sandbox"),
|
|
374
|
+
"comfy_path": (health or {}).get("comfy_path"),
|
|
375
|
+
"pending": state.pending,
|
|
376
|
+
"paths": state.paths,
|
|
377
|
+
"hosts": state.hosts,
|
|
378
|
+
# Offered only when a write from here would actually land. While an
|
|
379
|
+
# agent is answering it is the only writer of these files, and when
|
|
380
|
+
# the probe could not settle that, `allow`/`deny` refuse too — so an
|
|
381
|
+
# empty `grant` is the honest answer, not a missing one.
|
|
382
|
+
"grant": grant_hints(root) if writable else {},
|
|
383
|
+
}
|
|
384
|
+
if not writable:
|
|
385
|
+
payload["answer_in"] = ANSWER_IN_PANEL
|
|
386
|
+
if renderer.is_pretty():
|
|
387
|
+
# Everything interpolated below came from files on disk or a local
|
|
388
|
+
# server; it is escaped so a "[/]" in a reason cannot crash the print.
|
|
389
|
+
rprint(f"[bold]data dir[/bold] {esc(root)}")
|
|
390
|
+
status = f"running on port {state.port}" if payload["agent"]["running"] else "not running"
|
|
391
|
+
rprint(f"[bold]agent[/bold] {status}")
|
|
392
|
+
if payload["sandbox"]:
|
|
393
|
+
rprint(f"[bold]sandbox[/bold] {esc(json.dumps(payload['sandbox']))}")
|
|
394
|
+
if payload["comfy_path"]:
|
|
395
|
+
rprint(f"[bold]ComfyUI[/bold] {esc(payload['comfy_path'])}")
|
|
396
|
+
rprint("[bold]requests waiting for your approval[/bold]" + ("" if state.pending else " (none)"))
|
|
397
|
+
for e in state.pending:
|
|
398
|
+
rprint(
|
|
399
|
+
f" {esc(e['id'])} {e['kind']:<4} {esc(e['target'])} — {esc(e.get('reason', ''))}"
|
|
400
|
+
f" [dim]({_age(e.get('requested_at'))} ago)[/dim]"
|
|
401
|
+
)
|
|
402
|
+
if state.pending and writable:
|
|
403
|
+
first = state.pending[0]["id"]
|
|
404
|
+
approve_cmd, deny_cmd = (payload["grant"][k].replace("<id>", first) for k in ("approve", "deny"))
|
|
405
|
+
rprint(f" [dim]{esc(approve_cmd)} {esc(deny_cmd)}[/dim]")
|
|
406
|
+
elif state.pending:
|
|
407
|
+
rprint(f" [dim]{esc(ANSWER_IN_PANEL)}[/dim]")
|
|
408
|
+
rprint("[bold]folders the user approved[/bold]" + ("" if state.paths else " (none)"))
|
|
409
|
+
for e in state.paths:
|
|
410
|
+
rprint(f" {esc(e.get('path'))} — {esc(e.get('reason', ''))}")
|
|
411
|
+
rprint("[bold]hosts the user approved[/bold]" + ("" if state.hosts else " (none)"))
|
|
412
|
+
for e in state.hosts:
|
|
413
|
+
rprint(f" {esc(e.get('host'))} — {esc(e.get('reason', ''))}")
|
|
414
|
+
if writable:
|
|
415
|
+
rprint("\n[dim]" + "\n".join(esc(h) for h in payload["grant"].values()) + "[/dim]")
|
|
416
|
+
else:
|
|
417
|
+
rprint(f"\n[dim]{esc(ANSWER_IN_PANEL)}[/dim]")
|
|
418
|
+
renderer.emit(payload, command="agent permissions")
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _approve_request(root, request_id: str) -> None:
|
|
422
|
+
"""``comfy agent allow --approve <id>``: answer one request the agent recorded."""
|
|
423
|
+
renderer = get_renderer()
|
|
424
|
+
try:
|
|
425
|
+
state = read_state(root)
|
|
426
|
+
_refuse_while_live(root, state)
|
|
427
|
+
record, kind = approve(root, request_id)
|
|
428
|
+
except StateError as exc:
|
|
429
|
+
renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
|
|
430
|
+
raise typer.Exit(code=1) from exc
|
|
431
|
+
except UnknownRequestError as exc:
|
|
432
|
+
renderer.error(code="agent_unknown_request", message=str(exc), hint=UNKNOWN_REQUEST_HINT)
|
|
433
|
+
raise typer.Exit(code=1) from exc
|
|
434
|
+
except ValueError as exc:
|
|
435
|
+
renderer.error(code="agent_refused", message=str(exc), hint=REFUSED_REQUEST_HINT)
|
|
436
|
+
raise typer.Exit(code=1) from exc
|
|
437
|
+
except OSError as exc:
|
|
438
|
+
renderer.error(
|
|
439
|
+
code="agent_state_unwritable",
|
|
440
|
+
message=f"{root}: cannot write: {exc}",
|
|
441
|
+
hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
|
|
442
|
+
)
|
|
443
|
+
raise typer.Exit(code=1) from exc
|
|
444
|
+
approved = {"id": request_id, "kind": kind, "target": record[kind]}
|
|
445
|
+
payload: dict = {"data_dir": str(root), "approved": approved, "note": NEXT_START_NOTE}
|
|
446
|
+
if not state.running:
|
|
447
|
+
payload["warning"] = _no_agent_warning(root)
|
|
448
|
+
if renderer.is_pretty():
|
|
449
|
+
rprint(f"[green]approved[/green] {kind}: {esc(approved['target'])} (request {esc(request_id)})")
|
|
450
|
+
rprint(f"[dim]{NEXT_START_NOTE}[/dim]")
|
|
451
|
+
if "warning" in payload:
|
|
452
|
+
renderer.warn(payload["warning"])
|
|
453
|
+
renderer.emit(payload, command="agent allow")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@app.command(
|
|
457
|
+
"allow",
|
|
458
|
+
help="Approve one of the agent's pending requests (--approve <id>), or let it reach a folder (--path) or host (--host).",
|
|
459
|
+
)
|
|
460
|
+
@tracking.track_command("agent")
|
|
461
|
+
def allow_cmd(
|
|
462
|
+
approve_id: Annotated[
|
|
463
|
+
str | None,
|
|
464
|
+
typer.Option(
|
|
465
|
+
"--approve", show_default=False, help="The id of a pending request (see `comfy agent permissions`)."
|
|
466
|
+
),
|
|
467
|
+
] = None,
|
|
468
|
+
path: Annotated[
|
|
469
|
+
str | None, typer.Option("--path", show_default=False, help="An absolute, existing folder.")
|
|
470
|
+
] = None,
|
|
471
|
+
host: Annotated[str | None, typer.Option("--host", show_default=False, help="A host name, no scheme.")] = None,
|
|
472
|
+
reason: Annotated[
|
|
473
|
+
str, typer.Option("--reason", help="Why; recorded with the approval.")
|
|
474
|
+
] = "allowed with comfy agent allow",
|
|
475
|
+
data_dir_opt: _DATA_DIR_OPT = None,
|
|
476
|
+
):
|
|
477
|
+
renderer = get_renderer()
|
|
478
|
+
if approve_id and (path or host):
|
|
479
|
+
# A request names its own target and reason; --path/--host beside it
|
|
480
|
+
# would leave which target got approved to the reader's guess.
|
|
481
|
+
renderer.error(
|
|
482
|
+
code="agent_bad_args",
|
|
483
|
+
message="--approve answers one recorded request and cannot be combined with --path or --host",
|
|
484
|
+
hint="run `comfy agent allow --approve <id>` on its own, then `--path`/`--host` as a separate command",
|
|
485
|
+
)
|
|
486
|
+
raise typer.Exit(code=2)
|
|
487
|
+
if not path and not host and not approve_id:
|
|
488
|
+
renderer.error(
|
|
489
|
+
code="agent_bad_args",
|
|
490
|
+
message="nothing to allow",
|
|
491
|
+
hint="pass --approve <id> for a pending request, or --path <folder> and/or --host <host>",
|
|
492
|
+
)
|
|
493
|
+
raise typer.Exit(code=2)
|
|
494
|
+
root = data_dir(data_dir_opt)
|
|
495
|
+
if approve_id:
|
|
496
|
+
_approve_request(root, approve_id.strip())
|
|
497
|
+
return
|
|
498
|
+
added: dict = {}
|
|
499
|
+
try:
|
|
500
|
+
# Read every state file and vet every value before the first write, so
|
|
501
|
+
# a refused --host or a broken egress-allow.json does not leave a
|
|
502
|
+
# --path approved behind a failure exit.
|
|
503
|
+
state = read_state(root)
|
|
504
|
+
_refuse_while_live(root, state)
|
|
505
|
+
if path:
|
|
506
|
+
vet_path(path, root=root)
|
|
507
|
+
if host:
|
|
508
|
+
vet_host(host)
|
|
509
|
+
if path:
|
|
510
|
+
folder, is_new = allow_path(root, path, reason)
|
|
511
|
+
added["path"] = {"path": str(folder), "added": is_new}
|
|
512
|
+
if host:
|
|
513
|
+
h, is_new = allow_host(root, host, reason)
|
|
514
|
+
added["host"] = {"host": h, "added": is_new}
|
|
515
|
+
except StateError as exc:
|
|
516
|
+
renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
|
|
517
|
+
raise typer.Exit(code=1) from exc
|
|
518
|
+
except ValueError as exc:
|
|
519
|
+
renderer.error(code="agent_refused", message=str(exc), hint=REFUSED_HINT)
|
|
520
|
+
raise typer.Exit(code=1) from exc
|
|
521
|
+
except OSError as exc:
|
|
522
|
+
renderer.error(
|
|
523
|
+
code="agent_state_unwritable",
|
|
524
|
+
message=f"{root}: cannot write: {exc}",
|
|
525
|
+
hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
|
|
526
|
+
)
|
|
527
|
+
raise typer.Exit(code=1) from exc
|
|
528
|
+
payload: dict = {"data_dir": str(root), **added, "note": NEXT_START_NOTE}
|
|
529
|
+
if not state.running:
|
|
530
|
+
payload["warning"] = _no_agent_warning(root)
|
|
531
|
+
if renderer.is_pretty():
|
|
532
|
+
for kind, entry in added.items():
|
|
533
|
+
rprint(f"[green]{'allowed' if entry['added'] else 'already allowed'}[/green] {kind}: {esc(entry[kind])}")
|
|
534
|
+
rprint(f"[dim]{NEXT_START_NOTE}[/dim]")
|
|
535
|
+
if "warning" in payload:
|
|
536
|
+
renderer.warn(payload["warning"])
|
|
537
|
+
renderer.emit(payload, command="agent allow")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@app.command("deny", help="Refuse one of the agent's pending requests; nothing is approved and the request is dropped.")
|
|
541
|
+
@tracking.track_command("agent")
|
|
542
|
+
def deny_cmd(
|
|
543
|
+
request_id: Annotated[str, typer.Argument(help="The id of a pending request (see `comfy agent permissions`).")],
|
|
544
|
+
data_dir_opt: _DATA_DIR_OPT = None,
|
|
545
|
+
):
|
|
546
|
+
renderer = get_renderer()
|
|
547
|
+
root = data_dir(data_dir_opt)
|
|
548
|
+
request_id = request_id.strip()
|
|
549
|
+
try:
|
|
550
|
+
_refuse_while_live(root, read_state(root))
|
|
551
|
+
req = deny(root, request_id)
|
|
552
|
+
except StateError as exc:
|
|
553
|
+
renderer.error(code="agent_state_unreadable", message=str(exc), hint="fix or remove the file and try again")
|
|
554
|
+
raise typer.Exit(code=1) from exc
|
|
555
|
+
except UnknownRequestError as exc:
|
|
556
|
+
renderer.error(code="agent_unknown_request", message=str(exc), hint=UNKNOWN_REQUEST_HINT)
|
|
557
|
+
raise typer.Exit(code=1) from exc
|
|
558
|
+
except OSError as exc:
|
|
559
|
+
renderer.error(
|
|
560
|
+
code="agent_state_unwritable",
|
|
561
|
+
message=f"{root}: cannot write: {exc}",
|
|
562
|
+
hint="check the data dir's permissions, or pass --data-dir for the dir the agent uses",
|
|
563
|
+
)
|
|
564
|
+
raise typer.Exit(code=1) from exc
|
|
565
|
+
denied = {"id": request_id, "kind": req["kind"], "target": req["target"]}
|
|
566
|
+
payload = {"data_dir": str(root), "denied": denied}
|
|
567
|
+
if renderer.is_pretty():
|
|
568
|
+
rprint(f"[yellow]denied[/yellow] {denied['kind']}: {esc(denied['target'])} (request {esc(request_id)})")
|
|
569
|
+
renderer.emit(payload, command="agent deny")
|