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,664 @@
|
|
|
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
|
+
"""A system file resolved against its projects: agents, edges, environments and URLs.
|
|
16
|
+
|
|
17
|
+
``resolve`` reads every project the file names (its manifest, ``api-policy.yaml``,
|
|
18
|
+
the generated peers module and the chart's values) and turns each ``calls`` entry
|
|
19
|
+
into a ``Link`` with what ``peer add`` would be given for it. What makes the file
|
|
20
|
+
unusable is a ``SystemFileError`` (exit 3): a project that does not exist or that
|
|
21
|
+
two agents name, an edge to an unknown agent or to the agent itself, the same agent called twice by
|
|
22
|
+
one agent, two agents with one client id, an ``auth: exchange`` edge without
|
|
23
|
+
``identity``, an environment a project's manifest does not know. Everything else
|
|
24
|
+
is a finding of ``system check``.
|
|
25
|
+
|
|
26
|
+
The URL an agent is called at in an environment:
|
|
27
|
+
|
|
28
|
+
- ``port_base`` (local processes): ``http://127.0.0.1:<port_base + i>``, i its
|
|
29
|
+
position in ``agents``;
|
|
30
|
+
- ``url``: the template with ``{agent}``, ``{project}`` and ``{env}`` filled;
|
|
31
|
+
- otherwise in the cluster: ``http://<chart fullname>.<namespace>.svc.cluster.local``
|
|
32
|
+
(``:<service.port>`` unless 80), the namespace from the called project's
|
|
33
|
+
manifest (``environments.<env>.namespace``, default ``<project>-<env>``) and the
|
|
34
|
+
fullname the release name, which ``deploy`` sets to the project's name.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import copy
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
from functools import cached_property
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
import click
|
|
46
|
+
|
|
47
|
+
from graph_agents_cli._api_policy import (
|
|
48
|
+
DECIDE_DIRECT,
|
|
49
|
+
DECIDE_RELAYED,
|
|
50
|
+
REQUESTER_APPROVER,
|
|
51
|
+
approval_rules,
|
|
52
|
+
)
|
|
53
|
+
from graph_agents_cli._defaults import normalize_auth_policy
|
|
54
|
+
from graph_agents_cli._project import MANIFEST_FILENAME, ProjectConfig
|
|
55
|
+
from graph_agents_cli.api._files import chart_values_files, read_text
|
|
56
|
+
from graph_agents_cli.api.cmd_api import _Project, load_project_at
|
|
57
|
+
from graph_agents_cli.deploy._values import deep_merge, load_yaml
|
|
58
|
+
from graph_agents_cli.peer import _generate as gen
|
|
59
|
+
from graph_agents_cli.peer.cmd_peer import AUTH_BY_POLICY, PeerOptions, runtime_problem
|
|
60
|
+
from graph_agents_cli.system._model import (
|
|
61
|
+
DENY,
|
|
62
|
+
RELAY,
|
|
63
|
+
Edge,
|
|
64
|
+
SystemFile,
|
|
65
|
+
SystemFileError,
|
|
66
|
+
load,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
LOCAL = "local"
|
|
70
|
+
URL = "url"
|
|
71
|
+
CLUSTER = "cluster"
|
|
72
|
+
DEFAULT_SERVICE_PORT = 80
|
|
73
|
+
DEFAULT_TARGET_PORT = 8000
|
|
74
|
+
# `AUTH_MAX_DELEGATION_DEPTH` when a project does not set it (the template's default).
|
|
75
|
+
DEFAULT_MAX_DELEGATION_DEPTH = 3
|
|
76
|
+
# `DB_POOL_MAX_SIZE` when unset (the template's default), and LangGraph Server's own pool
|
|
77
|
+
# (`LANGGRAPH_POSTGRES_POOL_MAX_SIZE`, 150 in langgraph-api 0.14), which a langgraph-server
|
|
78
|
+
# replica opens beside the app's.
|
|
79
|
+
DEFAULT_DB_POOL_MAX_SIZE = 10
|
|
80
|
+
DEFAULT_LANGGRAPH_POOL_MAX_SIZE = 150
|
|
81
|
+
CLIENT_ACTOR_PREFIX = "client:"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class Env:
|
|
86
|
+
"""An environment of the file: where the agents run, so how they are called."""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
kind: str # LOCAL, URL or CLUSTER
|
|
90
|
+
port_base: int | None = None
|
|
91
|
+
url: str | None = None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def charted(self) -> bool:
|
|
95
|
+
"""Whether the agents run from their charts here (values-<env>.yaml)."""
|
|
96
|
+
return self.kind != LOCAL
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class Node:
|
|
101
|
+
"""One agent of the system and what its project says."""
|
|
102
|
+
|
|
103
|
+
name: str
|
|
104
|
+
index: int
|
|
105
|
+
root: Path
|
|
106
|
+
project: _Project
|
|
107
|
+
client_id: str
|
|
108
|
+
# The actor id the agents it calls see (act.sub): the file's actor_id, else client_id.
|
|
109
|
+
actor_id: str = ""
|
|
110
|
+
_values: dict[str | None, dict[str, Any]] = field(default_factory=dict, repr=False)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def config(self) -> ProjectConfig:
|
|
114
|
+
return self.project.config
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def document(self) -> dict[str, Any] | None:
|
|
118
|
+
return self.project.document
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def release(self) -> str:
|
|
122
|
+
"""The Helm release (and chart fullname): the project's name, as ``deploy`` sets it."""
|
|
123
|
+
return self.config.project_name[:63].rstrip("-")
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def chart_name(self) -> str:
|
|
127
|
+
"""The chart's ``agent.name``: ``nameOverride``, else the chart's name (the project's)."""
|
|
128
|
+
override = self.values(None).get("nameOverride")
|
|
129
|
+
name = str(override) if override else self.config.project_name
|
|
130
|
+
return name[:63].rstrip("-")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def pod_labels(self) -> dict[str, str]:
|
|
134
|
+
"""The agent pods' selector labels (the chart's ``agent.selectorLabels``): the name and
|
|
135
|
+
the release. The bundled database's pods share the release label, not the name."""
|
|
136
|
+
return {
|
|
137
|
+
"app.kubernetes.io/name": self.chart_name,
|
|
138
|
+
"app.kubernetes.io/instance": self.release,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def runtime_problem(self) -> str | None:
|
|
143
|
+
return runtime_problem(self.root, self.config.agent_directory)
|
|
144
|
+
|
|
145
|
+
@cached_property
|
|
146
|
+
def chart_files(self) -> list[Path]:
|
|
147
|
+
return chart_values_files(self.root, self.config)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def has_chart(self) -> bool:
|
|
151
|
+
return bool(self.chart_files)
|
|
152
|
+
|
|
153
|
+
def values_file(self, env: str) -> Path | None:
|
|
154
|
+
"""``values-<env>.yaml`` when the chart has it."""
|
|
155
|
+
for path in self.chart_files[1:]:
|
|
156
|
+
if path.stem == f"values-{env}":
|
|
157
|
+
return path
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def values(self, env: str | None = None) -> dict[str, Any]:
|
|
161
|
+
"""The chart's values merged as ``deploy --env`` merges them (``values.yaml`` alone
|
|
162
|
+
for None); empty without a chart. Unreadable values read as empty (``deploy``
|
|
163
|
+
reports them)."""
|
|
164
|
+
if env not in self._values:
|
|
165
|
+
merged: dict[str, Any] = {}
|
|
166
|
+
if self.chart_files:
|
|
167
|
+
try:
|
|
168
|
+
merged = load_yaml(self.chart_files[0])
|
|
169
|
+
extra = self.values_file(env) if env else None
|
|
170
|
+
if extra is not None:
|
|
171
|
+
merged = deep_merge(merged, load_yaml(extra))
|
|
172
|
+
except click.ClickException:
|
|
173
|
+
merged = {}
|
|
174
|
+
self._values[env] = merged
|
|
175
|
+
return copy.deepcopy(self._values[env])
|
|
176
|
+
|
|
177
|
+
def chart_env(self, env: str | None = None) -> dict[str, Any]:
|
|
178
|
+
found = self.values(env).get("env")
|
|
179
|
+
return dict(found) if isinstance(found, dict) else {}
|
|
180
|
+
|
|
181
|
+
def setting(self, env: str | None, key: str) -> str:
|
|
182
|
+
"""A chart ``env`` setting as a string ('' when unset or null)."""
|
|
183
|
+
value = self.chart_env(env).get(key)
|
|
184
|
+
return "" if value is None else str(value).strip()
|
|
185
|
+
|
|
186
|
+
def auth_policy(self, env: str | None = None) -> str:
|
|
187
|
+
"""``env.AUTH_POLICY`` of the chart values, else the manifest's policy."""
|
|
188
|
+
raw = self.setting(env, "AUTH_POLICY")
|
|
189
|
+
return normalize_auth_policy(raw or self.config.auth_policy, warn=False)
|
|
190
|
+
|
|
191
|
+
def a2a_name(self, env: str | None = None) -> str:
|
|
192
|
+
return self.setting(env, "A2A_NAME") or self.config.agent_directory
|
|
193
|
+
|
|
194
|
+
def mount(self, env: str | None = None) -> str:
|
|
195
|
+
"""Where the agent serves A2A: ``/a2a/<A2A_NAME>``."""
|
|
196
|
+
return f"/a2a/{self.a2a_name(env)}"
|
|
197
|
+
|
|
198
|
+
def namespace(self, env: str) -> str:
|
|
199
|
+
return self.config.environment(env)[1]
|
|
200
|
+
|
|
201
|
+
def context(self, env: str) -> str | None:
|
|
202
|
+
return self.config.environment(env)[0] or None
|
|
203
|
+
|
|
204
|
+
def _port(self, env: str, key: str, default: int) -> int | str:
|
|
205
|
+
service = self.values(env).get("service")
|
|
206
|
+
value = service.get(key) if isinstance(service, dict) else None
|
|
207
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
208
|
+
return value
|
|
209
|
+
if isinstance(value, str) and value.strip():
|
|
210
|
+
return int(value) if value.strip().isdigit() else value.strip()
|
|
211
|
+
return default
|
|
212
|
+
|
|
213
|
+
def service_port(self, env: str) -> int | str:
|
|
214
|
+
return self._port(env, "port", DEFAULT_SERVICE_PORT)
|
|
215
|
+
|
|
216
|
+
def target_port(self, env: str) -> int | str:
|
|
217
|
+
"""The pods' http port (``service.targetPort``): what a NetworkPolicy matches."""
|
|
218
|
+
return self._port(env, "targetPort", DEFAULT_TARGET_PORT)
|
|
219
|
+
|
|
220
|
+
def replicas(self, env: str) -> tuple[int, bool]:
|
|
221
|
+
"""``(replicas, autoscaled)``: ``hpa.maxReplicas`` while the HPA is on."""
|
|
222
|
+
values = self.values(env)
|
|
223
|
+
hpa = values.get("hpa") if isinstance(values.get("hpa"), dict) else {}
|
|
224
|
+
if _true(hpa.get("enabled")):
|
|
225
|
+
return _int(hpa.get("maxReplicas"), 1), True
|
|
226
|
+
return _int(values.get("replicaCount"), 1), False
|
|
227
|
+
|
|
228
|
+
def task_store(self, env: str | None = None) -> str:
|
|
229
|
+
"""Where the agent keeps A2A tasks: ``postgres`` (shared by replicas) or ``memory``."""
|
|
230
|
+
if self.config.runtime == "langgraph-server":
|
|
231
|
+
return "postgres"
|
|
232
|
+
if self.has_chart:
|
|
233
|
+
checkpointer = self.setting(env, "CHECKPOINTER") or "postgres"
|
|
234
|
+
else:
|
|
235
|
+
checkpointer = self.config.checkpointer
|
|
236
|
+
return "memory" if checkpointer.lower() == "memory" else "postgres"
|
|
237
|
+
|
|
238
|
+
def app_url(self, env: str) -> str:
|
|
239
|
+
"""The base URL the agent's card advertises (the chart's ``agent.appUrl``)."""
|
|
240
|
+
values = self.values(env)
|
|
241
|
+
if values.get("appUrl"):
|
|
242
|
+
return str(values["appUrl"])
|
|
243
|
+
if self.setting(env, "APP_URL"):
|
|
244
|
+
return self.setting(env, "APP_URL")
|
|
245
|
+
gateway = values.get("gateway") if isinstance(values.get("gateway"), dict) else {}
|
|
246
|
+
if _true(gateway.get("enabled")) and gateway.get("hostname"):
|
|
247
|
+
return f"https://{gateway['hostname']}"
|
|
248
|
+
ingress = values.get("ingress") if isinstance(values.get("ingress"), dict) else {}
|
|
249
|
+
if _true(ingress.get("enabled")) and ingress.get("hostname"):
|
|
250
|
+
tls = values.get("tls") if isinstance(values.get("tls"), dict) else {}
|
|
251
|
+
manager = tls.get("certManager") if isinstance(tls.get("certManager"), dict) else {}
|
|
252
|
+
secure = bool(tls.get("existingSecret")) or _true(manager.get("enabled"))
|
|
253
|
+
return f"{'https' if secure else 'http'}://{ingress['hostname']}"
|
|
254
|
+
return ""
|
|
255
|
+
|
|
256
|
+
def route_enabled(self, env: str) -> bool:
|
|
257
|
+
values = self.values(env)
|
|
258
|
+
return any(
|
|
259
|
+
_true((values.get(key) or {}).get("enabled"))
|
|
260
|
+
for key in ("gateway", "ingress")
|
|
261
|
+
if isinstance(values.get(key), dict)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def public_paths(self, env: str) -> list[dict[str, Any]]:
|
|
265
|
+
"""What the route publishes (``route.publicPaths``; empty with no route)."""
|
|
266
|
+
if not self.route_enabled(env):
|
|
267
|
+
return []
|
|
268
|
+
route = self.values(env).get("route")
|
|
269
|
+
paths = route.get("publicPaths") if isinstance(route, dict) else None
|
|
270
|
+
return [p for p in paths or [] if isinstance(p, dict)]
|
|
271
|
+
|
|
272
|
+
def max_delegation_depth(self, env: str | None = None) -> int:
|
|
273
|
+
raw = self.setting(env, "AUTH_MAX_DELEGATION_DEPTH")
|
|
274
|
+
return int(raw) if raw.isdigit() else DEFAULT_MAX_DELEGATION_DEPTH
|
|
275
|
+
|
|
276
|
+
def allowed_actors(self, env: str | None = None) -> list[str]:
|
|
277
|
+
return [a.strip() for a in self.setting(env, "AUTH_ALLOWED_ACTORS").split(",") if a.strip()]
|
|
278
|
+
|
|
279
|
+
@cached_property
|
|
280
|
+
def module_text(self) -> str | None:
|
|
281
|
+
return read_text(gen.module_path(self.root, self.config.agent_directory))
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def module_names(self) -> dict[str, str]:
|
|
285
|
+
return gen.names_in(self.module_text)
|
|
286
|
+
|
|
287
|
+
@cached_property
|
|
288
|
+
def peers(self) -> dict[str, gen.Peer]:
|
|
289
|
+
"""The peers the project declares, by peer name."""
|
|
290
|
+
return {p.name: p for p in gen.peers_of(self.document, self.module_names)}
|
|
291
|
+
|
|
292
|
+
def entry(self, api: str) -> dict[str, Any] | None:
|
|
293
|
+
found = ((self.document or {}).get("apis") or {}).get(api)
|
|
294
|
+
return found if isinstance(found, dict) else None
|
|
295
|
+
|
|
296
|
+
def requester_gates(self, document: dict[str, Any] | None = None) -> list[Gate]:
|
|
297
|
+
"""Every approval rule a requester decides, in the policy (what a relay reaches);
|
|
298
|
+
``document``: the policy as a plan will leave it (default: as it is)."""
|
|
299
|
+
gates: list[Gate] = []
|
|
300
|
+
policy = self.document if document is None else document
|
|
301
|
+
for api_name, api in ((policy or {}).get("apis") or {}).items():
|
|
302
|
+
rules = approval_rules(api) if isinstance(api, dict) else []
|
|
303
|
+
listed = isinstance(api.get("approval"), list)
|
|
304
|
+
for index, rule in enumerate(rules):
|
|
305
|
+
if REQUESTER_APPROVER not in [str(a) for a in rule.get("approvers") or []]:
|
|
306
|
+
continue
|
|
307
|
+
gates.append(
|
|
308
|
+
Gate(
|
|
309
|
+
api=str(api_name),
|
|
310
|
+
rule=index if listed else None,
|
|
311
|
+
decide_with=str(rule.get("decide_with") or DECIDE_DIRECT),
|
|
312
|
+
relayers=[str(r) for r in rule.get("relayers") or []],
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return gates
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass(frozen=True)
|
|
319
|
+
class Gate:
|
|
320
|
+
"""A requester gate of a called agent, as `api approval` names it."""
|
|
321
|
+
|
|
322
|
+
api: str
|
|
323
|
+
rule: int | None # --rule N for a list of rules
|
|
324
|
+
decide_with: str
|
|
325
|
+
relayers: list[str]
|
|
326
|
+
|
|
327
|
+
def relays(self, actor: str) -> bool:
|
|
328
|
+
return self.decide_with == DECIDE_RELAYED and actor in self.relayers
|
|
329
|
+
|
|
330
|
+
def command(self, actor: str) -> str:
|
|
331
|
+
"""The `api approval` line that lets ``actor`` relay the requester's decision here."""
|
|
332
|
+
relayers = [*self.relayers, actor] if actor not in self.relayers else self.relayers
|
|
333
|
+
rule = f" --rule {self.rule}" if self.rule is not None else ""
|
|
334
|
+
return (
|
|
335
|
+
f"graph-agents-cli api approval {self.api}{rule} --decide-with relayed "
|
|
336
|
+
f"--relayers {','.join(relayers)}"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@dataclass
|
|
341
|
+
class Link:
|
|
342
|
+
"""One edge: ``caller`` calls ``callee``, as the file says."""
|
|
343
|
+
|
|
344
|
+
caller: Node
|
|
345
|
+
callee: Node
|
|
346
|
+
edge: Edge
|
|
347
|
+
|
|
348
|
+
@property
|
|
349
|
+
def auth(self) -> str:
|
|
350
|
+
return self.edge.auth or AUTH_BY_POLICY.get(self.caller.config.auth_policy, "bearer")
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def peer(self) -> str:
|
|
354
|
+
return self.callee.name
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def existing(self) -> gen.Peer | None:
|
|
358
|
+
"""The caller's peer of the callee's name, when it has one (its API and variables are
|
|
359
|
+
kept: a peer added with --api-name or --url-env keeps them)."""
|
|
360
|
+
return self.caller.peers.get(self.callee.name)
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def entry(self) -> dict[str, Any] | None:
|
|
364
|
+
peer = self.existing
|
|
365
|
+
return self.caller.entry(peer.api) if peer else None
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def api_name(self) -> str:
|
|
369
|
+
peer = self.existing
|
|
370
|
+
return peer.api if peer else f"{self.callee.name}{gen.AGENT_SUFFIX}"
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def url_env(self) -> str:
|
|
374
|
+
entry = self.entry
|
|
375
|
+
if entry and entry.get("base_url_env"):
|
|
376
|
+
return str(entry["base_url_env"])
|
|
377
|
+
return f"{self.callee.name.upper()}_AGENT_URL"
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def token_env(self) -> str | None:
|
|
381
|
+
if self.auth != "bearer":
|
|
382
|
+
return None
|
|
383
|
+
entry = self.entry
|
|
384
|
+
if entry and entry.get("auth") == "bearer" and entry.get("token_env"):
|
|
385
|
+
return str(entry["token_env"])
|
|
386
|
+
return f"{self.callee.name.upper()}_AGENT_KEY"
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def audience(self) -> str:
|
|
390
|
+
"""What the callee's AUTH_JWT_AUDIENCE must hold: the callee's name."""
|
|
391
|
+
return self.callee.name
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def actor(self) -> str:
|
|
395
|
+
"""The actor id the callee sees for the caller (what AUTH_ALLOWED_ACTORS lists).
|
|
396
|
+
|
|
397
|
+
The act.sub of the caller's exchanged tokens (the file's ``actor_id``, else its
|
|
398
|
+
client id); ``client:<client id>`` for tokens that name no actor.
|
|
399
|
+
"""
|
|
400
|
+
if self.edge.allow_actorless:
|
|
401
|
+
return f"{CLIENT_ACTOR_PREFIX}{self.caller.client_id}"
|
|
402
|
+
return self.caller.actor_id or self.caller.client_id
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def carries_user(self) -> bool:
|
|
406
|
+
"""Whether the call carries the user's identity (exchange, forward)."""
|
|
407
|
+
return self.auth in ("exchange", "forward")
|
|
408
|
+
|
|
409
|
+
def describe(self) -> str:
|
|
410
|
+
return f"{self.caller.name} -> {self.callee.name}"
|
|
411
|
+
|
|
412
|
+
def options(self, *, local_url: str | None = None) -> PeerOptions:
|
|
413
|
+
"""What `peer add` is given for this edge."""
|
|
414
|
+
description = self.edge.description or self.callee.setting(None, "A2A_DESCRIPTION")
|
|
415
|
+
options = PeerOptions(
|
|
416
|
+
name=self.peer,
|
|
417
|
+
api_name=self.api_name,
|
|
418
|
+
url_env=self.url_env,
|
|
419
|
+
token_env=self.token_env,
|
|
420
|
+
path=self.callee.mount(),
|
|
421
|
+
auth=self.auth,
|
|
422
|
+
audience=self.audience if self.auth in ("exchange", "forward") else None,
|
|
423
|
+
scope=self.edge.scope,
|
|
424
|
+
allow_actorless=self.edge.allow_actorless,
|
|
425
|
+
description=description or None,
|
|
426
|
+
approvals=self.edge.approvals,
|
|
427
|
+
)
|
|
428
|
+
if self.edge.calls is not None:
|
|
429
|
+
options.calls = tuple(self.edge.calls)
|
|
430
|
+
if local_url:
|
|
431
|
+
options.local_url = local_url
|
|
432
|
+
return options
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@dataclass
|
|
436
|
+
class System:
|
|
437
|
+
"""A system file resolved against its projects."""
|
|
438
|
+
|
|
439
|
+
path: Path
|
|
440
|
+
file: SystemFile
|
|
441
|
+
nodes: dict[str, Node]
|
|
442
|
+
links: list[Link]
|
|
443
|
+
envs: dict[str, Env]
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def root(self) -> Path:
|
|
447
|
+
return self.path.parent
|
|
448
|
+
|
|
449
|
+
def calls(self, node: Node) -> list[Link]:
|
|
450
|
+
return [link for link in self.links if link.caller is node]
|
|
451
|
+
|
|
452
|
+
def callers(self, node: Node) -> list[Link]:
|
|
453
|
+
return [link for link in self.links if link.callee is node]
|
|
454
|
+
|
|
455
|
+
def url(self, node: Node, env: str) -> str:
|
|
456
|
+
"""The base URL ``node`` is called at in ``env``."""
|
|
457
|
+
spec = self.envs[env]
|
|
458
|
+
if spec.kind == LOCAL:
|
|
459
|
+
return f"http://127.0.0.1:{(spec.port_base or 0) + node.index}"
|
|
460
|
+
if spec.kind == URL:
|
|
461
|
+
return (
|
|
462
|
+
(spec.url or "")
|
|
463
|
+
.replace("{agent}", node.name)
|
|
464
|
+
.replace("{project}", node.config.project_name)
|
|
465
|
+
.replace("{env}", env)
|
|
466
|
+
.rstrip("/")
|
|
467
|
+
)
|
|
468
|
+
port = node.service_port(env)
|
|
469
|
+
suffix = "" if port == DEFAULT_SERVICE_PORT else f":{port}"
|
|
470
|
+
return f"http://{node.release}.{node.namespace(env)}.svc.cluster.local{suffix}"
|
|
471
|
+
|
|
472
|
+
def token_url(self, env: str) -> str | None:
|
|
473
|
+
identity = self.file.identity
|
|
474
|
+
if identity is None or identity.token_url is None:
|
|
475
|
+
return None
|
|
476
|
+
if isinstance(identity.token_url, str):
|
|
477
|
+
return identity.token_url
|
|
478
|
+
return identity.token_url.get(env)
|
|
479
|
+
|
|
480
|
+
def select(self, names: list[str] | tuple[str, ...] | None) -> list[str]:
|
|
481
|
+
"""The environments to look at: ``names`` (each must be in the file), else all."""
|
|
482
|
+
if not names:
|
|
483
|
+
return list(self.envs)
|
|
484
|
+
unknown = [n for n in names if n not in self.envs]
|
|
485
|
+
if unknown:
|
|
486
|
+
known = ", ".join(self.envs) or "none"
|
|
487
|
+
raise click.UsageError(
|
|
488
|
+
f"environment {', '.join(unknown)} is not in {self.path.name} (environments: "
|
|
489
|
+
f"{known})"
|
|
490
|
+
)
|
|
491
|
+
return list(dict.fromkeys(names))
|
|
492
|
+
|
|
493
|
+
def local_url(self, node: Node) -> str | None:
|
|
494
|
+
"""The URL of ``node`` in the first local environment (for .env.example)."""
|
|
495
|
+
for env in self.envs.values():
|
|
496
|
+
if env.kind == LOCAL:
|
|
497
|
+
return self.url(node, env.name)
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
def relay_gates(
|
|
501
|
+
self, link: Link, documents: dict[str, dict[str, Any] | None] | None = None
|
|
502
|
+
) -> list[Gate]:
|
|
503
|
+
"""The callee's requester gates that ``link``'s caller cannot relay to (SC07);
|
|
504
|
+
``documents``: each agent's policy as a plan will leave it."""
|
|
505
|
+
if link.edge.approvals != RELAY:
|
|
506
|
+
return []
|
|
507
|
+
document = (documents or {}).get(link.callee.name)
|
|
508
|
+
gates = link.callee.requester_gates(document)
|
|
509
|
+
return [gate for gate in gates if not gate.relays(link.actor)]
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _true(value: Any) -> bool:
|
|
513
|
+
return value is True or (
|
|
514
|
+
isinstance(value, str) and value.strip().lower() in ("true", "yes", "1", "on")
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _int(value: Any, default: int) -> int:
|
|
519
|
+
if isinstance(value, bool):
|
|
520
|
+
return default
|
|
521
|
+
if isinstance(value, int):
|
|
522
|
+
return value
|
|
523
|
+
if isinstance(value, str) and value.strip().isdigit():
|
|
524
|
+
return int(value.strip())
|
|
525
|
+
return default
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def resolve(path: Path) -> System:
|
|
529
|
+
"""Read ``path`` and every project it names; ``SystemFileError`` when it cannot be used."""
|
|
530
|
+
data = load(path)
|
|
531
|
+
problems: list[str] = []
|
|
532
|
+
nodes: dict[str, Node] = {}
|
|
533
|
+
roots: dict[Path, str] = {}
|
|
534
|
+
for index, (name, agent) in enumerate(data.agents.items()):
|
|
535
|
+
root = (path.parent / agent.project).resolve()
|
|
536
|
+
if root in roots:
|
|
537
|
+
problems.append(
|
|
538
|
+
f"agents.{name}.project: {agent.project} is also agents.{roots[root]}'s project "
|
|
539
|
+
"(one project is one agent: its release, namespace and card)"
|
|
540
|
+
)
|
|
541
|
+
continue
|
|
542
|
+
roots[root] = name
|
|
543
|
+
if not (root / MANIFEST_FILENAME).is_file():
|
|
544
|
+
problems.append(
|
|
545
|
+
f"agents.{name}.project: {agent.project} is not a graph-agents-cli project (no "
|
|
546
|
+
f"{MANIFEST_FILENAME} in {root})"
|
|
547
|
+
)
|
|
548
|
+
continue
|
|
549
|
+
try:
|
|
550
|
+
project = load_project_at(root)
|
|
551
|
+
except click.ClickException as exc: # an invalid manifest or policy, a legacy file
|
|
552
|
+
problems.append(f"agents.{name} ({agent.project}): {exc.format_message()}")
|
|
553
|
+
continue
|
|
554
|
+
client_id = agent.client_id or name
|
|
555
|
+
nodes[name] = Node(
|
|
556
|
+
name, index, root, project, client_id, actor_id=agent.actor_id or client_id
|
|
557
|
+
)
|
|
558
|
+
clients: dict[str, str] = {}
|
|
559
|
+
actors: dict[str, str] = {}
|
|
560
|
+
for node in nodes.values():
|
|
561
|
+
if node.client_id in clients:
|
|
562
|
+
problems.append(
|
|
563
|
+
f"agents.{node.name}.client_id: {node.client_id} is also agents."
|
|
564
|
+
f"{clients[node.client_id]}'s: each agent needs its own client id"
|
|
565
|
+
)
|
|
566
|
+
clients.setdefault(node.client_id, node.name)
|
|
567
|
+
if node.actor_id in actors and actors[node.actor_id] != node.name:
|
|
568
|
+
problems.append(
|
|
569
|
+
f"agents.{node.name}.actor_id: {node.actor_id} is also agents."
|
|
570
|
+
f"{actors[node.actor_id]}'s actor id: the agents they call could not tell them "
|
|
571
|
+
"apart"
|
|
572
|
+
)
|
|
573
|
+
actors.setdefault(node.actor_id, node.name)
|
|
574
|
+
links: list[Link] = []
|
|
575
|
+
for name in data.agents:
|
|
576
|
+
seen: set[str] = set()
|
|
577
|
+
for position, edge in enumerate(data.edges(name)):
|
|
578
|
+
where = f"agents.{name}.calls[{position}]"
|
|
579
|
+
if edge.agent == name:
|
|
580
|
+
problems.append(f"{where}: an agent cannot call itself ({name})")
|
|
581
|
+
elif edge.agent not in data.agents:
|
|
582
|
+
problems.append(f"{where}: {edge.agent} is not an agent of this file")
|
|
583
|
+
elif edge.agent in seen:
|
|
584
|
+
problems.append(f"{where}: {name} already calls {edge.agent} (one edge per pair)")
|
|
585
|
+
elif name in nodes and edge.agent in nodes:
|
|
586
|
+
links.append(Link(nodes[name], nodes[edge.agent], edge))
|
|
587
|
+
seen.add(edge.agent)
|
|
588
|
+
if data.identity is None:
|
|
589
|
+
exchanges = [link.describe() for link in links if link.auth == "exchange"]
|
|
590
|
+
if exchanges:
|
|
591
|
+
problems.append(
|
|
592
|
+
f"identity: required, since these edges use auth: exchange: {', '.join(exchanges)}"
|
|
593
|
+
)
|
|
594
|
+
envs: dict[str, Env] = {}
|
|
595
|
+
for env_name in data.environments:
|
|
596
|
+
spec = data.environment(env_name)
|
|
597
|
+
kind = LOCAL if spec.port_base is not None else URL if spec.url is not None else CLUSTER
|
|
598
|
+
envs[env_name] = Env(env_name, kind, spec.port_base, spec.url)
|
|
599
|
+
if kind == LOCAL:
|
|
600
|
+
continue
|
|
601
|
+
for node in nodes.values():
|
|
602
|
+
try:
|
|
603
|
+
node.config.environment(env_name)
|
|
604
|
+
except click.UsageError:
|
|
605
|
+
problems.append(
|
|
606
|
+
f"environments.{env_name}: {node.name}'s manifest does not know this "
|
|
607
|
+
f"environment (add it under environments: in {node.root / MANIFEST_FILENAME})"
|
|
608
|
+
)
|
|
609
|
+
for label, keys in (
|
|
610
|
+
(
|
|
611
|
+
"identity.token_url",
|
|
612
|
+
list(data.identity.token_url)
|
|
613
|
+
if data.identity and isinstance(data.identity.token_url, dict)
|
|
614
|
+
else [],
|
|
615
|
+
),
|
|
616
|
+
("database.max_connections", list(data.database.max_connections) if data.database else []),
|
|
617
|
+
):
|
|
618
|
+
for key in keys:
|
|
619
|
+
if key not in envs:
|
|
620
|
+
problems.append(f"{label}.{key}: not an environment of this file")
|
|
621
|
+
for env in envs.values():
|
|
622
|
+
if env.kind == URL:
|
|
623
|
+
leftover = (env.url or "").replace("{agent}", "").replace("{project}", "")
|
|
624
|
+
leftover = leftover.replace("{env}", "")
|
|
625
|
+
if "{" in leftover or "}" in leftover:
|
|
626
|
+
problems.append(
|
|
627
|
+
f"environments.{env.name}.url: only {{agent}}, {{project}} and {{env}} may be "
|
|
628
|
+
"filled in"
|
|
629
|
+
)
|
|
630
|
+
if problems:
|
|
631
|
+
lines = "\n".join(f" - {problem}" for problem in problems)
|
|
632
|
+
raise SystemFileError(f"{path} cannot be used:\n{lines}")
|
|
633
|
+
return System(path, data, nodes, links, envs)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def locate(file: str | None) -> Path:
|
|
637
|
+
"""The system file: ``--file``, else ``graph-agents-system.yaml`` here or above."""
|
|
638
|
+
from graph_agents_cli.system._model import SYSTEM_FILENAME, find
|
|
639
|
+
|
|
640
|
+
if file:
|
|
641
|
+
path = Path(file)
|
|
642
|
+
if not path.is_file():
|
|
643
|
+
raise SystemFileError(f"{file}: no such file")
|
|
644
|
+
return path.resolve()
|
|
645
|
+
found = find()
|
|
646
|
+
if found is None:
|
|
647
|
+
raise SystemFileError(f"no {SYSTEM_FILENAME} in this directory or its parents; pass --file")
|
|
648
|
+
return found
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
__all__ = [
|
|
652
|
+
"CLUSTER",
|
|
653
|
+
"DENY",
|
|
654
|
+
"LOCAL",
|
|
655
|
+
"RELAY",
|
|
656
|
+
"URL",
|
|
657
|
+
"Env",
|
|
658
|
+
"Gate",
|
|
659
|
+
"Link",
|
|
660
|
+
"Node",
|
|
661
|
+
"System",
|
|
662
|
+
"locate",
|
|
663
|
+
"resolve",
|
|
664
|
+
]
|