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,506 @@
|
|
|
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
|
+
"""What an ``api`` command changes in the policy, as data (no files, no output).
|
|
16
|
+
|
|
17
|
+
Access is always an explicit choice: ``read-only`` and ``read-write`` are
|
|
18
|
+
shorthands for a list of methods that is written into the file in full (the
|
|
19
|
+
file never stores a preset name), and ``custom`` takes the list itself.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
from collections.abc import Mapping, Sequence
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
import click
|
|
31
|
+
import yaml
|
|
32
|
+
|
|
33
|
+
from graph_agents_cli._api_policy import (
|
|
34
|
+
_RPC_METHOD_RE,
|
|
35
|
+
A2A_OPERATION_KEY,
|
|
36
|
+
A2A_OPERATIONS,
|
|
37
|
+
ANY_METHOD,
|
|
38
|
+
HTTP_METHODS,
|
|
39
|
+
RPC_METHOD_KEY,
|
|
40
|
+
normalize_path,
|
|
41
|
+
path_matches,
|
|
42
|
+
path_template_problem,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
READ_ONLY = "read-only"
|
|
46
|
+
READ_WRITE = "read-write"
|
|
47
|
+
CUSTOM = "custom"
|
|
48
|
+
ACCESS_CHOICES = (READ_ONLY, READ_WRITE, CUSTOM)
|
|
49
|
+
PRESETS: dict[str, tuple[str, ...]] = {
|
|
50
|
+
READ_ONLY: ("GET", "HEAD"),
|
|
51
|
+
READ_WRITE: ("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"),
|
|
52
|
+
}
|
|
53
|
+
ALLOWED = "allowed_operations"
|
|
54
|
+
DENIED = "denied_operations"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ApiCommandError(click.ClickException):
|
|
58
|
+
"""The change cannot be made: an invalid result or a configuration problem (exit 3)."""
|
|
59
|
+
|
|
60
|
+
exit_code = 3
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Methods
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_methods(value: str | None) -> list[str]:
|
|
69
|
+
"""``get, Post`` -> ``["GET", "POST"]`` (canonical order); ``*`` alone -> ``["*"]``."""
|
|
70
|
+
if value is None:
|
|
71
|
+
return []
|
|
72
|
+
items = [item.strip().upper() for item in value.split(",") if item.strip()]
|
|
73
|
+
if not items:
|
|
74
|
+
raise click.UsageError("--methods needs at least one HTTP method")
|
|
75
|
+
if ANY_METHOD in items:
|
|
76
|
+
if len(items) != 1:
|
|
77
|
+
raise click.UsageError('--methods "*" must stand alone (every method)')
|
|
78
|
+
return [ANY_METHOD]
|
|
79
|
+
unknown = [item for item in items if item not in HTTP_METHODS]
|
|
80
|
+
if unknown:
|
|
81
|
+
raise click.UsageError(
|
|
82
|
+
f"unknown HTTP method(s) {', '.join(unknown)} (allowed: {', '.join(HTTP_METHODS)}, "
|
|
83
|
+
'or "*" alone)'
|
|
84
|
+
)
|
|
85
|
+
return [m for m in HTTP_METHODS if m in items]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def access_methods(access: str, methods: list[str]) -> list[str]:
|
|
89
|
+
"""The ``allowed_methods`` an access choice writes into the file."""
|
|
90
|
+
if access == CUSTOM:
|
|
91
|
+
if not methods:
|
|
92
|
+
raise click.UsageError("custom access needs --methods M,... (the methods to allow)")
|
|
93
|
+
return list(methods)
|
|
94
|
+
if methods:
|
|
95
|
+
raise click.UsageError(f"--methods goes with custom access only (not {access})")
|
|
96
|
+
return list(PRESETS[access])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def preset_name(methods: Sequence[str]) -> str | None:
|
|
100
|
+
"""``read-only`` / ``read-write`` when ``methods`` is exactly that preset, else None."""
|
|
101
|
+
normalized = [str(m).upper() for m in methods]
|
|
102
|
+
for name, preset in PRESETS.items():
|
|
103
|
+
if sorted(normalized) == sorted(preset):
|
|
104
|
+
return name
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def effective_methods(methods: Sequence[str]) -> list[str]:
|
|
109
|
+
normalized = [str(m).upper() for m in methods]
|
|
110
|
+
return list(HTTP_METHODS) if ANY_METHOD in normalized else normalized
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def describe_methods(methods: Sequence[str]) -> str:
|
|
114
|
+
normalized = [str(m).upper() for m in methods]
|
|
115
|
+
if ANY_METHOD in normalized:
|
|
116
|
+
return '"*" (every method)'
|
|
117
|
+
text = ", ".join(normalized)
|
|
118
|
+
preset = preset_name(normalized)
|
|
119
|
+
return f"{text} (the {preset} preset)" if preset else text
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Operation entries
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class OperationRef:
|
|
129
|
+
"""How a command names an operation: an operationId, a method and a path, or all three;
|
|
130
|
+
on a JSON-RPC API also the JSON-RPC method (``rpc_method``) and what an A2A message
|
|
131
|
+
decides (``a2a_operation``)."""
|
|
132
|
+
|
|
133
|
+
operation_id: str | None = None
|
|
134
|
+
method: str | None = None
|
|
135
|
+
path: str | None = None
|
|
136
|
+
rpc_method: str | None = None
|
|
137
|
+
a2a_operation: str | None = None
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def rpc(self) -> bool:
|
|
141
|
+
"""Whether it names a JSON-RPC request (``rpc_method`` or ``a2a_operation``)."""
|
|
142
|
+
return self.rpc_method is not None or self.a2a_operation is not None
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def from_options(
|
|
146
|
+
cls,
|
|
147
|
+
operation_id: str | None,
|
|
148
|
+
method: str | None,
|
|
149
|
+
path: str | None,
|
|
150
|
+
*,
|
|
151
|
+
combine: bool = False,
|
|
152
|
+
rpc_method: str | None = None,
|
|
153
|
+
a2a_operation: str | None = None,
|
|
154
|
+
endpoint: bool = False,
|
|
155
|
+
) -> OperationRef:
|
|
156
|
+
"""Read OPERATION_ID / ``--method`` / ``--path`` (and ``--rpc-method`` /
|
|
157
|
+
``--a2a-operation``).
|
|
158
|
+
|
|
159
|
+
With ``combine`` (``allow``, ``deny``) OPERATION_ID may come with
|
|
160
|
+
``--method M --path P``, the endpoint it names: the entry then pins all
|
|
161
|
+
three. With ``--rpc-method`` or ``--a2a-operation`` the other options are
|
|
162
|
+
optional (``--method`` and ``--path`` together, and ``--method`` POST);
|
|
163
|
+
``endpoint`` (``allow``) requires them, so an allow names its endpoint.
|
|
164
|
+
"""
|
|
165
|
+
if rpc_method is not None or a2a_operation is not None:
|
|
166
|
+
return cls._rpc(operation_id, method, path, rpc_method, a2a_operation, endpoint)
|
|
167
|
+
if operation_id and (method or path) and not combine:
|
|
168
|
+
raise click.UsageError(
|
|
169
|
+
"name the operation by OPERATION_ID or by --method/--path, not both"
|
|
170
|
+
)
|
|
171
|
+
if operation_id and any(c.isspace() for c in operation_id):
|
|
172
|
+
raise click.UsageError("OPERATION_ID must not contain whitespace")
|
|
173
|
+
if operation_id and not (method or path):
|
|
174
|
+
return cls(operation_id=operation_id)
|
|
175
|
+
if not (method and path):
|
|
176
|
+
if operation_id:
|
|
177
|
+
raise click.UsageError(
|
|
178
|
+
"with OPERATION_ID, give both --method and --path (the endpoint it names), "
|
|
179
|
+
"or neither"
|
|
180
|
+
)
|
|
181
|
+
raise click.UsageError("name the operation: OPERATION_ID, or --method M --path P")
|
|
182
|
+
method = method.strip().upper()
|
|
183
|
+
if method not in HTTP_METHODS:
|
|
184
|
+
raise click.UsageError(
|
|
185
|
+
f"unknown HTTP method {method} (allowed: {', '.join(HTTP_METHODS)})"
|
|
186
|
+
)
|
|
187
|
+
problem = path_template_problem(path)
|
|
188
|
+
if problem:
|
|
189
|
+
raise click.UsageError(f"--path {path}: {problem}")
|
|
190
|
+
return cls(operation_id=operation_id or None, method=method, path=path)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def _rpc(
|
|
194
|
+
cls,
|
|
195
|
+
operation_id: str | None,
|
|
196
|
+
method: str | None,
|
|
197
|
+
path: str | None,
|
|
198
|
+
rpc_method: str | None,
|
|
199
|
+
a2a_operation: str | None,
|
|
200
|
+
endpoint: bool,
|
|
201
|
+
) -> OperationRef:
|
|
202
|
+
if operation_id and any(c.isspace() for c in operation_id):
|
|
203
|
+
raise click.UsageError("OPERATION_ID must not contain whitespace")
|
|
204
|
+
if rpc_method is not None and not _RPC_METHOD_RE.fullmatch(rpc_method):
|
|
205
|
+
raise click.UsageError(
|
|
206
|
+
f"--rpc-method {rpc_method!r}: a JSON-RPC method name is a letter, then up to 63 "
|
|
207
|
+
"letters, digits, '_', '/' or '.'"
|
|
208
|
+
)
|
|
209
|
+
if a2a_operation is not None and a2a_operation not in A2A_OPERATIONS:
|
|
210
|
+
raise click.UsageError("--a2a-operation takes approve or reject")
|
|
211
|
+
if bool(method) != bool(path) or (endpoint and not (method and path)):
|
|
212
|
+
raise click.UsageError(
|
|
213
|
+
"give both --method POST and --path P (the JSON-RPC endpoint)"
|
|
214
|
+
+ ("" if endpoint else ", or neither")
|
|
215
|
+
)
|
|
216
|
+
if method is not None:
|
|
217
|
+
method = method.strip().upper()
|
|
218
|
+
if method != "POST":
|
|
219
|
+
raise click.UsageError(
|
|
220
|
+
f"--method {method}: a JSON-RPC request is a POST (GET and HEAD carry no "
|
|
221
|
+
"rpc_method)"
|
|
222
|
+
)
|
|
223
|
+
problem = path_template_problem(path)
|
|
224
|
+
if problem:
|
|
225
|
+
raise click.UsageError(f"--path {path}: {problem}")
|
|
226
|
+
return cls(
|
|
227
|
+
operation_id=operation_id or None,
|
|
228
|
+
method=method,
|
|
229
|
+
path=path,
|
|
230
|
+
rpc_method=rpc_method,
|
|
231
|
+
a2a_operation=a2a_operation,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def describe(self) -> str:
|
|
235
|
+
endpoint = f"{self.method} {self.path}" if self.path else ""
|
|
236
|
+
rpc = [f"{k} {v}" for k, v in self._rpc_fields() if v]
|
|
237
|
+
return " ".join(part for part in (self.operation_id, endpoint, *rpc) if part)
|
|
238
|
+
|
|
239
|
+
def args(self) -> str:
|
|
240
|
+
"""The command-line form (for suggestions)."""
|
|
241
|
+
endpoint = f"--method {self.method} --path {self.path}" if self.path else ""
|
|
242
|
+
rpc = [f"--{k.replace('_', '-')} {v}" for k, v in self._rpc_fields() if v]
|
|
243
|
+
return " ".join(part for part in (self.operation_id, endpoint, *rpc) if part)
|
|
244
|
+
|
|
245
|
+
def _rpc_fields(self) -> tuple[tuple[str, str | None], ...]:
|
|
246
|
+
return ((RPC_METHOD_KEY, self.rpc_method), (A2A_OPERATION_KEY, self.a2a_operation))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def spec_operations(spec: Mapping[str, Any]) -> list[tuple[str, str, str | None]]:
|
|
250
|
+
"""``(path, METHOD, operationId)`` of every operation of an OpenAPI spec."""
|
|
251
|
+
from graph_agents_cli.dev.policy_check import _spec_operations
|
|
252
|
+
|
|
253
|
+
return _spec_operations(dict(spec))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def build_entry(
|
|
257
|
+
ref: OperationRef,
|
|
258
|
+
methods: list[str],
|
|
259
|
+
spec: Mapping[str, Any] | None,
|
|
260
|
+
spec_name: str,
|
|
261
|
+
) -> tuple[dict[str, Any], list[str]]:
|
|
262
|
+
"""The ``allowed_operations`` / ``denied_operations`` entry for ``ref``, and warnings.
|
|
263
|
+
|
|
264
|
+
With an OpenAPI spec, an operationId must exist in it, and its method and
|
|
265
|
+
path are filled in (all three must then match: AND semantics), so the entry
|
|
266
|
+
is exact; a ``--method``/``--path`` given with it must agree with the
|
|
267
|
+
spec. Without a spec, an operationId given with ``--method M --path P``
|
|
268
|
+
pins all three.
|
|
269
|
+
"""
|
|
270
|
+
warnings: list[str] = []
|
|
271
|
+
if ANY_METHOD in methods:
|
|
272
|
+
raise click.UsageError('an operation entry lists its methods; "*" is not allowed there')
|
|
273
|
+
wanted = [m for m in HTTP_METHODS if m in {*methods, *([ref.method] if ref.method else [])}]
|
|
274
|
+
if ref.rpc:
|
|
275
|
+
# A JSON-RPC request (0.3): judged by what its body is, not by an OpenAPI spec.
|
|
276
|
+
entry = {
|
|
277
|
+
key: value
|
|
278
|
+
for key, value in (
|
|
279
|
+
("operationId", ref.operation_id),
|
|
280
|
+
("path", ref.path),
|
|
281
|
+
("methods", wanted or None),
|
|
282
|
+
(RPC_METHOD_KEY, ref.rpc_method),
|
|
283
|
+
(A2A_OPERATION_KEY, ref.a2a_operation),
|
|
284
|
+
)
|
|
285
|
+
if value is not None
|
|
286
|
+
}
|
|
287
|
+
return entry, warnings
|
|
288
|
+
if ref.operation_id:
|
|
289
|
+
entry: dict[str, Any] = {"operationId": ref.operation_id}
|
|
290
|
+
if spec is not None:
|
|
291
|
+
found = [(p, m) for p, m, op_id in spec_operations(spec) if op_id == ref.operation_id]
|
|
292
|
+
if not found:
|
|
293
|
+
raise ApiCommandError(
|
|
294
|
+
f"operationId {ref.operation_id} is not in {spec_name} (the API's openapi "
|
|
295
|
+
"spec); check the id, or update the spec"
|
|
296
|
+
)
|
|
297
|
+
spec_path, spec_method = found[0]
|
|
298
|
+
if wanted and wanted != [spec_method]:
|
|
299
|
+
raise ApiCommandError(
|
|
300
|
+
f"{ref.operation_id} is {spec_method} {spec_path} in {spec_name}, not "
|
|
301
|
+
f"{', '.join(wanted)}"
|
|
302
|
+
)
|
|
303
|
+
if ref.path is not None and not path_matches(spec_path, ref.path):
|
|
304
|
+
raise ApiCommandError(
|
|
305
|
+
f"{ref.operation_id} is {spec_method} {spec_path} in {spec_name}, not "
|
|
306
|
+
f"{ref.path}"
|
|
307
|
+
)
|
|
308
|
+
entry["path"] = spec_path
|
|
309
|
+
entry["methods"] = [spec_method]
|
|
310
|
+
return entry, warnings
|
|
311
|
+
if ref.path is not None:
|
|
312
|
+
entry["path"] = ref.path
|
|
313
|
+
if wanted:
|
|
314
|
+
entry["methods"] = wanted
|
|
315
|
+
return entry, warnings
|
|
316
|
+
assert ref.method is not None and ref.path is not None
|
|
317
|
+
entry = {"path": ref.path, "methods": wanted}
|
|
318
|
+
if spec is not None:
|
|
319
|
+
known = {(p, m) for p, m, _ in spec_operations(spec)}
|
|
320
|
+
for method in wanted:
|
|
321
|
+
if not any(m == method and path_matches(p, ref.path) for p, m in known):
|
|
322
|
+
warnings.append(f"{method} {ref.path} is not in {spec_name}")
|
|
323
|
+
return entry, warnings
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _normalized(entry: Mapping[str, Any]) -> tuple[Any, ...]:
|
|
327
|
+
path = entry.get("path")
|
|
328
|
+
return (
|
|
329
|
+
entry.get("operationId"),
|
|
330
|
+
normalize_path(str(path)) if path is not None else None,
|
|
331
|
+
tuple(sorted(str(m).upper() for m in entry.get("methods") or [])),
|
|
332
|
+
entry.get(RPC_METHOD_KEY),
|
|
333
|
+
entry.get(A2A_OPERATION_KEY),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def same_entry(a: Mapping[str, Any], b: Mapping[str, Any]) -> bool:
|
|
338
|
+
return _normalized(a) == _normalized(b)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def describe_entry(entry: Mapping[str, Any]) -> str:
|
|
342
|
+
parts = []
|
|
343
|
+
if entry.get("operationId") is not None:
|
|
344
|
+
parts.append(str(entry["operationId"]))
|
|
345
|
+
methods = entry.get("methods")
|
|
346
|
+
parts.append(",".join(str(m).upper() for m in methods) if methods else "any method")
|
|
347
|
+
if entry.get("path") is not None:
|
|
348
|
+
parts.append(str(entry["path"]))
|
|
349
|
+
for key in (RPC_METHOD_KEY, A2A_OPERATION_KEY):
|
|
350
|
+
if entry.get(key) is not None:
|
|
351
|
+
parts.append(f"{key}={entry[key]}")
|
|
352
|
+
return " ".join(parts)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def matching_indexes(entries: Sequence[Mapping[str, Any]], ref: OperationRef) -> list[int]:
|
|
356
|
+
"""The entries ``ref`` names: by operationId, or by path covering the method; with
|
|
357
|
+
``rpc_method`` or ``a2a_operation``, the entries pinning those (and its operationId)."""
|
|
358
|
+
found = []
|
|
359
|
+
for index, entry in enumerate(entries):
|
|
360
|
+
if ref.rpc:
|
|
361
|
+
if all(
|
|
362
|
+
value is None or entry.get(key) == value
|
|
363
|
+
for key, value in ((*ref._rpc_fields(), ("operationId", ref.operation_id)))
|
|
364
|
+
):
|
|
365
|
+
found.append(index)
|
|
366
|
+
continue
|
|
367
|
+
if ref.operation_id is not None:
|
|
368
|
+
if entry.get("operationId") == ref.operation_id:
|
|
369
|
+
found.append(index)
|
|
370
|
+
continue
|
|
371
|
+
path = entry.get("path")
|
|
372
|
+
if path is None or normalize_path(str(path)) != normalize_path(str(ref.path)):
|
|
373
|
+
continue
|
|
374
|
+
methods = [str(m).upper() for m in entry.get("methods") or []]
|
|
375
|
+
if not methods or ref.method in methods:
|
|
376
|
+
found.append(index)
|
|
377
|
+
return found
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@dataclass
|
|
381
|
+
class Revocation:
|
|
382
|
+
"""One entry ``api revoke`` changes: removed, or only one of its methods removed."""
|
|
383
|
+
|
|
384
|
+
index: int
|
|
385
|
+
entry: dict[str, Any]
|
|
386
|
+
remaining_methods: list[str] | None # None: the whole entry goes
|
|
387
|
+
|
|
388
|
+
def describe(self) -> str:
|
|
389
|
+
if self.remaining_methods is None:
|
|
390
|
+
return f"remove {describe_entry(self.entry)}"
|
|
391
|
+
named = " ".join(
|
|
392
|
+
str(self.entry[key]) for key in ("operationId", "path") if self.entry.get(key)
|
|
393
|
+
)
|
|
394
|
+
return f"keep {named} for {', '.join(self.remaining_methods)} only"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def plan_revocations(
|
|
398
|
+
entries: Sequence[Mapping[str, Any]], ref: OperationRef, every_method: Sequence[str]
|
|
399
|
+
) -> list[Revocation]:
|
|
400
|
+
"""What revoking ``ref`` does to each matching entry, last entry first.
|
|
401
|
+
|
|
402
|
+
``--method M --path P`` takes M out of an entry and nothing else. An entry
|
|
403
|
+
without ``methods`` covers every method, ``every_method`` (the API's
|
|
404
|
+
allowed methods for an allowed entry, every HTTP method for a denial): it
|
|
405
|
+
keeps the others, listed explicitly, so revoking GET from a denial of every
|
|
406
|
+
method never lifts it for DELETE.
|
|
407
|
+
"""
|
|
408
|
+
plans = []
|
|
409
|
+
for index in reversed(matching_indexes(entries, ref)):
|
|
410
|
+
entry = dict(entries[index])
|
|
411
|
+
if ref.method is None:
|
|
412
|
+
plans.append(Revocation(index, entry, None))
|
|
413
|
+
continue
|
|
414
|
+
methods = [str(m).upper() for m in entry.get("methods") or []] or list(every_method)
|
|
415
|
+
remaining = [m for m in methods if m != ref.method]
|
|
416
|
+
plans.append(Revocation(index, entry, remaining or None))
|
|
417
|
+
return plans
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
# Limits
|
|
422
|
+
# ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def parse_limit(value: str | None, option: str) -> int | str | None:
|
|
426
|
+
"""``--max-calls-per-run 20`` -> 20, ``none`` -> None; not given -> ``"keep"``."""
|
|
427
|
+
if value is None:
|
|
428
|
+
return "keep"
|
|
429
|
+
if value.strip().lower() == "none":
|
|
430
|
+
return None
|
|
431
|
+
try:
|
|
432
|
+
number = int(value)
|
|
433
|
+
except ValueError:
|
|
434
|
+
raise click.UsageError(f"{option} takes an integer >= 1, or none") from None
|
|
435
|
+
if number < 1:
|
|
436
|
+
raise click.UsageError(f"{option} takes an integer >= 1, or none")
|
|
437
|
+
return number
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
LIMIT_KEYS = ("max_calls_per_run", "rate_per_minute", "max_response_bytes")
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def new_limits(
|
|
444
|
+
current: Mapping[str, Any] | None,
|
|
445
|
+
max_calls: int | str | None,
|
|
446
|
+
rate: int | str | None,
|
|
447
|
+
max_bytes: int | str | None = "keep",
|
|
448
|
+
) -> dict[str, int] | None:
|
|
449
|
+
"""The ``limits`` mapping after the change; None when no limit is left.
|
|
450
|
+
|
|
451
|
+
``"keep"`` leaves a limit as it is, None removes it; the other keys stay.
|
|
452
|
+
"""
|
|
453
|
+
limits = dict(current or {})
|
|
454
|
+
for key, value in zip(LIMIT_KEYS, (max_calls, rate, max_bytes), strict=True):
|
|
455
|
+
if value == "keep":
|
|
456
|
+
continue
|
|
457
|
+
if value is None:
|
|
458
|
+
limits.pop(key, None)
|
|
459
|
+
else:
|
|
460
|
+
limits[key] = value
|
|
461
|
+
ordered = {k: limits[k] for k in LIMIT_KEYS if k in limits}
|
|
462
|
+
return ordered or None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def describe_limits(limits: Mapping[str, Any] | None) -> str:
|
|
466
|
+
if not limits:
|
|
467
|
+
return "none"
|
|
468
|
+
parts = []
|
|
469
|
+
if "max_calls_per_run" in limits:
|
|
470
|
+
parts.append(f"{limits['max_calls_per_run']} call(s) per run")
|
|
471
|
+
if "rate_per_minute" in limits:
|
|
472
|
+
parts.append(f"{limits['rate_per_minute']} per minute per replica")
|
|
473
|
+
if "max_response_bytes" in limits:
|
|
474
|
+
parts.append(f"answers of at most {limits['max_response_bytes']} bytes")
|
|
475
|
+
return ", ".join(parts)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def limits_widen(before: Mapping[str, Any] | None, after: Mapping[str, Any] | None) -> bool:
|
|
479
|
+
"""True when a limit is removed or raised."""
|
|
480
|
+
before, after = before or {}, after or {}
|
|
481
|
+
return any(key not in after or after[key] > before[key] for key in before)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
# Specs
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def load_spec(root: Path, reference: str) -> dict[str, Any]:
|
|
490
|
+
"""The OpenAPI spec an API names (relative to the project root); exit 3 when unreadable."""
|
|
491
|
+
from graph_agents_cli.dev.policy_check import load_openapi
|
|
492
|
+
|
|
493
|
+
path = Path(reference)
|
|
494
|
+
if not path.is_absolute():
|
|
495
|
+
path = root / path
|
|
496
|
+
try:
|
|
497
|
+
return load_openapi(path)
|
|
498
|
+
except (OSError, ValueError, yaml.YAMLError) as exc:
|
|
499
|
+
raise ApiCommandError(f"cannot read the OpenAPI spec {reference}: {exc}") from exc
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def with_api(document: Mapping[str, Any] | None, name: str, api: Mapping[str, Any]) -> dict:
|
|
503
|
+
new = copy.deepcopy(dict(document)) if document else {"apis": {}}
|
|
504
|
+
new["apis"] = dict(new.get("apis") or {})
|
|
505
|
+
new["apis"][name] = copy.deepcopy(dict(api))
|
|
506
|
+
return new
|