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,1023 @@
|
|
|
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
|
+
"""``system check``: what would keep the agents of a system from calling each other.
|
|
16
|
+
|
|
17
|
+
Every check reads the projects' files (their manifests, policies, generated peers
|
|
18
|
+
modules and chart values); ``--live`` adds what only the cluster knows (SC14,
|
|
19
|
+
SC15). Per environment, the values checks read each chart's values merged as
|
|
20
|
+
``deploy --env`` merges them; a local environment (``port_base``) has no chart
|
|
21
|
+
values, so its settings live in ``.env``, which is never read here.
|
|
22
|
+
|
|
23
|
+
| Id | Check | Severity |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| SC01 | the projects exist (else exit 3), run a 0.3 runtime, have a chart and values file per charted environment | error |
|
|
26
|
+
| SC02 | every edge is a peer in the caller, as the file says; no peer of an agent the file no longer lets it call; tools/a2a_peers.py in step | error |
|
|
27
|
+
| SC03 | the caller's a2a.path is the callee's A2A mount | error |
|
|
28
|
+
| SC04 | auth compatibility (issuer, audience, the callee's policy) | error / warning |
|
|
29
|
+
| SC05 | per environment, the callee's appUrl is the URL the caller dials | error |
|
|
30
|
+
| SC06 | a callee with several replicas keeps A2A tasks in memory | error |
|
|
31
|
+
| SC07 | a relay edge's callee gates are decided directly | warning |
|
|
32
|
+
| SC08 | the callee's AUTH_ALLOWED_ACTORS lacks the caller | error |
|
|
33
|
+
| SC09 | cycles; a delegation chain longer than AUTH_MAX_DELEGATION_DEPTH | warning / error |
|
|
34
|
+
| SC10 | a shared database's connection budget | error |
|
|
35
|
+
| SC11 | the caller's secrets.keys holds the edges' secrets; PRINCIPAL_HASH_SALT | error / warning |
|
|
36
|
+
| SC12 | exchange or forward in a langgraph-server caller | error |
|
|
37
|
+
| SC13 | a callee's route still publishes its A2A path | warning |
|
|
38
|
+
| SC14 | (--live) the Services have a ready endpoint (EndpointSlices); the URLs and the token URL answer | error |
|
|
39
|
+
| SC15 | (--live) the Secrets hold the edges' keys (names only) | error / warning |
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import socket
|
|
46
|
+
import urllib.parse
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
from dataclasses import asdict, dataclass
|
|
49
|
+
from typing import Any
|
|
50
|
+
|
|
51
|
+
import click
|
|
52
|
+
|
|
53
|
+
from graph_agents_cli._api_policy import (
|
|
54
|
+
TOKEN_EXCHANGE_SECRET_ENV,
|
|
55
|
+
forward_runtime_problem,
|
|
56
|
+
summarize,
|
|
57
|
+
)
|
|
58
|
+
from graph_agents_cli.deploy import _image, _kube
|
|
59
|
+
from graph_agents_cli.deploy._kube import Target
|
|
60
|
+
from graph_agents_cli.peer import _generate as gen
|
|
61
|
+
from graph_agents_cli.system._system import (
|
|
62
|
+
CLUSTER,
|
|
63
|
+
DEFAULT_DB_POOL_MAX_SIZE,
|
|
64
|
+
DEFAULT_LANGGRAPH_POOL_MAX_SIZE,
|
|
65
|
+
LOCAL,
|
|
66
|
+
Link,
|
|
67
|
+
Node,
|
|
68
|
+
System,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ERROR = "error"
|
|
72
|
+
WARNING = "warning"
|
|
73
|
+
SALT = "PRINCIPAL_HASH_SALT"
|
|
74
|
+
DB_HEADROOM = 0.10 # SC10 keeps 10% of max_connections for everything else
|
|
75
|
+
# How many paths SC09 explores before giving up (a dense graph of many agents).
|
|
76
|
+
MAX_PATH_STEPS = 200_000
|
|
77
|
+
CARD_TIMEOUT_S = 5.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Finding:
|
|
82
|
+
"""One problem ``system check`` found."""
|
|
83
|
+
|
|
84
|
+
check: str
|
|
85
|
+
severity: str
|
|
86
|
+
message: str
|
|
87
|
+
agent: str | None = None
|
|
88
|
+
env: str | None = None
|
|
89
|
+
fix: str | None = None
|
|
90
|
+
|
|
91
|
+
def as_dict(self) -> dict[str, Any]:
|
|
92
|
+
return {k: v for k, v in asdict(self).items() if v is not None}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _where(system: System, node: Node) -> str:
|
|
96
|
+
try:
|
|
97
|
+
return node.root.relative_to(system.root).as_posix() or "."
|
|
98
|
+
except ValueError:
|
|
99
|
+
return str(node.root)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Static checks (the projects' files)
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _sc01(system: System, envs: list[str]) -> list[Finding]:
|
|
108
|
+
found = []
|
|
109
|
+
for node in system.nodes.values():
|
|
110
|
+
problem = node.runtime_problem
|
|
111
|
+
if problem:
|
|
112
|
+
found.append(
|
|
113
|
+
Finding(
|
|
114
|
+
"SC01",
|
|
115
|
+
ERROR,
|
|
116
|
+
f"{node.name}: {problem}",
|
|
117
|
+
agent=node.name,
|
|
118
|
+
fix=f"cd {_where(system, node)} && graph-agents-cli scaffold upgrade",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
for env in envs:
|
|
122
|
+
if not system.envs[env].charted:
|
|
123
|
+
continue
|
|
124
|
+
if not node.has_chart:
|
|
125
|
+
found.append(
|
|
126
|
+
Finding(
|
|
127
|
+
"SC01",
|
|
128
|
+
ERROR,
|
|
129
|
+
f"{node.name} has no Helm chart (deployment_target "
|
|
130
|
+
f"{node.config.deployment_target}): it cannot run in {env}",
|
|
131
|
+
agent=node.name,
|
|
132
|
+
env=env,
|
|
133
|
+
fix="graph-agents-cli scaffold enhance --deployment-target kubernetes",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
elif node.values_file(env) is None:
|
|
137
|
+
found.append(
|
|
138
|
+
Finding(
|
|
139
|
+
"SC01",
|
|
140
|
+
ERROR,
|
|
141
|
+
f"{node.name}'s chart has no values-{env}.yaml",
|
|
142
|
+
agent=node.name,
|
|
143
|
+
env=env,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return found
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _differs(have: dict[str, Any], want: dict[str, Any]) -> list[str]:
|
|
150
|
+
return sorted(k for k in set(have) | set(want) if have.get(k) != want.get(k))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _sc02(system: System, expected: dict[str, dict[str, Any] | None]) -> list[Finding]:
|
|
154
|
+
"""``expected``: each caller's policy as ``system apply`` would leave it (None: no plan,
|
|
155
|
+
the reason is reported by the caller)."""
|
|
156
|
+
found = []
|
|
157
|
+
for node in system.nodes.values():
|
|
158
|
+
links = system.calls(node)
|
|
159
|
+
fix = "graph-agents-cli system apply"
|
|
160
|
+
want_doc = expected.get(node.name)
|
|
161
|
+
for link in links:
|
|
162
|
+
peer = link.existing
|
|
163
|
+
if peer is None:
|
|
164
|
+
found.append(
|
|
165
|
+
Finding(
|
|
166
|
+
"SC02",
|
|
167
|
+
ERROR,
|
|
168
|
+
f"{link.describe()}: {node.name} has no protocol: a2a API for "
|
|
169
|
+
f"{link.callee.name} (no peer {link.callee.name} in its api-policy.yaml)",
|
|
170
|
+
agent=node.name,
|
|
171
|
+
fix=fix,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
want = ((want_doc or {}).get("apis") or {}).get(peer.api)
|
|
176
|
+
have = link.entry or {}
|
|
177
|
+
if want_doc is not None and isinstance(want, dict) and have != want:
|
|
178
|
+
keys = ", ".join(_differs(have, want))
|
|
179
|
+
found.append(
|
|
180
|
+
Finding(
|
|
181
|
+
"SC02",
|
|
182
|
+
ERROR,
|
|
183
|
+
f"{link.describe()}: {node.name}'s {peer.api} differs from the file "
|
|
184
|
+
f"({keys})",
|
|
185
|
+
agent=node.name,
|
|
186
|
+
fix=fix,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
wanted = {link.callee.name for link in links}
|
|
190
|
+
for name, peer in node.peers.items():
|
|
191
|
+
if name in system.nodes and name not in wanted:
|
|
192
|
+
found.append(
|
|
193
|
+
Finding(
|
|
194
|
+
"SC02",
|
|
195
|
+
ERROR,
|
|
196
|
+
f"{node.name} still has the peer {name} ({peer.api}), which the file no "
|
|
197
|
+
"longer lets it call",
|
|
198
|
+
agent=node.name,
|
|
199
|
+
fix=fix,
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
if node.document is not None or node.module_text is not None:
|
|
203
|
+
peers = gen.peers_of(node.document, node.module_names)
|
|
204
|
+
text = gen.module_text(peers, node.config.agent_directory)
|
|
205
|
+
if text != node.module_text:
|
|
206
|
+
rel = f"{node.config.agent_directory}/tools/{gen.MODULE_NAME}"
|
|
207
|
+
found.append(
|
|
208
|
+
Finding(
|
|
209
|
+
"SC02",
|
|
210
|
+
ERROR,
|
|
211
|
+
f"{node.name}: {rel} is out of step with its api-policy.yaml",
|
|
212
|
+
agent=node.name,
|
|
213
|
+
fix=f"cd {_where(system, node)} && graph-agents-cli peer sync (or {fix})",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
return found
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _sc03(system: System, envs: list[str]) -> list[Finding]:
|
|
220
|
+
found = []
|
|
221
|
+
for link in system.links:
|
|
222
|
+
entry = link.entry
|
|
223
|
+
if entry is None:
|
|
224
|
+
continue # SC02
|
|
225
|
+
path = str((entry.get("a2a") or {}).get("path") or "")
|
|
226
|
+
places: list[str | None] = [None] + [e for e in envs if system.envs[e].charted]
|
|
227
|
+
for env in places:
|
|
228
|
+
mount = link.callee.mount(env)
|
|
229
|
+
if path != mount:
|
|
230
|
+
where = f" in {env}" if env else ""
|
|
231
|
+
found.append(
|
|
232
|
+
Finding(
|
|
233
|
+
"SC03",
|
|
234
|
+
ERROR,
|
|
235
|
+
f"{link.describe()}: {link.caller.name} calls {path}, but "
|
|
236
|
+
f"{link.callee.name} serves A2A at {mount}{where} (A2A_NAME "
|
|
237
|
+
f"{link.callee.a2a_name(env)})",
|
|
238
|
+
agent=link.caller.name,
|
|
239
|
+
env=env,
|
|
240
|
+
fix="graph-agents-cli system apply",
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
break
|
|
244
|
+
return found
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _csv(value: str) -> list[str]:
|
|
248
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _sc04(system: System, envs: list[str]) -> list[Finding]:
|
|
252
|
+
found = []
|
|
253
|
+
issuer = system.file.identity.issuer if system.file.identity else None
|
|
254
|
+
charted = [e for e in envs if system.envs[e].charted]
|
|
255
|
+
for link in system.links:
|
|
256
|
+
caller, callee = link.caller, link.callee
|
|
257
|
+
if caller.config.auth_policy == "shared-bearer" and link.carries_user:
|
|
258
|
+
found.append(
|
|
259
|
+
Finding(
|
|
260
|
+
"SC04",
|
|
261
|
+
ERROR,
|
|
262
|
+
f"{link.describe()}: auth {link.auth} needs a user token, and {caller.name} "
|
|
263
|
+
"uses shared-bearer (every caller is one principal)",
|
|
264
|
+
agent=caller.name,
|
|
265
|
+
fix=f"give the edge auth: bearer (with {callee.name}'s agent key)",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
# The callee's policy: the manifest's, and each environment's where its chart sets
|
|
269
|
+
# another (env.AUTH_POLICY).
|
|
270
|
+
base = callee.auth_policy()
|
|
271
|
+
finding = _policy_finding(link, base, None)
|
|
272
|
+
if finding is not None:
|
|
273
|
+
found.append(finding)
|
|
274
|
+
for env in charted:
|
|
275
|
+
if not callee.has_chart:
|
|
276
|
+
break
|
|
277
|
+
policy = callee.auth_policy(env)
|
|
278
|
+
if policy != base:
|
|
279
|
+
finding = _policy_finding(link, policy, env)
|
|
280
|
+
if finding is not None:
|
|
281
|
+
found.append(finding)
|
|
282
|
+
if policy == "jwt" and link.carries_user:
|
|
283
|
+
found.extend(_jwt_findings(link, issuer, env))
|
|
284
|
+
return found
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _policy_finding(link: Link, policy: str, env: str | None) -> Finding | None:
|
|
288
|
+
"""Whether ``link``'s auth mode can work with the callee's auth policy at all."""
|
|
289
|
+
callee, what = link.callee, link.describe()
|
|
290
|
+
where = f" in {env}" if env else ""
|
|
291
|
+
if link.auth == "bearer":
|
|
292
|
+
if policy == "jwt":
|
|
293
|
+
return Finding(
|
|
294
|
+
"SC04",
|
|
295
|
+
ERROR,
|
|
296
|
+
f"{what}: auth bearer sends a static key, and {callee.name} verifies JWTs "
|
|
297
|
+
f"(jwt{where})",
|
|
298
|
+
agent=callee.name,
|
|
299
|
+
env=env,
|
|
300
|
+
fix="give the edge auth: exchange",
|
|
301
|
+
)
|
|
302
|
+
if policy == "shared-bearer" and "API_KEY" not in callee.config.secret_keys:
|
|
303
|
+
return Finding(
|
|
304
|
+
"SC04",
|
|
305
|
+
ERROR,
|
|
306
|
+
f"{what}: {callee.name}'s secrets.keys has no API_KEY, the key "
|
|
307
|
+
f"{link.caller.name} sends",
|
|
308
|
+
agent=callee.name,
|
|
309
|
+
fix="add API_KEY to secrets.keys in its manifest",
|
|
310
|
+
)
|
|
311
|
+
if policy == "custom":
|
|
312
|
+
return Finding(
|
|
313
|
+
"SC04",
|
|
314
|
+
WARNING,
|
|
315
|
+
f"{what}: {callee.name}'s custom policy{where} must accept {link.caller.name}'s "
|
|
316
|
+
"key",
|
|
317
|
+
agent=callee.name,
|
|
318
|
+
env=env,
|
|
319
|
+
)
|
|
320
|
+
return None
|
|
321
|
+
if policy == "shared-bearer":
|
|
322
|
+
return Finding(
|
|
323
|
+
"SC04",
|
|
324
|
+
ERROR,
|
|
325
|
+
f"{what}: auth {link.auth} carries the user's token, and {callee.name} uses "
|
|
326
|
+
f"shared-bearer{where}: it cannot verify it",
|
|
327
|
+
agent=callee.name,
|
|
328
|
+
env=env,
|
|
329
|
+
fix=f"give the edge auth: bearer, or give {callee.name} the jwt auth policy",
|
|
330
|
+
)
|
|
331
|
+
if policy == "custom":
|
|
332
|
+
return Finding(
|
|
333
|
+
"SC04",
|
|
334
|
+
WARNING,
|
|
335
|
+
f"{what}: {callee.name}'s custom policy{where} must accept the token "
|
|
336
|
+
f"{link.caller.name} sends (audience {link.audience}) and set its actor",
|
|
337
|
+
agent=callee.name,
|
|
338
|
+
env=env,
|
|
339
|
+
)
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _jwt_findings(link: Link, issuer: str | None, env: str) -> list[Finding]:
|
|
344
|
+
"""A jwt callee's issuer and audience in ``env``, against the tokens the caller sends."""
|
|
345
|
+
callee, what = link.callee, link.describe()
|
|
346
|
+
found = []
|
|
347
|
+
if link.auth == "exchange" and issuer is not None:
|
|
348
|
+
have = callee.setting(env, "AUTH_JWT_ISSUER")
|
|
349
|
+
if have != issuer:
|
|
350
|
+
found.append(
|
|
351
|
+
Finding(
|
|
352
|
+
"SC04",
|
|
353
|
+
ERROR,
|
|
354
|
+
f"{what}: {callee.name}'s AUTH_JWT_ISSUER in {env} is {have or '(empty)'}, "
|
|
355
|
+
f"not the file's issuer {issuer}",
|
|
356
|
+
agent=callee.name,
|
|
357
|
+
env=env,
|
|
358
|
+
fix=f"set env.AUTH_JWT_ISSUER: {issuer} in its values-{env}.yaml",
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
audiences = _csv(callee.setting(env, "AUTH_JWT_AUDIENCE"))
|
|
362
|
+
if link.audience not in audiences:
|
|
363
|
+
found.append(
|
|
364
|
+
Finding(
|
|
365
|
+
"SC04",
|
|
366
|
+
ERROR,
|
|
367
|
+
f"{what}: {callee.name}'s AUTH_JWT_AUDIENCE in {env} "
|
|
368
|
+
f"({','.join(audiences) or 'empty'}) does not include {link.audience}, the "
|
|
369
|
+
f"audience {link.caller.name}'s tokens are minted for",
|
|
370
|
+
agent=callee.name,
|
|
371
|
+
env=env,
|
|
372
|
+
fix=(
|
|
373
|
+
"graph-agents-cli system apply (sets it when empty), or add "
|
|
374
|
+
f"{link.audience} to it"
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
return found
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _placeholder(value: str) -> bool:
|
|
382
|
+
return not value or _image.has_placeholder(value)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _sc05(system: System, envs: list[str]) -> list[Finding]:
|
|
386
|
+
found = []
|
|
387
|
+
for env in envs:
|
|
388
|
+
if not system.envs[env].charted:
|
|
389
|
+
continue
|
|
390
|
+
for link in system.links:
|
|
391
|
+
if not (link.caller.has_chart and link.callee.has_chart):
|
|
392
|
+
continue
|
|
393
|
+
dialled = link.caller.setting(env, link.url_env)
|
|
394
|
+
served = link.callee.app_url(env)
|
|
395
|
+
if _placeholder(dialled):
|
|
396
|
+
message = (
|
|
397
|
+
f"{link.describe()}: {link.caller.name}'s {link.url_env} in {env} is "
|
|
398
|
+
f"{dialled or 'unset'}"
|
|
399
|
+
)
|
|
400
|
+
elif not served:
|
|
401
|
+
message = (
|
|
402
|
+
f"{link.describe()}: {link.callee.name} has no appUrl in {env}: its card "
|
|
403
|
+
f"names its bind address, not {dialled}"
|
|
404
|
+
)
|
|
405
|
+
elif dialled.rstrip("/") != served.rstrip("/"):
|
|
406
|
+
message = (
|
|
407
|
+
f"{link.describe()}: {link.caller.name} dials {dialled} in {env}, but "
|
|
408
|
+
f"{link.callee.name}'s card names {served} (appUrl)"
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
continue
|
|
412
|
+
found.append(
|
|
413
|
+
Finding(
|
|
414
|
+
"SC05",
|
|
415
|
+
ERROR,
|
|
416
|
+
message,
|
|
417
|
+
agent=link.caller.name,
|
|
418
|
+
env=env,
|
|
419
|
+
fix="graph-agents-cli system apply",
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
return found
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _sc06(system: System, envs: list[str]) -> list[Finding]:
|
|
426
|
+
found = []
|
|
427
|
+
for env in envs:
|
|
428
|
+
if not system.envs[env].charted:
|
|
429
|
+
continue
|
|
430
|
+
for node in system.nodes.values():
|
|
431
|
+
if not system.callers(node) or not node.has_chart:
|
|
432
|
+
continue
|
|
433
|
+
replicas, autoscaled = node.replicas(env)
|
|
434
|
+
if node.task_store(env) == "memory" and (replicas > 1 or autoscaled):
|
|
435
|
+
many = "an HPA" if autoscaled else f"{replicas} replicas"
|
|
436
|
+
found.append(
|
|
437
|
+
Finding(
|
|
438
|
+
"SC06",
|
|
439
|
+
ERROR,
|
|
440
|
+
f"{node.name} runs {many} in {env} with A2A tasks in memory "
|
|
441
|
+
"(CHECKPOINTER=memory): a caller's GetTask or approval that reaches "
|
|
442
|
+
"another replica finds no task",
|
|
443
|
+
agent=node.name,
|
|
444
|
+
env=env,
|
|
445
|
+
fix="set CHECKPOINTER: postgres (the tasks then live in the database), "
|
|
446
|
+
"or one replica",
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
return found
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _sc07(system: System) -> list[Finding]:
|
|
453
|
+
found = []
|
|
454
|
+
for link in system.links:
|
|
455
|
+
for gate in system.relay_gates(link):
|
|
456
|
+
found.append(
|
|
457
|
+
Finding(
|
|
458
|
+
"SC07",
|
|
459
|
+
WARNING,
|
|
460
|
+
f"relays to {link.callee.name} need the person to approve at "
|
|
461
|
+
f"{link.callee.name} ({link.describe()}: its gate {gate.api}"
|
|
462
|
+
+ (f" rule {gate.rule}" if gate.rule is not None else "")
|
|
463
|
+
+ f" is decided {gate.decide_with})",
|
|
464
|
+
agent=link.callee.name,
|
|
465
|
+
fix=f"cd {_where(system, link.callee)} && {gate.command(link.actor)}",
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
return found
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _sc08(system: System, envs: list[str]) -> list[Finding]:
|
|
472
|
+
found = []
|
|
473
|
+
for env in envs:
|
|
474
|
+
if not system.envs[env].charted:
|
|
475
|
+
continue
|
|
476
|
+
for link in system.links:
|
|
477
|
+
if not link.carries_user or not link.callee.has_chart:
|
|
478
|
+
continue
|
|
479
|
+
actors = link.callee.allowed_actors(env)
|
|
480
|
+
if "*" in actors or link.actor in actors:
|
|
481
|
+
continue
|
|
482
|
+
found.append(
|
|
483
|
+
Finding(
|
|
484
|
+
"SC08",
|
|
485
|
+
ERROR,
|
|
486
|
+
f"{link.describe()}: {link.callee.name}'s AUTH_ALLOWED_ACTORS in {env} "
|
|
487
|
+
f"({','.join(actors) or 'empty'}) lacks {link.actor}: it refuses "
|
|
488
|
+
f"{link.caller.name}'s calls (403)",
|
|
489
|
+
agent=link.callee.name,
|
|
490
|
+
env=env,
|
|
491
|
+
fix="graph-agents-cli system apply",
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
return found
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def cycles(system: System) -> list[list[str]]:
|
|
498
|
+
"""The cycles of the call graph, each once (its agents in call order)."""
|
|
499
|
+
graph = {
|
|
500
|
+
name: [lk.callee.name for lk in system.links if lk.caller.name == name]
|
|
501
|
+
for name in system.nodes
|
|
502
|
+
}
|
|
503
|
+
found: list[list[str]] = []
|
|
504
|
+
seen: set[frozenset[str]] = set()
|
|
505
|
+
|
|
506
|
+
def walk(start: str, here: str, path: list[str]) -> None:
|
|
507
|
+
for nxt in graph[here]:
|
|
508
|
+
if nxt == start:
|
|
509
|
+
key = frozenset(path)
|
|
510
|
+
if key not in seen:
|
|
511
|
+
seen.add(key)
|
|
512
|
+
found.append([*path, start])
|
|
513
|
+
elif nxt not in path and list(system.nodes).index(nxt) > list(system.nodes).index(
|
|
514
|
+
start
|
|
515
|
+
):
|
|
516
|
+
walk(start, nxt, [*path, nxt])
|
|
517
|
+
|
|
518
|
+
for name in system.nodes:
|
|
519
|
+
walk(name, name, [name])
|
|
520
|
+
return found
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def chain_depths(system: System) -> dict[str, tuple[int, list[str]]]:
|
|
524
|
+
"""For each agent, the longest chain of exchange edges ending at it: the agents between
|
|
525
|
+
the user and it, which its AUTH_MAX_DELEGATION_DEPTH caps (forward and bearer edges add
|
|
526
|
+
no actor). ``(length, path)``."""
|
|
527
|
+
into: dict[str, list[str]] = {name: [] for name in system.nodes}
|
|
528
|
+
for link in system.links:
|
|
529
|
+
if link.auth == "exchange":
|
|
530
|
+
into[link.callee.name].append(link.caller.name)
|
|
531
|
+
best: dict[str, tuple[int, list[str]]] = {}
|
|
532
|
+
steps = 0
|
|
533
|
+
|
|
534
|
+
def longest(node: str, seen: frozenset[str]) -> tuple[int, list[str]]:
|
|
535
|
+
nonlocal steps
|
|
536
|
+
steps += 1
|
|
537
|
+
if steps > MAX_PATH_STEPS:
|
|
538
|
+
return 0, [node]
|
|
539
|
+
result = (0, [node])
|
|
540
|
+
for caller in into[node]:
|
|
541
|
+
if caller in seen:
|
|
542
|
+
continue
|
|
543
|
+
length, path = longest(caller, seen | {caller})
|
|
544
|
+
if length + 1 > result[0]:
|
|
545
|
+
result = (length + 1, [*path, node])
|
|
546
|
+
return result
|
|
547
|
+
|
|
548
|
+
for name in system.nodes:
|
|
549
|
+
best[name] = longest(name, frozenset({name}))
|
|
550
|
+
return best
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _sc09(system: System, envs: list[str]) -> list[Finding]:
|
|
554
|
+
found = []
|
|
555
|
+
for cycle in cycles(system):
|
|
556
|
+
found.append(
|
|
557
|
+
Finding(
|
|
558
|
+
"SC09",
|
|
559
|
+
WARNING,
|
|
560
|
+
f"a cycle: {' -> '.join(cycle)} (a request can loop back; the client refuses a "
|
|
561
|
+
"peer already in the delegation chain)",
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
places: list[str | None] = [None] + [e for e in envs if system.envs[e].charted]
|
|
565
|
+
for name, (length, path) in chain_depths(system).items():
|
|
566
|
+
node = system.nodes[name]
|
|
567
|
+
for env in places:
|
|
568
|
+
limit = node.max_delegation_depth(env)
|
|
569
|
+
if length > limit:
|
|
570
|
+
where = f" in {env}" if env else ""
|
|
571
|
+
found.append(
|
|
572
|
+
Finding(
|
|
573
|
+
"SC09",
|
|
574
|
+
ERROR,
|
|
575
|
+
f"{' -> '.join(path)}: {length} agents stand between the user and {name}, "
|
|
576
|
+
f"more than its AUTH_MAX_DELEGATION_DEPTH ({limit}){where}: it refuses "
|
|
577
|
+
"that chain (401)",
|
|
578
|
+
agent=name,
|
|
579
|
+
env=env,
|
|
580
|
+
fix=f"raise AUTH_MAX_DELEGATION_DEPTH on {name} (at most 8), or shorten "
|
|
581
|
+
"the chain",
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
break
|
|
585
|
+
return found
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _pool(node: Node, env: str) -> tuple[int, str]:
|
|
589
|
+
"""Connections one replica of ``node`` opens at most, and how that was counted."""
|
|
590
|
+
raw = node.setting(env, "DB_POOL_MAX_SIZE")
|
|
591
|
+
app = int(raw) if raw.isdigit() else DEFAULT_DB_POOL_MAX_SIZE
|
|
592
|
+
if node.config.runtime != "langgraph-server":
|
|
593
|
+
return app, f"DB_POOL_MAX_SIZE {app}"
|
|
594
|
+
raw = node.setting(env, "LANGGRAPH_POSTGRES_POOL_MAX_SIZE")
|
|
595
|
+
server = int(raw) if raw.isdigit() else DEFAULT_LANGGRAPH_POOL_MAX_SIZE
|
|
596
|
+
return app + server, f"DB_POOL_MAX_SIZE {app} + LANGGRAPH_POSTGRES_POOL_MAX_SIZE {server}"
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _sc10(system: System, envs: list[str]) -> list[Finding]:
|
|
600
|
+
database = system.file.database
|
|
601
|
+
if database is None:
|
|
602
|
+
return []
|
|
603
|
+
found = []
|
|
604
|
+
for env in envs:
|
|
605
|
+
limit = database.max_connections.get(env)
|
|
606
|
+
if limit is None:
|
|
607
|
+
continue
|
|
608
|
+
total, parts = 0, []
|
|
609
|
+
for node in system.nodes.values():
|
|
610
|
+
values = node.values(env)
|
|
611
|
+
bundled = isinstance(values.get("postgresql"), dict) and (
|
|
612
|
+
values["postgresql"].get("enabled") is True
|
|
613
|
+
)
|
|
614
|
+
if not node.has_chart or bundled or node.task_store(env) == "memory":
|
|
615
|
+
continue # its own database, or none
|
|
616
|
+
replicas, _autoscaled = node.replicas(env)
|
|
617
|
+
per, how = _pool(node, env)
|
|
618
|
+
total += replicas * per
|
|
619
|
+
parts.append(f"{node.name} {replicas} x {per} ({how})")
|
|
620
|
+
budget = int(limit * (1 - DB_HEADROOM))
|
|
621
|
+
if total > budget:
|
|
622
|
+
found.append(
|
|
623
|
+
Finding(
|
|
624
|
+
"SC10",
|
|
625
|
+
ERROR,
|
|
626
|
+
f"the agents may open {total} connections to the shared database in {env}, "
|
|
627
|
+
f"more than {budget} (max_connections {limit} less 10%): " + "; ".join(parts),
|
|
628
|
+
env=env,
|
|
629
|
+
fix="lower DB_POOL_MAX_SIZE or the replicas, raise max_connections, or put "
|
|
630
|
+
"PgBouncer in front (above about 120 connections)",
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
return found
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _sc11(system: System) -> list[Finding]:
|
|
637
|
+
found = []
|
|
638
|
+
for node in system.nodes.values():
|
|
639
|
+
links = system.calls(node)
|
|
640
|
+
if not links:
|
|
641
|
+
continue
|
|
642
|
+
keys = set(node.config.secret_keys)
|
|
643
|
+
needed = []
|
|
644
|
+
if any(link.auth == "exchange" for link in links):
|
|
645
|
+
needed.append(TOKEN_EXCHANGE_SECRET_ENV)
|
|
646
|
+
needed += [link.token_env for link in links if link.token_env]
|
|
647
|
+
for key in dict.fromkeys(needed):
|
|
648
|
+
if key and key not in keys:
|
|
649
|
+
found.append(
|
|
650
|
+
Finding(
|
|
651
|
+
"SC11",
|
|
652
|
+
ERROR,
|
|
653
|
+
f"{node.name}'s secrets.keys lacks {key}: its pods never get it",
|
|
654
|
+
agent=node.name,
|
|
655
|
+
fix="graph-agents-cli system apply (or add it to secrets.keys)",
|
|
656
|
+
)
|
|
657
|
+
)
|
|
658
|
+
if SALT not in keys:
|
|
659
|
+
found.append(
|
|
660
|
+
Finding(
|
|
661
|
+
"SC11",
|
|
662
|
+
WARNING,
|
|
663
|
+
f"{node.name}: {SALT} is not in secrets.keys, so the conversation ids it "
|
|
664
|
+
"sends are keyed without a secret (anyone who knows a thread id can compute "
|
|
665
|
+
"them)",
|
|
666
|
+
agent=node.name,
|
|
667
|
+
fix=f"add {SALT} to secrets.keys and set it in .env.<env>",
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
return found
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _sc12(system: System) -> list[Finding]:
|
|
674
|
+
found = []
|
|
675
|
+
for node in system.nodes.values():
|
|
676
|
+
links = [link for link in system.calls(node) if link.carries_user]
|
|
677
|
+
if not links or node.config.runtime != "langgraph-server":
|
|
678
|
+
continue
|
|
679
|
+
document = {
|
|
680
|
+
"apis": {
|
|
681
|
+
link.api_name: {"base_url_env": link.url_env, "auth": link.auth} for link in links
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
problem = forward_runtime_problem(summarize(document), node.config.runtime)
|
|
685
|
+
if problem:
|
|
686
|
+
found.append(Finding("SC12", ERROR, f"{node.name}: {problem}", agent=node.name))
|
|
687
|
+
return found
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _publishes(path: dict[str, Any], mount: str) -> bool:
|
|
691
|
+
value = str(path.get("path") or "")
|
|
692
|
+
if str(path.get("type") or "") == "Exact":
|
|
693
|
+
return value == mount
|
|
694
|
+
value = value.rstrip("/") or "/"
|
|
695
|
+
return value == "/" or mount == value or mount.startswith(value + "/")
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _sc13(system: System, envs: list[str]) -> list[Finding]:
|
|
699
|
+
"""Only where agents call each other inside the cluster: with a url environment they
|
|
700
|
+
call the public URL, so the route must publish the A2A path there."""
|
|
701
|
+
found = []
|
|
702
|
+
for env in envs:
|
|
703
|
+
if system.envs[env].kind != CLUSTER:
|
|
704
|
+
continue
|
|
705
|
+
for node in system.nodes.values():
|
|
706
|
+
if not system.callers(node) or not node.has_chart:
|
|
707
|
+
continue
|
|
708
|
+
mount = node.mount(env)
|
|
709
|
+
if any(_publishes(p, mount) for p in node.public_paths(env)):
|
|
710
|
+
found.append(
|
|
711
|
+
Finding(
|
|
712
|
+
"SC13",
|
|
713
|
+
WARNING,
|
|
714
|
+
f"{node.name}'s route still publishes {mount} in {env} although agents "
|
|
715
|
+
"call it (inside the cluster)",
|
|
716
|
+
agent=node.name,
|
|
717
|
+
env=env,
|
|
718
|
+
fix=f"drop {mount} from route.publicPaths in values-{env}.yaml if only "
|
|
719
|
+
"agents call it",
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
return found
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# Live checks (--live)
|
|
727
|
+
# ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@dataclass
|
|
731
|
+
class Probe:
|
|
732
|
+
"""What ``--live`` asks the cluster and the network (replaced in tests)."""
|
|
733
|
+
|
|
734
|
+
def ready_endpoints(self, target: Target, service: str) -> int:
|
|
735
|
+
"""Ready endpoints behind a Service (-1: no such Service), read from its
|
|
736
|
+
EndpointSlices (the v1 Endpoints API is deprecated since Kubernetes 1.33);
|
|
737
|
+
``ToolFailed`` when kubectl cannot say."""
|
|
738
|
+
found = _kube.run_cmd(
|
|
739
|
+
_kube.kubectl_args(["get", "service", service, "-o", "name"], target),
|
|
740
|
+
check=False,
|
|
741
|
+
quiet=True,
|
|
742
|
+
)
|
|
743
|
+
if found.returncode != 0:
|
|
744
|
+
if "(NotFound)" in (found.stderr or ""):
|
|
745
|
+
return -1
|
|
746
|
+
raise _kube.ToolFailed(_first_line(found))
|
|
747
|
+
label = f"kubernetes.io/service-name={service}"
|
|
748
|
+
result = _kube.run_cmd(
|
|
749
|
+
_kube.kubectl_args(["get", "endpointslices", "-l", label, "-o", "json"], target),
|
|
750
|
+
check=False,
|
|
751
|
+
quiet=True,
|
|
752
|
+
)
|
|
753
|
+
if result.returncode != 0:
|
|
754
|
+
raise _kube.ToolFailed(_first_line(result))
|
|
755
|
+
body = json.loads(result.stdout or "{}")
|
|
756
|
+
return sum(
|
|
757
|
+
1
|
|
758
|
+
for item in body.get("items") or []
|
|
759
|
+
for endpoint in item.get("endpoints") or []
|
|
760
|
+
# An unset `ready` means ready (the EndpointSlice API).
|
|
761
|
+
if (endpoint.get("conditions") or {}).get("ready") is not False
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
def secret_keys(self, target: Target, name: str) -> set[str] | None:
|
|
765
|
+
"""The key names of a Secret (never its values); None when it does not exist."""
|
|
766
|
+
result = _kube.run_cmd(
|
|
767
|
+
_kube.kubectl_args(["get", "secret", name, "-o", "json"], target),
|
|
768
|
+
check=False,
|
|
769
|
+
quiet=True,
|
|
770
|
+
)
|
|
771
|
+
if result.returncode != 0:
|
|
772
|
+
if "(NotFound)" in (result.stderr or ""):
|
|
773
|
+
return None
|
|
774
|
+
raise _kube.ToolFailed(_first_line(result))
|
|
775
|
+
body = json.loads(result.stdout or "{}")
|
|
776
|
+
return set(body.get("data") or {}) | set(body.get("stringData") or {})
|
|
777
|
+
|
|
778
|
+
def resolves(self, host: str) -> str | None:
|
|
779
|
+
"""None when ``host`` resolves, else why not."""
|
|
780
|
+
try:
|
|
781
|
+
socket.getaddrinfo(host, None)
|
|
782
|
+
except OSError as exc:
|
|
783
|
+
return str(exc)
|
|
784
|
+
return None
|
|
785
|
+
|
|
786
|
+
def card(self, url: str) -> tuple[bool, str]:
|
|
787
|
+
"""GET a card with no credential: 200 or 401 is reachable (KI-120)."""
|
|
788
|
+
import httpx
|
|
789
|
+
|
|
790
|
+
try:
|
|
791
|
+
response = httpx.get(
|
|
792
|
+
url, headers={"A2A-Version": "1.0"}, timeout=CARD_TIMEOUT_S, follow_redirects=False
|
|
793
|
+
)
|
|
794
|
+
except httpx.HTTPError as exc:
|
|
795
|
+
return False, f"unreachable ({type(exc).__name__})"
|
|
796
|
+
if response.status_code in (200, 401):
|
|
797
|
+
return True, f"HTTP {response.status_code}"
|
|
798
|
+
return False, f"HTTP {response.status_code}"
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _first_line(result: Any) -> str:
|
|
802
|
+
lines = (result.stderr or result.stdout or "").strip().splitlines()
|
|
803
|
+
return lines[0] if lines else f"kubectl exit {result.returncode}"
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _cluster_service(host: str) -> tuple[str, str] | None:
|
|
807
|
+
"""``(service, namespace)`` of an in-cluster name, else None."""
|
|
808
|
+
parts = host.lower().rstrip(".").split(".")
|
|
809
|
+
if len(parts) >= 3 and parts[2] == "svc":
|
|
810
|
+
return parts[0], parts[1]
|
|
811
|
+
return None
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _target(node: Node, env: str) -> tuple[Target | None, str | None]:
|
|
815
|
+
"""Where kubectl looks for ``node`` in ``env``: its manifest's context; outside dev never
|
|
816
|
+
the kubeconfig's current one (the `deploy` rule)."""
|
|
817
|
+
context = node.context(env)
|
|
818
|
+
if context is None and env != "dev":
|
|
819
|
+
return None, (
|
|
820
|
+
f"{node.name}'s manifest records no kube context for {env} "
|
|
821
|
+
f"(environments.{env}.context): the current context is never used outside dev"
|
|
822
|
+
)
|
|
823
|
+
return Target(context=context, namespace=node.namespace(env)), None
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _live(system: System, envs: list[str], probe: Probe) -> list[Finding]:
|
|
827
|
+
found: list[Finding] = []
|
|
828
|
+
|
|
829
|
+
def add(check: str, severity: str, message: str, **kw: Any) -> None:
|
|
830
|
+
found.append(Finding(check, severity, message, **kw))
|
|
831
|
+
|
|
832
|
+
for env in envs:
|
|
833
|
+
spec = system.envs[env]
|
|
834
|
+
urls: dict[str, tuple[Link, str]] = {}
|
|
835
|
+
for link in system.links:
|
|
836
|
+
if spec.charted and link.caller.has_chart:
|
|
837
|
+
url = link.caller.setting(env, link.url_env) or system.url(link.callee, env)
|
|
838
|
+
else:
|
|
839
|
+
url = system.url(link.callee, env)
|
|
840
|
+
urls.setdefault(url, (link, url))
|
|
841
|
+
for link, url in urls.values():
|
|
842
|
+
host = urllib.parse.urlsplit(url).hostname or ""
|
|
843
|
+
service = _cluster_service(host)
|
|
844
|
+
if service is not None and spec.kind == CLUSTER:
|
|
845
|
+
target, why = _target(link.caller, env)
|
|
846
|
+
if target is None:
|
|
847
|
+
add("SC14", ERROR, why or "", agent=link.caller.name, env=env)
|
|
848
|
+
continue
|
|
849
|
+
try:
|
|
850
|
+
ready = probe.ready_endpoints(
|
|
851
|
+
Target(context=target.context, namespace=service[1]), service[0]
|
|
852
|
+
)
|
|
853
|
+
except _kube.ToolFailed as exc:
|
|
854
|
+
add("SC14", ERROR, f"{url}: kubectl failed: {exc.message}", env=env)
|
|
855
|
+
continue
|
|
856
|
+
if ready <= 0:
|
|
857
|
+
what = "does not exist" if ready < 0 else "has no ready endpoint"
|
|
858
|
+
add(
|
|
859
|
+
"SC14",
|
|
860
|
+
ERROR,
|
|
861
|
+
f"{link.describe()}: the Service {service[0]} in {service[1]} {what}",
|
|
862
|
+
agent=link.callee.name,
|
|
863
|
+
env=env,
|
|
864
|
+
fix=f"graph-agents-cli system deploy --env {env} --only {link.callee.name}",
|
|
865
|
+
)
|
|
866
|
+
continue
|
|
867
|
+
problem = probe.resolves(host) if spec.kind != LOCAL else None
|
|
868
|
+
if problem:
|
|
869
|
+
add(
|
|
870
|
+
"SC14",
|
|
871
|
+
ERROR,
|
|
872
|
+
f"{link.describe()}: {host} does not resolve ({problem})",
|
|
873
|
+
env=env,
|
|
874
|
+
)
|
|
875
|
+
continue
|
|
876
|
+
card = f"{url.rstrip('/')}{link.callee.mount(env if spec.charted else None)}"
|
|
877
|
+
card += gen.CARD_SUFFIX
|
|
878
|
+
ok, detail = probe.card(card)
|
|
879
|
+
if not ok:
|
|
880
|
+
add(
|
|
881
|
+
"SC14",
|
|
882
|
+
ERROR,
|
|
883
|
+
f"{link.describe()}: {card} is not reachable ({detail})",
|
|
884
|
+
agent=link.callee.name,
|
|
885
|
+
env=env,
|
|
886
|
+
)
|
|
887
|
+
token = system.token_url(env)
|
|
888
|
+
if token and any(link.auth == "exchange" for link in system.links):
|
|
889
|
+
host = urllib.parse.urlsplit(token).hostname or ""
|
|
890
|
+
service = _cluster_service(host)
|
|
891
|
+
if service is not None and spec.kind == CLUSTER:
|
|
892
|
+
caller = next(lk.caller for lk in system.links if lk.auth == "exchange")
|
|
893
|
+
target, why = _target(caller, env)
|
|
894
|
+
if target is not None:
|
|
895
|
+
try:
|
|
896
|
+
ready = probe.ready_endpoints(
|
|
897
|
+
Target(context=target.context, namespace=service[1]), service[0]
|
|
898
|
+
)
|
|
899
|
+
except _kube.ToolFailed as exc:
|
|
900
|
+
ready = 0
|
|
901
|
+
why = str(exc.message)
|
|
902
|
+
if ready <= 0:
|
|
903
|
+
add(
|
|
904
|
+
"SC14",
|
|
905
|
+
ERROR,
|
|
906
|
+
f"the token URL {token}: no ready issuer behind {service[0]} in "
|
|
907
|
+
f"{service[1]}" + (f" ({why})" if why else ""),
|
|
908
|
+
env=env,
|
|
909
|
+
)
|
|
910
|
+
elif spec.kind != LOCAL:
|
|
911
|
+
problem = probe.resolves(host)
|
|
912
|
+
if problem:
|
|
913
|
+
add(
|
|
914
|
+
"SC14",
|
|
915
|
+
ERROR,
|
|
916
|
+
f"the token URL {token} does not resolve ({problem})",
|
|
917
|
+
env=env,
|
|
918
|
+
)
|
|
919
|
+
if not spec.charted:
|
|
920
|
+
continue
|
|
921
|
+
for node in system.nodes.values():
|
|
922
|
+
links = system.calls(node)
|
|
923
|
+
if not links or not node.has_chart:
|
|
924
|
+
continue
|
|
925
|
+
needed = []
|
|
926
|
+
if any(link.auth == "exchange" for link in links):
|
|
927
|
+
needed.append(TOKEN_EXCHANGE_SECRET_ENV)
|
|
928
|
+
needed += [link.token_env for link in links if link.token_env]
|
|
929
|
+
target, why = _target(node, env)
|
|
930
|
+
if target is None:
|
|
931
|
+
add("SC15", ERROR, why or "", agent=node.name, env=env)
|
|
932
|
+
continue
|
|
933
|
+
secret = f"{node.config.project_name}-app"
|
|
934
|
+
try:
|
|
935
|
+
keys = probe.secret_keys(target, secret)
|
|
936
|
+
except _kube.ToolFailed as exc:
|
|
937
|
+
add(
|
|
938
|
+
"SC15",
|
|
939
|
+
ERROR,
|
|
940
|
+
f"{node.name}: kubectl failed: {exc.message}",
|
|
941
|
+
agent=node.name,
|
|
942
|
+
env=env,
|
|
943
|
+
)
|
|
944
|
+
continue
|
|
945
|
+
missing = [k for k in dict.fromkeys(needed) if k and k not in (keys or set())]
|
|
946
|
+
if missing:
|
|
947
|
+
add(
|
|
948
|
+
"SC15",
|
|
949
|
+
ERROR,
|
|
950
|
+
f"{node.name}'s Secret {secret} in {target.namespace} "
|
|
951
|
+
+ ("does not exist" if keys is None else f"lacks {', '.join(missing)}"),
|
|
952
|
+
agent=node.name,
|
|
953
|
+
env=env,
|
|
954
|
+
fix=f"put them in .env.{env}, then graph-agents-cli secrets apply --env {env}",
|
|
955
|
+
)
|
|
956
|
+
if keys is not None and SALT not in keys:
|
|
957
|
+
add(
|
|
958
|
+
"SC15",
|
|
959
|
+
WARNING,
|
|
960
|
+
f"{node.name}'s Secret {secret} has no {SALT}",
|
|
961
|
+
agent=node.name,
|
|
962
|
+
env=env,
|
|
963
|
+
)
|
|
964
|
+
return found
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
# ---------------------------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def run(
|
|
971
|
+
system: System,
|
|
972
|
+
envs: list[str],
|
|
973
|
+
*,
|
|
974
|
+
live: bool = False,
|
|
975
|
+
probe: Probe | None = None,
|
|
976
|
+
expected: dict[str, dict[str, Any] | None] | None = None,
|
|
977
|
+
) -> list[Finding]:
|
|
978
|
+
"""Every finding for ``envs`` (static checks always; SC14 and SC15 with ``live``)."""
|
|
979
|
+
from graph_agents_cli.system import _apply
|
|
980
|
+
|
|
981
|
+
if expected is None:
|
|
982
|
+
expected = {}
|
|
983
|
+
try:
|
|
984
|
+
for result in _apply.plan_system(system, envs):
|
|
985
|
+
expected[result.node.name] = result.document
|
|
986
|
+
except click.ClickException as exc:
|
|
987
|
+
return [
|
|
988
|
+
*_static(system, envs, {}),
|
|
989
|
+
Finding(
|
|
990
|
+
"SC02",
|
|
991
|
+
ERROR,
|
|
992
|
+
exc.format_message(),
|
|
993
|
+
fix="fix the project, then graph-agents-cli system apply",
|
|
994
|
+
),
|
|
995
|
+
]
|
|
996
|
+
found = _static(system, envs, expected)
|
|
997
|
+
if live:
|
|
998
|
+
found += _live(system, envs, probe or Probe())
|
|
999
|
+
return found
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _static(
|
|
1003
|
+
system: System, envs: list[str], expected: dict[str, dict[str, Any] | None]
|
|
1004
|
+
) -> list[Finding]:
|
|
1005
|
+
checks: list[Callable[[], list[Finding]]] = [
|
|
1006
|
+
lambda: _sc01(system, envs),
|
|
1007
|
+
lambda: _sc02(system, expected),
|
|
1008
|
+
lambda: _sc03(system, envs),
|
|
1009
|
+
lambda: _sc04(system, envs),
|
|
1010
|
+
lambda: _sc05(system, envs),
|
|
1011
|
+
lambda: _sc06(system, envs),
|
|
1012
|
+
lambda: _sc07(system),
|
|
1013
|
+
lambda: _sc08(system, envs),
|
|
1014
|
+
lambda: _sc09(system, envs),
|
|
1015
|
+
lambda: _sc10(system, envs),
|
|
1016
|
+
lambda: _sc11(system),
|
|
1017
|
+
lambda: _sc12(system),
|
|
1018
|
+
lambda: _sc13(system, envs),
|
|
1019
|
+
]
|
|
1020
|
+
found: list[Finding] = []
|
|
1021
|
+
for check in checks:
|
|
1022
|
+
found.extend(check())
|
|
1023
|
+
return found
|