graph-agents-cli 0.3.1__py3-none-any.whl
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.
- graph_agents_cli/__init__.py +26 -0
- graph_agents_cli/_api_policy.py +2145 -0
- graph_agents_cli/_approvals.py +400 -0
- graph_agents_cli/_build.py +186 -0
- graph_agents_cli/_build_info.json +7 -0
- graph_agents_cli/_chat_client.py +462 -0
- graph_agents_cli/_click.py +157 -0
- graph_agents_cli/_defaults.py +139 -0
- graph_agents_cli/_experiments.py +64 -0
- graph_agents_cli/_http.py +192 -0
- graph_agents_cli/_output.py +83 -0
- graph_agents_cli/_project.py +462 -0
- graph_agents_cli/_remote.py +220 -0
- graph_agents_cli/_response_schema.py +264 -0
- graph_agents_cli/_runner.py +319 -0
- graph_agents_cli/_skills_check.py +274 -0
- graph_agents_cli/_tools.py +189 -0
- graph_agents_cli/_trust.py +66 -0
- graph_agents_cli/api/__init__.py +15 -0
- graph_agents_cli/api/_changes.py +506 -0
- graph_agents_cli/api/_files.py +658 -0
- graph_agents_cli/api/cmd_api.py +2480 -0
- graph_agents_cli/deploy/__init__.py +15 -0
- graph_agents_cli/deploy/_config.py +171 -0
- graph_agents_cli/deploy/_image.py +128 -0
- graph_agents_cli/deploy/_kube.py +286 -0
- graph_agents_cli/deploy/_modes.py +234 -0
- graph_agents_cli/deploy/_preflight.py +370 -0
- graph_agents_cli/deploy/_values.py +168 -0
- graph_agents_cli/deploy/cmd_deploy.py +1866 -0
- graph_agents_cli/deploy/gitops.py +562 -0
- graph_agents_cli/deploy/local_load.py +273 -0
- graph_agents_cli/dev/__init__.py +13 -0
- graph_agents_cli/dev/cmd_build.py +131 -0
- graph_agents_cli/dev/cmd_install.py +78 -0
- graph_agents_cli/dev/cmd_lint.py +119 -0
- graph_agents_cli/dev/cmd_playground.py +297 -0
- graph_agents_cli/dev/policy_check.py +1287 -0
- graph_agents_cli/eval/__init__.py +22 -0
- graph_agents_cli/eval/_client.py +670 -0
- graph_agents_cli/eval/_common.py +177 -0
- graph_agents_cli/eval/_judge.py +168 -0
- graph_agents_cli/eval/_judge_runner.py +238 -0
- graph_agents_cli/eval/_paths.py +212 -0
- graph_agents_cli/eval/checks.py +581 -0
- graph_agents_cli/eval/cmd_analyze.py +278 -0
- graph_agents_cli/eval/cmd_compare.py +284 -0
- graph_agents_cli/eval/cmd_eval_group.py +80 -0
- graph_agents_cli/eval/cmd_generate.py +558 -0
- graph_agents_cli/eval/cmd_grade.py +466 -0
- graph_agents_cli/eval/cmd_metric.py +156 -0
- graph_agents_cli/eval/cmd_run.py +370 -0
- graph_agents_cli/eval/cmd_submit.py +400 -0
- graph_agents_cli/eval/config.py +435 -0
- graph_agents_cli/eval/dataset.py +350 -0
- graph_agents_cli/eval/gate.py +420 -0
- graph_agents_cli/eval/transcript.py +192 -0
- graph_agents_cli/extension/__init__.py +13 -0
- graph_agents_cli/extension/_compat.py +86 -0
- graph_agents_cli/extension/_loader.py +293 -0
- graph_agents_cli/extension/_manifest.py +135 -0
- graph_agents_cli/extension/_overrides.py +195 -0
- graph_agents_cli/extension/_paths.py +91 -0
- graph_agents_cli/extension/_refs.py +193 -0
- graph_agents_cli/extension/_resolver.py +453 -0
- graph_agents_cli/extension/_schema.py +106 -0
- graph_agents_cli/extension/_spec.py +253 -0
- graph_agents_cli/extension/_sync.py +102 -0
- graph_agents_cli/extension/_trust.py +58 -0
- graph_agents_cli/extension/cmd_extension_add.py +259 -0
- graph_agents_cli/extension/cmd_extension_group.py +57 -0
- graph_agents_cli/extension/cmd_extension_list.py +56 -0
- graph_agents_cli/extension/cmd_extension_remove.py +61 -0
- graph_agents_cli/extension/cmd_extension_update.py +195 -0
- graph_agents_cli/info/__init__.py +13 -0
- graph_agents_cli/info/cmd_info.py +222 -0
- graph_agents_cli/infra/__init__.py +15 -0
- graph_agents_cli/infra/checks.py +1169 -0
- graph_agents_cli/infra/cmd_infra.py +103 -0
- graph_agents_cli/main.py +591 -0
- graph_agents_cli/peer/__init__.py +15 -0
- graph_agents_cli/peer/_generate.py +254 -0
- graph_agents_cli/peer/cmd_peer.py +1151 -0
- graph_agents_cli/run/__init__.py +13 -0
- graph_agents_cli/run/_local_server.py +1157 -0
- graph_agents_cli/run/_signals.py +141 -0
- graph_agents_cli/run/cmd_approvals.py +530 -0
- graph_agents_cli/run/cmd_run.py +1421 -0
- graph_agents_cli/scaffold/__init__.py +19 -0
- graph_agents_cli/scaffold/agents/README.md +24 -0
- graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
- graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
- graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
- graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
- graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
- graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
- graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
- graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
- graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
- graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
- graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
- graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
- graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
- graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
- graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
- graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
- graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
- graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
- graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
- graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
- graph_agents_cli/scaffold/commands/__init__.py +13 -0
- graph_agents_cli/scaffold/commands/create.py +1424 -0
- graph_agents_cli/scaffold/commands/enhance.py +1652 -0
- graph_agents_cli/scaffold/commands/upgrade.py +570 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
- graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
- graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
- graph_agents_cli/scaffold/utils/__init__.py +13 -0
- graph_agents_cli/scaffold/utils/backup.py +212 -0
- graph_agents_cli/scaffold/utils/build_record.py +257 -0
- graph_agents_cli/scaffold/utils/cli_options.py +184 -0
- graph_agents_cli/scaffold/utils/fs.py +83 -0
- graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
- graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
- graph_agents_cli/scaffold/utils/keyedit.py +768 -0
- graph_agents_cli/scaffold/utils/keymerge.py +537 -0
- graph_agents_cli/scaffold/utils/language.py +138 -0
- graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
- graph_agents_cli/scaffold/utils/logging.py +77 -0
- graph_agents_cli/scaffold/utils/manifest.py +292 -0
- graph_agents_cli/scaffold/utils/merge.py +970 -0
- graph_agents_cli/scaffold/utils/merge3.py +216 -0
- graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
- graph_agents_cli/scaffold/utils/remote_template.py +376 -0
- graph_agents_cli/scaffold/utils/template.py +1352 -0
- graph_agents_cli/scaffold/utils/upgrade.py +894 -0
- graph_agents_cli/scaffold/utils/version.py +438 -0
- graph_agents_cli/secrets/__init__.py +15 -0
- graph_agents_cli/secrets/_apply.py +954 -0
- graph_agents_cli/secrets/_required.py +188 -0
- graph_agents_cli/secrets/cmd_secrets.py +211 -0
- graph_agents_cli/setup/__init__.py +13 -0
- graph_agents_cli/setup/_antigravity.py +221 -0
- graph_agents_cli/setup/cmd_auth.py +1030 -0
- graph_agents_cli/setup/cmd_dev_token.py +513 -0
- graph_agents_cli/setup/cmd_setup.py +428 -0
- graph_agents_cli/setup/cmd_update.py +140 -0
- graph_agents_cli/skills/__init__.py +13 -0
- graph_agents_cli/skills/_bundle.py +65 -0
- graph_agents_cli/skills/data/README.md +19 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
- graph_agents_cli/system/__init__.py +15 -0
- graph_agents_cli/system/_apply.py +519 -0
- graph_agents_cli/system/_checks.py +1023 -0
- graph_agents_cli/system/_deploy.py +215 -0
- graph_agents_cli/system/_model.py +363 -0
- graph_agents_cli/system/_system.py +664 -0
- graph_agents_cli/system/_views.py +208 -0
- graph_agents_cli/system/cmd_system.py +423 -0
- graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
- graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
- graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
- graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
- graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
- graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Turn SIGTERM and SIGHUP into the same clean shutdown as Ctrl-C.
|
|
16
|
+
|
|
17
|
+
Python's default SIGTERM action ends the interpreter on the spot: no
|
|
18
|
+
``finally``, no ``except BaseException``. The commands that start servers
|
|
19
|
+
(``run``, ``eval generate``, ``playground``) rely on exactly that cleanup to stop
|
|
20
|
+
the server they started and remove its pid file, so an IDE stop button, a CI
|
|
21
|
+
job timeout or a plain ``kill`` used to leave a detached server behind. Inside
|
|
22
|
+
:func:`terminate_like_interrupt` those signals raise :class:`TerminationSignal`
|
|
23
|
+
(a ``KeyboardInterrupt``), so every existing Ctrl-C cleanup path runs, and the
|
|
24
|
+
CLI exits with ``128 + signal number`` (143 for SIGTERM).
|
|
25
|
+
|
|
26
|
+
The cleanup itself runs under :func:`shielded`: a signal that arrives while a
|
|
27
|
+
server is being stopped (the first one, during a normal teardown) is held until
|
|
28
|
+
the pid file is gone, then handled, so no stale record is left behind.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import contextlib
|
|
34
|
+
import signal
|
|
35
|
+
import threading
|
|
36
|
+
from collections.abc import Iterator
|
|
37
|
+
from types import FrameType
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TerminationSignal(KeyboardInterrupt):
|
|
41
|
+
"""SIGTERM/SIGHUP received: unwinds like Ctrl-C, exits with ``128 + signum``."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, signum: int) -> None:
|
|
44
|
+
super().__init__(signum)
|
|
45
|
+
self.signum = signum
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def exit_code(self) -> int:
|
|
49
|
+
return 128 + self.signum
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _signals() -> list[int]:
|
|
53
|
+
return [
|
|
54
|
+
sig for sig in (getattr(signal, "SIGTERM", None), getattr(signal, "SIGHUP", None)) if sig
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _raise(signum: int, _frame: FrameType | None) -> None:
|
|
59
|
+
# The cleanup this starts (stopping a server, removing its pid file) must
|
|
60
|
+
# not be cut short by a second signal: ignore them until the block exits.
|
|
61
|
+
for sig in _signals():
|
|
62
|
+
with contextlib.suppress(ValueError, OSError):
|
|
63
|
+
signal.signal(sig, signal.SIG_IGN)
|
|
64
|
+
raise TerminationSignal(signum)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _deliver(signum: int, handler: object) -> None:
|
|
68
|
+
"""Act on a signal held by :func:`shielded` as the handler it was held from would."""
|
|
69
|
+
if handler is signal.SIG_IGN:
|
|
70
|
+
return
|
|
71
|
+
if handler is None or handler is signal.SIG_DFL:
|
|
72
|
+
# The default action (end the process), now that the cleanup is done.
|
|
73
|
+
signal.raise_signal(signum)
|
|
74
|
+
return
|
|
75
|
+
if callable(handler):
|
|
76
|
+
handler(signum, None) # raises TerminationSignal / KeyboardInterrupt
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@contextlib.contextmanager
|
|
80
|
+
def shielded() -> Iterator[None]:
|
|
81
|
+
"""Hold SIGINT, SIGTERM and SIGHUP until the block ends, then deliver the first one.
|
|
82
|
+
|
|
83
|
+
For cleanup that must not stop halfway: stopping a server and removing its
|
|
84
|
+
pid file. Unlike :func:`terminate_like_interrupt`, which turns the signal
|
|
85
|
+
into an exception wherever the main thread is (inside ``psutil.wait_procs``
|
|
86
|
+
of a teardown, say), a signal arriving here is only recorded; once the
|
|
87
|
+
block is done the previous handlers are back and the signal is handled as
|
|
88
|
+
they would have (a :class:`TerminationSignal`, a ``KeyboardInterrupt``, or
|
|
89
|
+
the default action). A no-op outside the main thread.
|
|
90
|
+
"""
|
|
91
|
+
if threading.current_thread() is not threading.main_thread():
|
|
92
|
+
yield
|
|
93
|
+
return
|
|
94
|
+
held: list[int] = []
|
|
95
|
+
|
|
96
|
+
def _hold(signum: int, _frame: FrameType | None) -> None:
|
|
97
|
+
if not held:
|
|
98
|
+
held.append(signum)
|
|
99
|
+
|
|
100
|
+
previous: dict[int, object] = {}
|
|
101
|
+
for sig in (signal.SIGINT, *_signals()):
|
|
102
|
+
try:
|
|
103
|
+
previous[sig] = signal.signal(sig, _hold)
|
|
104
|
+
except (ValueError, OSError): # not supported on this platform
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
yield
|
|
108
|
+
finally:
|
|
109
|
+
for sig, handler in previous.items():
|
|
110
|
+
restore = signal.SIG_DFL if handler is None else handler
|
|
111
|
+
with contextlib.suppress(ValueError, OSError, TypeError):
|
|
112
|
+
signal.signal(sig, restore) # type: ignore[arg-type]
|
|
113
|
+
if held:
|
|
114
|
+
_deliver(held[0], previous.get(held[0]))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@contextlib.contextmanager
|
|
118
|
+
def terminate_like_interrupt() -> Iterator[None]:
|
|
119
|
+
"""Within the block, SIGTERM and SIGHUP raise :class:`TerminationSignal`.
|
|
120
|
+
|
|
121
|
+
A no-op outside the main thread (only it may install handlers). The
|
|
122
|
+
previous handlers are restored on exit; once one signal has arrived, further
|
|
123
|
+
ones are ignored so the cleanup it triggers runs to the end.
|
|
124
|
+
"""
|
|
125
|
+
if threading.current_thread() is not threading.main_thread():
|
|
126
|
+
yield
|
|
127
|
+
return
|
|
128
|
+
previous: dict[int, object] = {}
|
|
129
|
+
for sig in _signals():
|
|
130
|
+
try:
|
|
131
|
+
previous[sig] = signal.signal(sig, _raise)
|
|
132
|
+
except (ValueError, OSError): # not supported on this platform
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
yield
|
|
136
|
+
finally:
|
|
137
|
+
for sig, handler in previous.items():
|
|
138
|
+
# None: the previous handler was not installed from Python.
|
|
139
|
+
restore = signal.SIG_DFL if handler is None else handler
|
|
140
|
+
with contextlib.suppress(ValueError, OSError, TypeError):
|
|
141
|
+
signal.signal(sig, restore) # type: ignore[arg-type]
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""graph-agents-cli approvals commands: list and decide the calls agent runs wait on.
|
|
16
|
+
|
|
17
|
+
A call an API's ``approval`` block gates is not sent until an allowed
|
|
18
|
+
approver decides it: ``requester`` (the principal who started the run) and/or
|
|
19
|
+
``role:<name>`` (another principal holding that role). These commands are the
|
|
20
|
+
client of the chat API's approval routes, locally or against a deployed agent
|
|
21
|
+
(``--url``), with the credentials ``run`` sends:
|
|
22
|
+
|
|
23
|
+
* ``GET /threads/{thread_id}/approvals`` lists a thread's approvals;
|
|
24
|
+
* ``GET /approvals`` lists, across threads, the ones the caller may see (its
|
|
25
|
+
own, the ones its roles may decide, every one for a read-across role);
|
|
26
|
+
against an agent without it, the caller's own threads are searched;
|
|
27
|
+
* ``POST /threads/{thread_id}/approvals/{approval_id}`` decides one and
|
|
28
|
+
streams the resumed run (the same events as ``POST /chat``).
|
|
29
|
+
|
|
30
|
+
The decision carries only ``decision`` and ``comment``: the server sends
|
|
31
|
+
exactly the call it recorded, once, or nothing.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
from collections.abc import Callable, Iterator
|
|
39
|
+
from contextlib import contextmanager
|
|
40
|
+
from dataclasses import replace
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Any, NamedTuple
|
|
43
|
+
|
|
44
|
+
import click
|
|
45
|
+
import httpx
|
|
46
|
+
|
|
47
|
+
from graph_agents_cli import _chat_client
|
|
48
|
+
from graph_agents_cli._approvals import STATUS_PENDING, Approval, awaiting_lines, safe_text
|
|
49
|
+
from graph_agents_cli._chat_client import (
|
|
50
|
+
DECISION_APPROVE,
|
|
51
|
+
DECISION_REJECT,
|
|
52
|
+
STATUS_AWAITING_APPROVAL,
|
|
53
|
+
ChatClientError,
|
|
54
|
+
ChatHTTPError,
|
|
55
|
+
)
|
|
56
|
+
from graph_agents_cli._click import LazyGroup
|
|
57
|
+
from graph_agents_cli._project import (
|
|
58
|
+
chdir_project_root,
|
|
59
|
+
read_project_config,
|
|
60
|
+
require_agent_directory,
|
|
61
|
+
)
|
|
62
|
+
from graph_agents_cli._remote import API_KEY_ENV, build_headers
|
|
63
|
+
from graph_agents_cli.run import _local_server
|
|
64
|
+
from graph_agents_cli.run._signals import terminate_like_interrupt
|
|
65
|
+
from graph_agents_cli.run.cmd_run import (
|
|
66
|
+
AgentError,
|
|
67
|
+
DecisionRefused,
|
|
68
|
+
_auth_hint,
|
|
69
|
+
_build_resume_flags,
|
|
70
|
+
_ChatRenderer,
|
|
71
|
+
_local_api_key,
|
|
72
|
+
_project_auth_policy,
|
|
73
|
+
_TransportFailure,
|
|
74
|
+
decision_refused_message,
|
|
75
|
+
print_approval,
|
|
76
|
+
render_chat_events,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# What `list` / a decision without --thread-id look through: GET /approvals
|
|
80
|
+
# pages of 100 (or, against an agent without that route, the caller's most
|
|
81
|
+
# recently active threads, GET /threads pages of 100).
|
|
82
|
+
THREAD_PAGE = 100
|
|
83
|
+
MAX_THREAD_PAGES = 5
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class NoPendingRun(click.ClickException):
|
|
87
|
+
"""Nothing can be pending where the command looked (exit 1)."""
|
|
88
|
+
|
|
89
|
+
exit_code = 1
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _Target(NamedTuple):
|
|
93
|
+
base_url: str
|
|
94
|
+
headers: dict[str, str]
|
|
95
|
+
remote: bool
|
|
96
|
+
flags: str # repeated in printed commands (credentials redacted)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _local_checkpointer(project_root: Path, configured: str) -> str:
|
|
100
|
+
"""The checkpointer a local server would run with (the environment, then .env, then the manifest)."""
|
|
101
|
+
env = {**_local_server.dotenv_settings(project_root / ".env"), **os.environ}
|
|
102
|
+
return (env.get("CHECKPOINTER") or configured or "memory").strip().lower()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@contextmanager
|
|
106
|
+
def _target(url: str | None, header: tuple[str, ...], cookie: tuple[str, ...]) -> Iterator[_Target]:
|
|
107
|
+
"""Where the approvals live: ``--url``, or the project's local server.
|
|
108
|
+
|
|
109
|
+
Locally, the running server (``run --start-server``, or the one a paused
|
|
110
|
+
run kept) is used. With none running, a temporary server is started (and
|
|
111
|
+
stopped afterwards) only where a paused run and its approval outlive
|
|
112
|
+
their server: ``fastapi`` with a postgres checkpointer, and
|
|
113
|
+
``langgraph-server``, whose ``langgraph dev`` keeps its threads and the
|
|
114
|
+
app's approvals in ``.langgraph_api/`` of the project. With the fastapi
|
|
115
|
+
runtime's in-memory checkpointer, nothing can be pending.
|
|
116
|
+
"""
|
|
117
|
+
headers = build_headers(header, cookie)
|
|
118
|
+
flags = _build_resume_flags(url, "chat", header, cookie, None)
|
|
119
|
+
if url:
|
|
120
|
+
yield _Target(url.rstrip("/"), headers, True, flags)
|
|
121
|
+
return
|
|
122
|
+
chdir_project_root()
|
|
123
|
+
cfg = read_project_config()
|
|
124
|
+
require_agent_directory(cfg)
|
|
125
|
+
root = Path.cwd()
|
|
126
|
+
if not any(k.lower() == "authorization" for k in headers):
|
|
127
|
+
api_key = _local_api_key(root)
|
|
128
|
+
if api_key:
|
|
129
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
130
|
+
port = _local_server.get_server_port(root)
|
|
131
|
+
if port is not None:
|
|
132
|
+
_local_server.touch_activity(root)
|
|
133
|
+
yield _Target(f"http://127.0.0.1:{port}", headers, False, flags)
|
|
134
|
+
return
|
|
135
|
+
runtime = getattr(cfg, "runtime", "fastapi") or "fastapi"
|
|
136
|
+
checkpointer = _local_checkpointer(root, getattr(cfg, "checkpointer", "memory") or "memory")
|
|
137
|
+
if runtime == "fastapi" and checkpointer != "postgres":
|
|
138
|
+
raise NoPendingRun(
|
|
139
|
+
"No local server is running, and with the in-memory checkpointer "
|
|
140
|
+
f"(CHECKPOINTER={checkpointer}) a paused run lives only in the server that ran it: "
|
|
141
|
+
"nothing is pending. A run that pauses keeps its one-off server running; "
|
|
142
|
+
"`run --start-server` keeps one explicitly. Pass --url for a deployed agent."
|
|
143
|
+
)
|
|
144
|
+
with terminate_like_interrupt():
|
|
145
|
+
server = _local_server.ensure_server(
|
|
146
|
+
root, cfg.agent_directory, runtime=runtime, checkpointer=checkpointer
|
|
147
|
+
)
|
|
148
|
+
try:
|
|
149
|
+
yield _Target(server.base_url, headers, False, flags)
|
|
150
|
+
finally:
|
|
151
|
+
if server.started:
|
|
152
|
+
try:
|
|
153
|
+
_local_server.stop_server(root, pid=server.pid)
|
|
154
|
+
except _local_server.ServerStopError as exc: # never replaces the result
|
|
155
|
+
_local_server.warn_not_stopped(exc)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _http_failure(exc: ChatHTTPError, target: _Target, what: str) -> click.ClickException:
|
|
159
|
+
if exc.status_code == 401:
|
|
160
|
+
hint = _auth_hint(_project_auth_policy(remote=target.remote), remote=target.remote)
|
|
161
|
+
return click.ClickException(f"{what} failed (HTTP 401): not authenticated.{hint}")
|
|
162
|
+
if exc.status_code == 403:
|
|
163
|
+
return click.ClickException(
|
|
164
|
+
f"{what} failed (HTTP 403): you may not see this thread's approvals (its owner, its "
|
|
165
|
+
"approvers and read-across roles may)."
|
|
166
|
+
)
|
|
167
|
+
if exc.status_code == 404:
|
|
168
|
+
return click.ClickException(
|
|
169
|
+
f"{what} failed (HTTP 404): no such thread (a wrong id, a deleted thread, or an "
|
|
170
|
+
"agent without approval routes: check --url)."
|
|
171
|
+
)
|
|
172
|
+
return click.ClickException(f"{what} failed (HTTP {exc.status_code}):\n {exc.body}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _transport_failure(exc: httpx.TransportError, target: _Target) -> _TransportFailure:
|
|
176
|
+
where = target.base_url if target.remote else "the local server"
|
|
177
|
+
return _TransportFailure(f"Could not reach {where}: {exc}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _thread_approvals(target: _Target, thread_id: str) -> list[Approval]:
|
|
181
|
+
"""A thread's approvals; each is decided on the thread it was listed for."""
|
|
182
|
+
rows = _chat_client.list_approvals(target.base_url, thread_id, headers=target.headers)
|
|
183
|
+
found = [Approval.from_payload(row, thread_id=thread_id) for row in rows]
|
|
184
|
+
return [replace(a, thread_id=thread_id) for a in found if a is not None]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _own_thread_ids(target: _Target) -> tuple[list[str], bool]:
|
|
188
|
+
"""The caller's thread ids (most recent first, as the server lists them); True when cut."""
|
|
189
|
+
ids: list[str] = []
|
|
190
|
+
for page in range(MAX_THREAD_PAGES):
|
|
191
|
+
rows = _chat_client.list_threads(
|
|
192
|
+
target.base_url, headers=target.headers, limit=THREAD_PAGE, offset=page * THREAD_PAGE
|
|
193
|
+
)
|
|
194
|
+
ids.extend(str(r["thread_id"]) for r in rows if isinstance(r.get("thread_id"), str))
|
|
195
|
+
if len(rows) < THREAD_PAGE:
|
|
196
|
+
return ids, False
|
|
197
|
+
return ids, True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _visible_approvals(
|
|
201
|
+
target: _Target, *, pending_only: bool
|
|
202
|
+
) -> tuple[list[Approval], bool] | None:
|
|
203
|
+
"""GET /approvals, page by page: every approval the caller may see; True when cut.
|
|
204
|
+
|
|
205
|
+
None when the agent has no such route (404/405 on the first page).
|
|
206
|
+
"""
|
|
207
|
+
found: list[Approval] = []
|
|
208
|
+
for page in range(MAX_THREAD_PAGES):
|
|
209
|
+
try:
|
|
210
|
+
rows = _chat_client.list_visible_approvals(
|
|
211
|
+
target.base_url,
|
|
212
|
+
status=STATUS_PENDING if pending_only else None,
|
|
213
|
+
headers=target.headers,
|
|
214
|
+
limit=THREAD_PAGE,
|
|
215
|
+
offset=page * THREAD_PAGE,
|
|
216
|
+
)
|
|
217
|
+
except ChatHTTPError as exc:
|
|
218
|
+
if page == 0 and exc.status_code in (404, 405):
|
|
219
|
+
return None
|
|
220
|
+
raise
|
|
221
|
+
for row in rows:
|
|
222
|
+
approval = Approval.from_payload(row)
|
|
223
|
+
if approval is not None and approval.thread_id:
|
|
224
|
+
found.append(approval)
|
|
225
|
+
if len(rows) < THREAD_PAGE:
|
|
226
|
+
return found, False
|
|
227
|
+
return found, True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class _Collected(NamedTuple):
|
|
231
|
+
approvals: list[Approval]
|
|
232
|
+
cut: bool # more than the pages looked through
|
|
233
|
+
own_threads_only: bool # the fallback scan: another principal's thread needs --thread-id
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _collect(target: _Target, thread_id: str | None, *, pending_only: bool = False) -> _Collected:
|
|
237
|
+
"""Approvals of one thread, or every one the caller may see (see the module doc)."""
|
|
238
|
+
if thread_id:
|
|
239
|
+
return _Collected(_thread_approvals(target, thread_id), False, False)
|
|
240
|
+
visible = _visible_approvals(target, pending_only=pending_only)
|
|
241
|
+
if visible is not None:
|
|
242
|
+
return _Collected(visible[0], visible[1], False)
|
|
243
|
+
thread_ids, cut = _own_thread_ids(target)
|
|
244
|
+
approvals: list[Approval] = []
|
|
245
|
+
for tid in thread_ids:
|
|
246
|
+
try:
|
|
247
|
+
approvals.extend(_thread_approvals(target, tid))
|
|
248
|
+
except ChatHTTPError as exc:
|
|
249
|
+
if exc.status_code in (403, 404): # deleted meanwhile, or not visible: skip it
|
|
250
|
+
continue
|
|
251
|
+
raise
|
|
252
|
+
return _Collected(approvals, cut, True)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _common_options(function: Callable[..., Any]) -> Callable[..., Any]:
|
|
256
|
+
function = click.option(
|
|
257
|
+
"--cookie",
|
|
258
|
+
"cookie",
|
|
259
|
+
multiple=True,
|
|
260
|
+
help="Cookie ('name=value'), for a custom auth policy that reads cookies. Repeatable.",
|
|
261
|
+
)(function)
|
|
262
|
+
function = click.option(
|
|
263
|
+
"--header",
|
|
264
|
+
"-H",
|
|
265
|
+
"header",
|
|
266
|
+
multiple=True,
|
|
267
|
+
help=(
|
|
268
|
+
"Custom HTTP header ('Key: Value'). Repeatable. An Authorization header overrides "
|
|
269
|
+
f"{API_KEY_ENV}; prefer the variable for bearer credentials."
|
|
270
|
+
),
|
|
271
|
+
)(function)
|
|
272
|
+
function = click.option(
|
|
273
|
+
"--url",
|
|
274
|
+
default=None,
|
|
275
|
+
help="Base URL of a deployed agent. Without it, the project's local server.",
|
|
276
|
+
)(function)
|
|
277
|
+
return function
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@click.group("approvals", cls=LazyGroup)
|
|
281
|
+
def approvals_group() -> None:
|
|
282
|
+
"""List and decide the gated API calls agent runs are waiting on.
|
|
283
|
+
|
|
284
|
+
A call gated by an API's approval block in api-policy.yaml is not sent
|
|
285
|
+
until an approver decides it: requester (the principal who started the
|
|
286
|
+
run) and/or role:<name> (a principal holding that role; a requester
|
|
287
|
+
decides their own call only when requester is listed). Approving sends
|
|
288
|
+
exactly the call shown, once; rejecting, or letting it expire, sends
|
|
289
|
+
nothing and the agent is told it was not approved.
|
|
290
|
+
|
|
291
|
+
\b
|
|
292
|
+
Credentials follow the project's auth policy, as for `run`: put a bearer
|
|
293
|
+
credential in GRAPH_AGENTS_CLI_API_KEY (locally, the API_KEY in .env is
|
|
294
|
+
used when it is unset), or pass --header / --cookie. Each approver uses
|
|
295
|
+
their own credential.
|
|
296
|
+
|
|
297
|
+
\b
|
|
298
|
+
Exit codes:
|
|
299
|
+
0 listed, or decided (the resumed run was shown)
|
|
300
|
+
1 refused (not an allowed approver, already decided, expired, not
|
|
301
|
+
found), or the resumed run ended with an error
|
|
302
|
+
2 the agent could not be reached
|
|
303
|
+
3 configuration error (not in a project without --url)
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@approvals_group.command("list")
|
|
308
|
+
@_common_options
|
|
309
|
+
@click.option(
|
|
310
|
+
"--thread-id",
|
|
311
|
+
default=None,
|
|
312
|
+
help=(
|
|
313
|
+
"The thread to list. Without it: every approval you may see (your own, the ones "
|
|
314
|
+
"your roles may decide, all of them for a read-across role; the newest "
|
|
315
|
+
f"{THREAD_PAGE * MAX_THREAD_PAGES})."
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
@click.option(
|
|
319
|
+
"--all", "show_all", is_flag=True, default=False, help="Also list decided and expired ones."
|
|
320
|
+
)
|
|
321
|
+
@click.option("--json", "as_json", is_flag=True, default=False, help="Print JSON.")
|
|
322
|
+
def cmd_list(
|
|
323
|
+
url: str | None,
|
|
324
|
+
header: tuple[str, ...],
|
|
325
|
+
cookie: tuple[str, ...],
|
|
326
|
+
thread_id: str | None,
|
|
327
|
+
show_all: bool,
|
|
328
|
+
as_json: bool,
|
|
329
|
+
) -> None:
|
|
330
|
+
"""List pending approvals (a thread's, or every one you may see)."""
|
|
331
|
+
try:
|
|
332
|
+
with _target(url, header, cookie) as target:
|
|
333
|
+
flags = target.flags
|
|
334
|
+
try:
|
|
335
|
+
approvals, cut, own_threads_only = _collect(
|
|
336
|
+
target, thread_id, pending_only=not show_all
|
|
337
|
+
)
|
|
338
|
+
except ChatHTTPError as exc:
|
|
339
|
+
raise _http_failure(exc, target, "Listing approvals") from exc
|
|
340
|
+
except httpx.TransportError as exc:
|
|
341
|
+
raise _transport_failure(exc, target) from exc
|
|
342
|
+
except ChatClientError as exc:
|
|
343
|
+
raise click.ClickException(f"Listing approvals failed: {exc}") from exc
|
|
344
|
+
except NoPendingRun as exc:
|
|
345
|
+
if as_json:
|
|
346
|
+
click.echo(json.dumps({"approvals": [], "note": exc.message}, indent=2))
|
|
347
|
+
else:
|
|
348
|
+
click.echo(exc.message)
|
|
349
|
+
return
|
|
350
|
+
shown = [a for a in approvals if show_all or a.pending]
|
|
351
|
+
if as_json:
|
|
352
|
+
payload = {"approvals": [dict(a.raw, thread_id=a.thread_id) for a in shown]}
|
|
353
|
+
if cut:
|
|
354
|
+
payload["truncated"] = True
|
|
355
|
+
# ASCII-escaped: the call's text came from the model.
|
|
356
|
+
click.echo(json.dumps(payload, indent=2))
|
|
357
|
+
return
|
|
358
|
+
what = "approvals" if show_all else "pending approvals"
|
|
359
|
+
if thread_id:
|
|
360
|
+
where = f"thread {safe_text(thread_id)}"
|
|
361
|
+
else:
|
|
362
|
+
where = "your threads" if own_threads_only else "the threads you may see"
|
|
363
|
+
if not shown:
|
|
364
|
+
click.echo(f"No {what} on {where}.")
|
|
365
|
+
for approval in shown:
|
|
366
|
+
click.echo()
|
|
367
|
+
for line in approval.lines():
|
|
368
|
+
click.echo(line)
|
|
369
|
+
if approval.pending and approval.thread_id:
|
|
370
|
+
for line in awaiting_lines(approval, approval.thread_id, flags)[1:3]:
|
|
371
|
+
click.echo(line)
|
|
372
|
+
if cut:
|
|
373
|
+
searched = (
|
|
374
|
+
f"your {THREAD_PAGE * MAX_THREAD_PAGES} most recent threads"
|
|
375
|
+
if own_threads_only
|
|
376
|
+
else f"the newest {THREAD_PAGE * MAX_THREAD_PAGES} approvals"
|
|
377
|
+
)
|
|
378
|
+
click.secho(
|
|
379
|
+
f"Only {searched} were searched; pass --thread-id for an older one.", fg="yellow"
|
|
380
|
+
)
|
|
381
|
+
if own_threads_only:
|
|
382
|
+
click.secho(
|
|
383
|
+
"This agent lists approvals per thread only: one on another principal's thread "
|
|
384
|
+
"(a role: approver's) needs --thread-id.",
|
|
385
|
+
dim=True,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _find(target: _Target, approval_id: str, thread_id: str | None) -> Approval:
|
|
390
|
+
approvals, cut, own_threads_only = _collect(target, thread_id)
|
|
391
|
+
for approval in approvals:
|
|
392
|
+
if approval.approval_id == approval_id:
|
|
393
|
+
return approval
|
|
394
|
+
if thread_id:
|
|
395
|
+
where, extra = f"thread {safe_text(thread_id)}", ""
|
|
396
|
+
elif own_threads_only:
|
|
397
|
+
where = "your threads"
|
|
398
|
+
extra = (
|
|
399
|
+
"; an approval on another principal's thread needs --thread-id (the requester's "
|
|
400
|
+
"`run` printed it)"
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
where, extra = "the threads you may see", ""
|
|
404
|
+
if cut:
|
|
405
|
+
extra += "; only the most recent ones were searched (pass --thread-id)"
|
|
406
|
+
raise NoPendingRun(f"No approval {safe_text(approval_id)} on {where}{extra}.")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _decide(
|
|
410
|
+
decision: str,
|
|
411
|
+
approval_id: str,
|
|
412
|
+
*,
|
|
413
|
+
url: str | None,
|
|
414
|
+
header: tuple[str, ...],
|
|
415
|
+
cookie: tuple[str, ...],
|
|
416
|
+
thread_id: str | None,
|
|
417
|
+
comment: str | None,
|
|
418
|
+
verbose: bool,
|
|
419
|
+
) -> None:
|
|
420
|
+
with _target(url, header, cookie) as target:
|
|
421
|
+
try:
|
|
422
|
+
approval = _find(target, approval_id, thread_id)
|
|
423
|
+
except ChatHTTPError as exc:
|
|
424
|
+
raise _http_failure(exc, target, "Looking up the approval") from exc
|
|
425
|
+
except httpx.TransportError as exc:
|
|
426
|
+
raise _transport_failure(exc, target) from exc
|
|
427
|
+
except ChatClientError as exc:
|
|
428
|
+
raise click.ClickException(f"Looking up the approval failed: {exc}") from exc
|
|
429
|
+
# The decider sees the call before it is decided, whoever started the run.
|
|
430
|
+
print_approval(approval)
|
|
431
|
+
if not approval.pending:
|
|
432
|
+
raise DecisionRefused(
|
|
433
|
+
f"Approval {safe_text(approval_id)} is {safe_text(approval.status)}, not pending: "
|
|
434
|
+
"an approval is decided once."
|
|
435
|
+
)
|
|
436
|
+
assert approval.thread_id is not None
|
|
437
|
+
verb = "Approving" if decision == DECISION_APPROVE else "Rejecting"
|
|
438
|
+
click.secho(
|
|
439
|
+
f"{verb}; the resumed run follows.",
|
|
440
|
+
fg="green" if decision == DECISION_APPROVE else "yellow",
|
|
441
|
+
)
|
|
442
|
+
renderer = _ChatRenderer(verbose=verbose)
|
|
443
|
+
try:
|
|
444
|
+
events = _chat_client.decide_approval(
|
|
445
|
+
target.base_url,
|
|
446
|
+
approval.thread_id,
|
|
447
|
+
approval.approval_id,
|
|
448
|
+
decision,
|
|
449
|
+
comment=comment,
|
|
450
|
+
headers=target.headers,
|
|
451
|
+
)
|
|
452
|
+
outcome = render_chat_events(events, renderer=renderer)
|
|
453
|
+
except ChatHTTPError as exc:
|
|
454
|
+
raise DecisionRefused(
|
|
455
|
+
decision_refused_message(
|
|
456
|
+
exc,
|
|
457
|
+
approval_id,
|
|
458
|
+
remote=target.remote,
|
|
459
|
+
policy=_project_auth_policy(remote=target.remote),
|
|
460
|
+
)
|
|
461
|
+
) from exc
|
|
462
|
+
except AgentError as exc:
|
|
463
|
+
click.secho(f"[error: {exc.code}]: {exc.message}", fg="red")
|
|
464
|
+
raise click.exceptions.Exit(1) from exc
|
|
465
|
+
except httpx.TransportError as exc:
|
|
466
|
+
raise _TransportFailure(
|
|
467
|
+
f"The connection dropped while the resumed run streamed: {exc}\n"
|
|
468
|
+
" The decision was recorded if the server answered; check with "
|
|
469
|
+
f"`graph-agents-cli approvals list --all --thread-id {approval.thread_id}`."
|
|
470
|
+
) from exc
|
|
471
|
+
if outcome.status == STATUS_AWAITING_APPROVAL:
|
|
472
|
+
nxt = Approval.from_payload(outcome.approval, thread_id=approval.thread_id)
|
|
473
|
+
if nxt is not None:
|
|
474
|
+
nxt = replace(nxt, thread_id=approval.thread_id)
|
|
475
|
+
print_approval(nxt)
|
|
476
|
+
click.echo()
|
|
477
|
+
lines = awaiting_lines(nxt, nxt.thread_id, target.flags)
|
|
478
|
+
click.secho(lines[0], fg="yellow", bold=True)
|
|
479
|
+
for line in lines[1:]:
|
|
480
|
+
click.secho(line, fg="yellow")
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _decision_command(decision: str) -> click.Command:
|
|
484
|
+
approve = decision == DECISION_APPROVE
|
|
485
|
+
summary = (
|
|
486
|
+
"Approve a pending call: it is sent exactly as shown, once, and the run resumes."
|
|
487
|
+
if approve
|
|
488
|
+
else "Reject a pending call: it is never sent, and the run resumes without it."
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
@click.command(decision, help=summary, short_help=summary)
|
|
492
|
+
@click.argument("approval_id")
|
|
493
|
+
@_common_options
|
|
494
|
+
@click.option(
|
|
495
|
+
"--thread-id",
|
|
496
|
+
default=None,
|
|
497
|
+
help=(
|
|
498
|
+
"The approval's thread (printed with the approval). Without it, every approval "
|
|
499
|
+
"you may see is searched."
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
@click.option("--comment", default=None, help="Why; recorded with the decision.")
|
|
503
|
+
@click.option(
|
|
504
|
+
"--verbose", "-v", is_flag=True, default=False, help="Print each event of the resumed run."
|
|
505
|
+
)
|
|
506
|
+
def command(
|
|
507
|
+
approval_id: str,
|
|
508
|
+
url: str | None,
|
|
509
|
+
header: tuple[str, ...],
|
|
510
|
+
cookie: tuple[str, ...],
|
|
511
|
+
thread_id: str | None,
|
|
512
|
+
comment: str | None,
|
|
513
|
+
verbose: bool,
|
|
514
|
+
) -> None:
|
|
515
|
+
_decide(
|
|
516
|
+
decision,
|
|
517
|
+
approval_id,
|
|
518
|
+
url=url,
|
|
519
|
+
header=header,
|
|
520
|
+
cookie=cookie,
|
|
521
|
+
thread_id=thread_id,
|
|
522
|
+
comment=comment,
|
|
523
|
+
verbose=verbose,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return command
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
approvals_group.add_command(_decision_command(DECISION_APPROVE))
|
|
530
|
+
approvals_group.add_command(_decision_command(DECISION_REJECT))
|