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,519 @@
|
|
|
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 apply``: make every project of a system agree with the file.
|
|
16
|
+
|
|
17
|
+
One ``Plan`` per project, built from the project's files as they are (so a
|
|
18
|
+
second run finds nothing to do):
|
|
19
|
+
|
|
20
|
+
- **as a caller**, for each edge, what ``peer add`` writes (``plan_add``, with the
|
|
21
|
+
path from the called agent's A2A name and the audience from its name), and per
|
|
22
|
+
environment the called agent's URL (``<PEER>_AGENT_URL``), the issuer's token
|
|
23
|
+
endpoint (``TOKEN_EXCHANGE_URL``) and ``networkPolicy.egressTo`` to the called
|
|
24
|
+
agent's pods; ``TOKEN_EXCHANGE_CLIENT_ID`` is the file's ``client_id``. A peer
|
|
25
|
+
that is an agent of the file and has left ``calls`` is removed
|
|
26
|
+
(``plan_remove``); other peers are never touched.
|
|
27
|
+
- **as a called agent**: ``appUrl`` per environment (the URL callers dial, so
|
|
28
|
+
their card check passes), ``AUTH_JWT_AUDIENCE`` when it is empty, the callers'
|
|
29
|
+
client ids added to ``AUTH_ALLOWED_ACTORS``, and per environment
|
|
30
|
+
``networkPolicy.ingressFrom`` from the callers' pods, plus the Gateway's
|
|
31
|
+
namespace while the route publishes anything (the Gateway must still reach
|
|
32
|
+
``/chat`` and whatever else it routes once the policy is on).
|
|
33
|
+
|
|
34
|
+
Never written: approval gates (a called agent's ``decide_with: relayed`` stays a
|
|
35
|
+
reviewed ``api approval`` command, printed), secrets, ``.env``, and anything for
|
|
36
|
+
local environments (their lines are printed). An existing peer's entry is
|
|
37
|
+
rewritten only where it differs from the file; its limits, timeouts, approval
|
|
38
|
+
timeout, resource indicator and description stay (``plan_add(keep_tuning=True)``).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import copy
|
|
44
|
+
from collections.abc import Callable
|
|
45
|
+
from dataclasses import dataclass, field
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
import click
|
|
50
|
+
|
|
51
|
+
from graph_agents_cli._api_policy import (
|
|
52
|
+
TOKEN_EXCHANGE_CLIENT_ID_ENV,
|
|
53
|
+
TOKEN_EXCHANGE_URL_ENV,
|
|
54
|
+
)
|
|
55
|
+
from graph_agents_cli.api._files import Plan, read_text, values_env_set
|
|
56
|
+
from graph_agents_cli.peer import _generate as gen
|
|
57
|
+
from graph_agents_cli.peer.cmd_peer import plan_add, plan_remove
|
|
58
|
+
from graph_agents_cli.scaffold.utils.keyedit import EditError, YamlText
|
|
59
|
+
from graph_agents_cli.system._system import CLUSTER, LOCAL, Link, Node, System
|
|
60
|
+
|
|
61
|
+
NS_LABEL = "kubernetes.io/metadata.name"
|
|
62
|
+
INGRESS_PATH = ("networkPolicy", "ingressFrom")
|
|
63
|
+
EGRESS_PATH = ("networkPolicy", "egressTo")
|
|
64
|
+
ALLOWED_ACTORS_ENV = "AUTH_ALLOWED_ACTORS"
|
|
65
|
+
AUDIENCE_ENV = "AUTH_JWT_AUDIENCE"
|
|
66
|
+
ALLOWED_ACTORS_COMMENT = (
|
|
67
|
+
"Agents that may call this agent for a user: their actor ids (graph-agents-cli system apply "
|
|
68
|
+
"adds its callers)."
|
|
69
|
+
)
|
|
70
|
+
APP_URL_COMMENT = "The URL other agents call this agent at (graph-agents-cli system apply)."
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SystemApplyError(click.ClickException):
|
|
74
|
+
"""A project cannot take the file's edges (exit 3); nothing was written."""
|
|
75
|
+
|
|
76
|
+
exit_code = 3
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ProjectPlan:
|
|
81
|
+
"""What ``system apply`` changes in one project, and what it leaves to the user."""
|
|
82
|
+
|
|
83
|
+
node: Node
|
|
84
|
+
plan: Plan
|
|
85
|
+
notes: list[str] = field(default_factory=list)
|
|
86
|
+
todos: list[str] = field(default_factory=list)
|
|
87
|
+
document: dict[str, Any] | None = None # the policy as it will be
|
|
88
|
+
|
|
89
|
+
def note(self, text: str) -> None:
|
|
90
|
+
if text not in self.notes:
|
|
91
|
+
self.notes.append(text)
|
|
92
|
+
|
|
93
|
+
def todo(self, text: str) -> None:
|
|
94
|
+
if text not in self.todos:
|
|
95
|
+
self.todos.append(text)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def pod_peer(namespace: str, labels: dict[str, str]) -> dict[str, Any]:
|
|
99
|
+
"""A NetworkPolicy peer: the agent pods (``labels``, the chart's selector labels) in
|
|
100
|
+
``namespace``; both must match."""
|
|
101
|
+
return {
|
|
102
|
+
"namespaceSelector": {"matchLabels": {NS_LABEL: namespace}},
|
|
103
|
+
"podSelector": {"matchLabels": dict(labels)},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def egress_rule(namespace: str, labels: dict[str, str], port: int | str) -> dict[str, Any]:
|
|
108
|
+
"""An egress rule to the agent pods, on their http port (the pod's port, not the
|
|
109
|
+
Service's: a NetworkPolicy sees the connection after the Service has translated it)."""
|
|
110
|
+
return {"to": [pod_peer(namespace, labels)], "ports": [{"port": port, "protocol": "TCP"}]}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def gateway_peer(namespace: str) -> dict[str, Any]:
|
|
114
|
+
return {"namespaceSelector": {"matchLabels": {NS_LABEL: namespace}}}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _rel(node: Node, path: Path) -> str:
|
|
118
|
+
return path.relative_to(node.root).as_posix()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _edit(result: ProjectPlan, rel: str, action: Callable[[YamlText], None], what: str) -> None:
|
|
122
|
+
"""Apply ``action`` to the planned text of the values file ``rel``; a todo when it cannot."""
|
|
123
|
+
plan = result.plan
|
|
124
|
+
before = plan.current(rel)
|
|
125
|
+
if before is None:
|
|
126
|
+
result.todo(f"{rel}: {what} (the file does not exist)")
|
|
127
|
+
return
|
|
128
|
+
try:
|
|
129
|
+
values = YamlText(before)
|
|
130
|
+
action(values)
|
|
131
|
+
except EditError as exc:
|
|
132
|
+
result.todo(f"{rel}: {what} (not edited: {exc})")
|
|
133
|
+
return
|
|
134
|
+
plan.set_text(rel, read_text(plan.root / rel), values.text)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _set_env(values: YamlText, key: str, value: str, *, comment: str | None = None) -> None:
|
|
138
|
+
env = values.get(("env",))
|
|
139
|
+
if env is None:
|
|
140
|
+
values.set(("env",), {key: value})
|
|
141
|
+
elif not isinstance(env, dict):
|
|
142
|
+
raise EditError("env: is not a mapping")
|
|
143
|
+
elif key not in env:
|
|
144
|
+
values.set(("env", key), value, after="AUTH_ADMIN_ROLES", comment=comment)
|
|
145
|
+
elif env.get(key) != value:
|
|
146
|
+
values.set(("env", key), value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _sync_list(
|
|
150
|
+
values: YamlText,
|
|
151
|
+
path: tuple[str, ...],
|
|
152
|
+
base: list[Any],
|
|
153
|
+
desired: list[dict[str, Any]],
|
|
154
|
+
managed: list[dict[str, Any]],
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Make the list at ``path`` hold every ``desired`` entry and no ``managed`` one that is
|
|
157
|
+
not desired; other entries stay. A values-<env>.yaml without the list starts from the
|
|
158
|
+
base values' list (Helm replaces a list whole, so the base entries must be repeated)."""
|
|
159
|
+
current = values.get(path)
|
|
160
|
+
if current is None:
|
|
161
|
+
start = [e for e in base if not (e in managed and e not in desired)]
|
|
162
|
+
new = start + [d for d in desired if d not in start]
|
|
163
|
+
if new != base:
|
|
164
|
+
values.set(path, new, block=True)
|
|
165
|
+
return
|
|
166
|
+
if not isinstance(current, list):
|
|
167
|
+
raise EditError(f"{'.'.join(path)} is not a list")
|
|
168
|
+
for entry in desired:
|
|
169
|
+
if entry not in values.get(path):
|
|
170
|
+
values.append(path, entry)
|
|
171
|
+
stale = [i for i, e in enumerate(values.get(path)) if e in managed and e not in desired]
|
|
172
|
+
for index in reversed(stale):
|
|
173
|
+
values.remove_item(path, index)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _base_list(node: Node, path: tuple[str, ...]) -> list[Any]:
|
|
177
|
+
found: Any = node.values(None)
|
|
178
|
+
for part in path:
|
|
179
|
+
found = found.get(part) if isinstance(found, dict) else None
|
|
180
|
+
return copy.deepcopy(found) if isinstance(found, list) else []
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _network_enabled(node: Node, env: str) -> tuple[bool, bool]:
|
|
184
|
+
policy = node.values(env).get("networkPolicy")
|
|
185
|
+
policy = policy if isinstance(policy, dict) else {}
|
|
186
|
+
return policy.get("enabled") is True, policy.get("restrictEgress") is True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def plan_system(system: System, envs: list[str]) -> list[ProjectPlan]:
|
|
190
|
+
"""A plan per project (file order); ``SystemApplyError`` before anything is written."""
|
|
191
|
+
results = []
|
|
192
|
+
for node in system.nodes.values():
|
|
193
|
+
result = ProjectPlan(node, Plan(node.root), document=node.document)
|
|
194
|
+
_plan_caller(system, node, envs, result)
|
|
195
|
+
_plan_callee(system, node, envs, result)
|
|
196
|
+
for item in result.plan.left_for_you:
|
|
197
|
+
result.todo(item)
|
|
198
|
+
results.append(result)
|
|
199
|
+
return results
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _first_local(system: System, envs: list[str]) -> str | None:
|
|
203
|
+
return next((e for e in envs if system.envs[e].kind == LOCAL), None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _plan_caller(system: System, node: Node, envs: list[str], result: ProjectPlan) -> None:
|
|
207
|
+
links = system.calls(node)
|
|
208
|
+
project = node.project
|
|
209
|
+
names: dict[str, str] = {}
|
|
210
|
+
local = _first_local(system, envs)
|
|
211
|
+
for link in links:
|
|
212
|
+
options = link.options(local_url=system.url(link.callee, local) if local else None)
|
|
213
|
+
try:
|
|
214
|
+
added = plan_add(project, result.plan, options, names=names, keep_tuning=True)
|
|
215
|
+
except click.ClickException as exc:
|
|
216
|
+
raise SystemApplyError(
|
|
217
|
+
f"{link.describe()}: {node.name} ({node.root}) cannot take this edge; nothing was "
|
|
218
|
+
f"written:\n {exc.format_message()}"
|
|
219
|
+
) from None
|
|
220
|
+
project, names = added.project, added.names
|
|
221
|
+
if added.api != link.entry: # peer add's notes, for an entry this run writes
|
|
222
|
+
for note in added.notes:
|
|
223
|
+
result.note(note)
|
|
224
|
+
wanted = {link.callee.name for link in links}
|
|
225
|
+
current = gen.peers_of(project.document, {**node.module_names, **names})
|
|
226
|
+
for peer in current:
|
|
227
|
+
if peer.name in system.nodes and peer.name not in wanted:
|
|
228
|
+
try:
|
|
229
|
+
removed = plan_remove(project, result.plan, peer.name, names=names)
|
|
230
|
+
except click.ClickException as exc:
|
|
231
|
+
raise SystemApplyError(
|
|
232
|
+
f"{node.name}: the peer {peer.name} left its calls but cannot be removed; "
|
|
233
|
+
f"nothing was written:\n {exc.format_message()}"
|
|
234
|
+
) from None
|
|
235
|
+
project = removed.project
|
|
236
|
+
result.note(f"{peer.name} is no longer in {node.name}'s calls: its peer entry goes")
|
|
237
|
+
for note in removed.notes:
|
|
238
|
+
result.note(note)
|
|
239
|
+
result.document = project.document
|
|
240
|
+
charted = [e for e in envs if system.envs[e].charted]
|
|
241
|
+
if not links:
|
|
242
|
+
# No edge left: only the egress rules to agents of the file go.
|
|
243
|
+
for env in charted:
|
|
244
|
+
if node.has_chart and system.envs[env].kind == CLUSTER:
|
|
245
|
+
_plan_egress(system, node, env, [], result)
|
|
246
|
+
return
|
|
247
|
+
exchange = any(link.auth == "exchange" for link in links)
|
|
248
|
+
if node.has_chart:
|
|
249
|
+
for link in links:
|
|
250
|
+
urls = {env: system.url(link.callee, env) for env in charted}
|
|
251
|
+
values_env_set(result.plan, node.config, link.url_env, urls)
|
|
252
|
+
if exchange:
|
|
253
|
+
tokens: dict[str, str] = {}
|
|
254
|
+
for env in charted:
|
|
255
|
+
token = system.token_url(env)
|
|
256
|
+
if token:
|
|
257
|
+
tokens[env] = token
|
|
258
|
+
values_env_set(result.plan, node.config, TOKEN_EXCHANGE_URL_ENV, tokens)
|
|
259
|
+
missing = [e for e in charted if e not in tokens]
|
|
260
|
+
if missing:
|
|
261
|
+
result.todo(
|
|
262
|
+
f"set {TOKEN_EXCHANGE_URL_ENV} for {', '.join(missing)} in "
|
|
263
|
+
"values-<env>.yaml (identity.token_url names none)"
|
|
264
|
+
)
|
|
265
|
+
_set_client_id(node, result)
|
|
266
|
+
for env in charted:
|
|
267
|
+
if system.envs[env].kind == CLUSTER:
|
|
268
|
+
_plan_egress(system, node, env, links, result)
|
|
269
|
+
for env in envs:
|
|
270
|
+
if system.envs[env].kind != LOCAL:
|
|
271
|
+
continue
|
|
272
|
+
lines = [f"{link.url_env}={system.url(link.callee, env)}" for link in links]
|
|
273
|
+
if exchange:
|
|
274
|
+
token = system.token_url(env)
|
|
275
|
+
lines.append(f"{TOKEN_EXCHANGE_URL_ENV}={token or '<the issuer token endpoint>'}")
|
|
276
|
+
lines.append(f"{TOKEN_EXCHANGE_CLIENT_ID_ENV}={node.client_id}")
|
|
277
|
+
result.todo(f"in {node.name}'s .env for {env} (never written): {', '.join(lines)}")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _set_client_id(node: Node, result: ProjectPlan) -> None:
|
|
281
|
+
"""``TOKEN_EXCHANGE_CLIENT_ID`` is the file's client id: in values.yaml (where `peer add`
|
|
282
|
+
put the project's name), and in each values-<env>.yaml that sets its own."""
|
|
283
|
+
files = node.chart_files
|
|
284
|
+
for index, path in enumerate(files):
|
|
285
|
+
rel = _rel(node, path)
|
|
286
|
+
text = result.plan.current(rel) or ""
|
|
287
|
+
try:
|
|
288
|
+
env = YamlText(text).get(("env",))
|
|
289
|
+
except EditError:
|
|
290
|
+
env = None
|
|
291
|
+
if index > 0 and not (isinstance(env, dict) and TOKEN_EXCHANGE_CLIENT_ID_ENV in env):
|
|
292
|
+
continue
|
|
293
|
+
_edit(
|
|
294
|
+
result,
|
|
295
|
+
rel,
|
|
296
|
+
lambda v: _set_env(v, TOKEN_EXCHANGE_CLIENT_ID_ENV, node.client_id),
|
|
297
|
+
f"set env.{TOKEN_EXCHANGE_CLIENT_ID_ENV}: {node.client_id}",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _plan_egress(
|
|
302
|
+
system: System, node: Node, env: str, links: list[Link], result: ProjectPlan
|
|
303
|
+
) -> None:
|
|
304
|
+
values_file = node.values_file(env)
|
|
305
|
+
if values_file is None:
|
|
306
|
+
if links:
|
|
307
|
+
result.todo(f"values-{env}.yaml does not exist: no egress rules written for {env}")
|
|
308
|
+
return
|
|
309
|
+
desired = [
|
|
310
|
+
egress_rule(
|
|
311
|
+
link.callee.namespace(env), link.callee.pod_labels, link.callee.target_port(env)
|
|
312
|
+
)
|
|
313
|
+
for link in links
|
|
314
|
+
]
|
|
315
|
+
managed = [
|
|
316
|
+
egress_rule(other.namespace(env), other.pod_labels, other.target_port(env))
|
|
317
|
+
for other in system.nodes.values()
|
|
318
|
+
]
|
|
319
|
+
base = _base_list(node, EGRESS_PATH)
|
|
320
|
+
_edit(
|
|
321
|
+
result,
|
|
322
|
+
_rel(node, values_file),
|
|
323
|
+
lambda v: _sync_list(v, EGRESS_PATH, base, desired, managed),
|
|
324
|
+
f"networkPolicy.egressTo: allow the pods of {', '.join(lk.callee.name for lk in links)}",
|
|
325
|
+
)
|
|
326
|
+
enabled, restricted = _network_enabled(node, env)
|
|
327
|
+
if not links:
|
|
328
|
+
return
|
|
329
|
+
if not (enabled and restricted):
|
|
330
|
+
result.note(
|
|
331
|
+
f"values-{env}.yaml: the egress rules take effect with networkPolicy.enabled and "
|
|
332
|
+
"restrictEgress (both off now)"
|
|
333
|
+
)
|
|
334
|
+
elif any(link.auth == "exchange" for link in links):
|
|
335
|
+
result.todo(
|
|
336
|
+
f"values-{env}.yaml: with restrictEgress, also allow the token issuer "
|
|
337
|
+
f"({TOKEN_EXCHANGE_URL_ENV}) in networkPolicy.egressTo"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _stale_actors(system: System, node: Node, actors: list[str]) -> list[str]:
|
|
342
|
+
"""Agents of the file ``node``'s AUTH_ALLOWED_ACTORS still lists that no longer call it."""
|
|
343
|
+
clients = {n.client_id for n in system.nodes.values()}
|
|
344
|
+
known = clients | {n.actor_id for n in system.nodes.values()}
|
|
345
|
+
known |= {f"client:{client}" for client in clients}
|
|
346
|
+
listed = node.allowed_actors() if node.has_chart else []
|
|
347
|
+
return [a for a in listed if a in known and a not in actors]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _plan_callee(system: System, node: Node, envs: list[str], result: ProjectPlan) -> None:
|
|
351
|
+
links = system.callers(node)
|
|
352
|
+
charted = [e for e in envs if system.envs[e].charted]
|
|
353
|
+
stale = _stale_actors(system, node, [link.actor for link in links if link.carries_user])
|
|
354
|
+
if stale:
|
|
355
|
+
result.note(
|
|
356
|
+
f"AUTH_ALLOWED_ACTORS still lists {', '.join(stale)}, which no longer call "
|
|
357
|
+
f"{node.name} for a user: remove them if nothing else does (apply never takes access "
|
|
358
|
+
"away)"
|
|
359
|
+
)
|
|
360
|
+
if not links:
|
|
361
|
+
# Nobody calls it any more: only the ingress rules for agents of the file go (its
|
|
362
|
+
# appUrl, audience and allowed actors stay: apply never takes access away).
|
|
363
|
+
for env in charted:
|
|
364
|
+
if node.has_chart and system.envs[env].kind == CLUSTER:
|
|
365
|
+
_plan_ingress(system, node, env, [], result)
|
|
366
|
+
return
|
|
367
|
+
identity = [link for link in links if link.carries_user]
|
|
368
|
+
actors = list(dict.fromkeys(link.actor for link in identity))
|
|
369
|
+
jwt = node.auth_policy() == "jwt"
|
|
370
|
+
if node.has_chart:
|
|
371
|
+
for env in charted:
|
|
372
|
+
values_file = node.values_file(env)
|
|
373
|
+
if values_file is None:
|
|
374
|
+
result.todo(f"values-{env}.yaml does not exist: set appUrl for {env} there")
|
|
375
|
+
continue
|
|
376
|
+
url = system.url(node, env)
|
|
377
|
+
_edit(
|
|
378
|
+
result,
|
|
379
|
+
_rel(node, values_file),
|
|
380
|
+
lambda v, url=url: v.set(("appUrl",), url, comment=APP_URL_COMMENT),
|
|
381
|
+
f"set appUrl: {url}",
|
|
382
|
+
)
|
|
383
|
+
if identity and jwt:
|
|
384
|
+
_set_audience(node, result)
|
|
385
|
+
if actors:
|
|
386
|
+
_add_actors(node, actors, result)
|
|
387
|
+
for env in charted:
|
|
388
|
+
if system.envs[env].kind == CLUSTER:
|
|
389
|
+
_plan_ingress(system, node, env, links, result)
|
|
390
|
+
for env in envs:
|
|
391
|
+
if system.envs[env].kind != LOCAL:
|
|
392
|
+
continue
|
|
393
|
+
port = system.url(node, env).rsplit(":", 1)[-1]
|
|
394
|
+
lines = [f"port {port} (APP_URL=http://127.0.0.1:{port})"]
|
|
395
|
+
if identity and jwt:
|
|
396
|
+
lines.append(f"{AUDIENCE_ENV} including {node.name}")
|
|
397
|
+
if actors:
|
|
398
|
+
lines.append(f"{ALLOWED_ACTORS_ENV} including {','.join(actors)}")
|
|
399
|
+
result.todo(f"run {node.name} for {env} on {', '.join(lines)} (.env is never written)")
|
|
400
|
+
if not jwt and identity:
|
|
401
|
+
result.note(
|
|
402
|
+
f"{node.name} uses the {node.auth_policy()} auth policy: its policy must accept the "
|
|
403
|
+
f"callers' credentials and set their actor ({', '.join(actors)})"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _set_audience(node: Node, result: ProjectPlan) -> None:
|
|
408
|
+
"""``AUTH_JWT_AUDIENCE`` becomes the agent's name where it is empty (values.yaml, and each
|
|
409
|
+
values-<env>.yaml that sets it empty); a set audience is never changed (SC04 checks it)."""
|
|
410
|
+
for index, path in enumerate(node.chart_files):
|
|
411
|
+
rel = _rel(node, path)
|
|
412
|
+
try:
|
|
413
|
+
env = YamlText(result.plan.current(rel) or "").get(("env",))
|
|
414
|
+
except EditError:
|
|
415
|
+
continue
|
|
416
|
+
env = env if isinstance(env, dict) else {}
|
|
417
|
+
if index > 0 and AUDIENCE_ENV not in env:
|
|
418
|
+
continue
|
|
419
|
+
if str(env.get(AUDIENCE_ENV) or "").strip():
|
|
420
|
+
continue
|
|
421
|
+
_edit(
|
|
422
|
+
result,
|
|
423
|
+
rel,
|
|
424
|
+
lambda v: _set_env(v, AUDIENCE_ENV, node.name),
|
|
425
|
+
f"set env.{AUDIENCE_ENV}: {node.name}",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _add_actors(node: Node, actors: list[str], result: ProjectPlan) -> None:
|
|
430
|
+
"""The callers' actor ids join ``AUTH_ALLOWED_ACTORS`` (values.yaml, and each
|
|
431
|
+
values-<env>.yaml that sets its own). Ids already there, and ``*``, stay; none goes."""
|
|
432
|
+
for index, path in enumerate(node.chart_files):
|
|
433
|
+
rel = _rel(node, path)
|
|
434
|
+
try:
|
|
435
|
+
env = YamlText(result.plan.current(rel) or "").get(("env",))
|
|
436
|
+
except EditError:
|
|
437
|
+
continue
|
|
438
|
+
env = env if isinstance(env, dict) else {}
|
|
439
|
+
if index > 0 and ALLOWED_ACTORS_ENV not in env:
|
|
440
|
+
continue
|
|
441
|
+
present = [a.strip() for a in str(env.get(ALLOWED_ACTORS_ENV) or "").split(",")]
|
|
442
|
+
present = [a for a in present if a]
|
|
443
|
+
if "*" in present:
|
|
444
|
+
continue
|
|
445
|
+
wanted = present + [a for a in actors if a not in present]
|
|
446
|
+
if wanted == present:
|
|
447
|
+
continue
|
|
448
|
+
value = ",".join(wanted)
|
|
449
|
+
_edit(
|
|
450
|
+
result,
|
|
451
|
+
rel,
|
|
452
|
+
lambda v, value=value: _set_env(
|
|
453
|
+
v, ALLOWED_ACTORS_ENV, value, comment=ALLOWED_ACTORS_COMMENT
|
|
454
|
+
),
|
|
455
|
+
f"set env.{ALLOWED_ACTORS_ENV}: {value}",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _plan_ingress(
|
|
460
|
+
system: System, node: Node, env: str, links: list[Link], result: ProjectPlan
|
|
461
|
+
) -> None:
|
|
462
|
+
values_file = node.values_file(env)
|
|
463
|
+
if values_file is None:
|
|
464
|
+
return
|
|
465
|
+
desired = [pod_peer(link.caller.namespace(env), link.caller.pod_labels) for link in links]
|
|
466
|
+
managed = [pod_peer(o.namespace(env), o.pod_labels) for o in system.nodes.values()]
|
|
467
|
+
if links and node.public_paths(env):
|
|
468
|
+
values = node.values(env)
|
|
469
|
+
gateway = values.get("gateway") if isinstance(values.get("gateway"), dict) else {}
|
|
470
|
+
parent = gateway.get("parentRef") if isinstance(gateway.get("parentRef"), dict) else {}
|
|
471
|
+
namespace = str(parent.get("namespace") or "").strip()
|
|
472
|
+
if gateway.get("enabled") is True and namespace:
|
|
473
|
+
desired.append(gateway_peer(namespace))
|
|
474
|
+
else:
|
|
475
|
+
result.todo(
|
|
476
|
+
f"values-{env}.yaml: the route publishes paths, so networkPolicy.ingressFrom must "
|
|
477
|
+
"also admit the namespace of the Gateway's (or ingress controller's) pods: add it"
|
|
478
|
+
)
|
|
479
|
+
base = _base_list(node, INGRESS_PATH)
|
|
480
|
+
_edit(
|
|
481
|
+
result,
|
|
482
|
+
_rel(node, values_file),
|
|
483
|
+
lambda v: _sync_list(v, INGRESS_PATH, base, desired, managed),
|
|
484
|
+
f"networkPolicy.ingressFrom: admit {', '.join(lk.caller.name for lk in links)}",
|
|
485
|
+
)
|
|
486
|
+
enabled, _restricted = _network_enabled(node, env)
|
|
487
|
+
if not links:
|
|
488
|
+
return
|
|
489
|
+
if not enabled:
|
|
490
|
+
result.note(
|
|
491
|
+
f"values-{env}.yaml: the ingress rules take effect with networkPolicy.enabled (off now)"
|
|
492
|
+
)
|
|
493
|
+
metrics = node.values(env).get("metrics")
|
|
494
|
+
metrics = metrics if isinstance(metrics, dict) else {}
|
|
495
|
+
monitor = (
|
|
496
|
+
metrics.get("serviceMonitor") if isinstance(metrics.get("serviceMonitor"), dict) else {}
|
|
497
|
+
)
|
|
498
|
+
if metrics.get("scrapeAnnotations") is True or monitor.get("enabled") is True:
|
|
499
|
+
result.todo(
|
|
500
|
+
f"values-{env}.yaml: Prometheus scrapes /metrics: admit its namespace in "
|
|
501
|
+
"networkPolicy.ingressFrom too"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def relay_commands(
|
|
506
|
+
system: System, results: list[ProjectPlan] | None = None
|
|
507
|
+
) -> list[tuple[Node, str, str]]:
|
|
508
|
+
"""``(called agent, why, command)`` for each relay edge a called agent's gates refuse, the
|
|
509
|
+
policies as ``results`` leave them (a caller's own relay gates come with its peers)."""
|
|
510
|
+
documents = {r.node.name: r.document for r in results or []}
|
|
511
|
+
found = []
|
|
512
|
+
for link in system.links:
|
|
513
|
+
for gate in system.relay_gates(link, documents):
|
|
514
|
+
why = (
|
|
515
|
+
f"relays from {link.caller.name} to {link.callee.name} need the person to approve "
|
|
516
|
+
f"at {link.callee.name}"
|
|
517
|
+
)
|
|
518
|
+
found.append((link.callee, why, gate.command(link.actor)))
|
|
519
|
+
return found
|