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,400 @@
|
|
|
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
|
+
"""Client side of the human approval gate: the paused call, shown safely, and how to decide it.
|
|
16
|
+
|
|
17
|
+
An API's ``approval`` block in ``api-policy.yaml`` makes the agent pause
|
|
18
|
+
before sending a gated call. The chat API then ends the run's stream with
|
|
19
|
+
``message.end`` whose ``status`` is ``awaiting_approval`` and whose
|
|
20
|
+
``approval`` describes the call (``approval_id``, ``api``, ``method``,
|
|
21
|
+
``path``, ``query``, ``body``, ``operation_id``, ``reason``, ``approvers``,
|
|
22
|
+
``expires_at``). ``GET /threads/{thread_id}/approvals`` lists a thread's
|
|
23
|
+
approvals and ``POST /threads/{thread_id}/approvals/{approval_id}`` decides
|
|
24
|
+
one. ``run``, ``approvals`` and ``eval generate`` share this module. A decision
|
|
25
|
+
this agent relays to another agent carries ``effect`` (the call that will
|
|
26
|
+
actually happen there) and ``nested`` (the approvals it passes through): they
|
|
27
|
+
are printed first.
|
|
28
|
+
|
|
29
|
+
Everything in an approval except its id came, one way or another, from the
|
|
30
|
+
model (the path's ids, the body, the stated reason), and the model may have
|
|
31
|
+
read text an attacker wrote. A human decides from what is printed here, so
|
|
32
|
+
it is printed in full (never cut) and with every control, format and
|
|
33
|
+
separator character escaped: no text can move the cursor, recolour the
|
|
34
|
+
screen, reorder characters or fake a line of this output.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import re
|
|
41
|
+
import shlex
|
|
42
|
+
import unicodedata
|
|
43
|
+
from collections.abc import Mapping, Sequence
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
from graph_agents_cli._api_policy import (
|
|
48
|
+
HTTP_METHODS,
|
|
49
|
+
REQUESTER_APPROVER,
|
|
50
|
+
path_matches,
|
|
51
|
+
path_template_problem,
|
|
52
|
+
)
|
|
53
|
+
from graph_agents_cli._chat_client import DECISION_APPROVE, DECISION_REJECT
|
|
54
|
+
|
|
55
|
+
# Approval statuses the server reports.
|
|
56
|
+
STATUS_PENDING = "pending"
|
|
57
|
+
STATUS_APPROVED = "approved"
|
|
58
|
+
STATUS_REJECTED = "rejected"
|
|
59
|
+
STATUS_EXPIRED = "expired"
|
|
60
|
+
|
|
61
|
+
# What an eval case's `approvals` instructions and `expect.approvals` match a gate on.
|
|
62
|
+
MATCH_KEYS = ("api", "operation_id", "method", "path")
|
|
63
|
+
|
|
64
|
+
# The refusals of `POST /threads/{thread_id}/approvals/{approval_id}`.
|
|
65
|
+
DECISION_REFUSALS: dict[int, str] = {
|
|
66
|
+
403: (
|
|
67
|
+
"you are not an allowed approver of this call. requester is the principal who "
|
|
68
|
+
"started the run; role:<name> is any other principal holding that role (a requester "
|
|
69
|
+
"decides their own call only when requester is listed)"
|
|
70
|
+
),
|
|
71
|
+
404: "no such approval on that thread (a wrong id, or the thread was deleted)",
|
|
72
|
+
409: "it is not pending any more: it was already decided (an approval is single-use)",
|
|
73
|
+
410: "it expired, which rejected the call: nothing was sent; ask the agent again",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_ESCAPED_CATEGORIES = frozenset({"Cc", "Cf", "Zl", "Zp", "Cs", "Co", "Cn"})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def safe_text(value: Any) -> str:
|
|
80
|
+
"""``value`` as one line of terminal-safe text.
|
|
81
|
+
|
|
82
|
+
Every control (C0, DEL, C1), format (bidi overrides, zero-width marks),
|
|
83
|
+
line/paragraph separator, surrogate, private-use or unassigned character is
|
|
84
|
+
written as its ``\\x``/``\\u``/``\\U`` escape, so what the terminal shows
|
|
85
|
+
is exactly what the value holds.
|
|
86
|
+
"""
|
|
87
|
+
text = value if isinstance(value, str) else str(value)
|
|
88
|
+
out: list[str] = []
|
|
89
|
+
for char in text:
|
|
90
|
+
if unicodedata.category(char) in _ESCAPED_CATEGORIES:
|
|
91
|
+
code = ord(char)
|
|
92
|
+
if code <= 0xFF:
|
|
93
|
+
out.append(f"\\x{code:02x}")
|
|
94
|
+
elif code <= 0xFFFF:
|
|
95
|
+
out.append(f"\\u{code:04x}")
|
|
96
|
+
else:
|
|
97
|
+
out.append(f"\\U{code:08x}")
|
|
98
|
+
else:
|
|
99
|
+
out.append(char)
|
|
100
|
+
return "".join(out)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# What streamed agent text may not send to the terminal: C0 controls but tab and
|
|
104
|
+
# newline (ESC starts every cursor, colour and conceal sequence), DEL, C1 controls
|
|
105
|
+
# (0x9b is a one-byte CSI) and the bidi embedding, override and isolate marks.
|
|
106
|
+
_TERMINAL_CONTROLS = re.compile("[\\x00-\\x08\\x0b-\\x1f\\x7f-\\x9f\\u202a-\\u202e\\u2066-\\u2069]")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def terminal_text(text: str) -> str:
|
|
110
|
+
"""Streamed agent text with the characters that steer a terminal escaped.
|
|
111
|
+
|
|
112
|
+
Line breaks and tabs stay (a reply has paragraphs), so the text reads as
|
|
113
|
+
written; nothing in it can hide, recolour or overwrite the lines around
|
|
114
|
+
it, such as the approval shown before "Approve? [y/N]".
|
|
115
|
+
"""
|
|
116
|
+
return _TERMINAL_CONTROLS.sub(lambda m: safe_text(m.group(0)), text)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def json_lines(value: Any) -> list[str]:
|
|
120
|
+
"""``value`` as indented JSON, one terminal-safe line per list item.
|
|
121
|
+
|
|
122
|
+
``json.dumps`` escapes the newlines inside strings, so the line breaks left
|
|
123
|
+
are the ones indentation adds; each line is then passed through ``safe_text``.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
text = json.dumps(value, indent=2, ensure_ascii=False, sort_keys=True, default=str)
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
text = repr(value)
|
|
129
|
+
return [safe_text(line) for line in text.split("\n")]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _opt_str(value: Any) -> str | None:
|
|
133
|
+
return value if isinstance(value, str) and value else None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class Approval:
|
|
138
|
+
"""One approval: the paused call and who may decide it."""
|
|
139
|
+
|
|
140
|
+
approval_id: str
|
|
141
|
+
thread_id: str | None = None
|
|
142
|
+
api: str | None = None
|
|
143
|
+
method: str | None = None
|
|
144
|
+
path: str | None = None
|
|
145
|
+
query: Any = None
|
|
146
|
+
body: Any = None
|
|
147
|
+
operation_id: str | None = None
|
|
148
|
+
reason: str | None = None
|
|
149
|
+
approvers: tuple[str, ...] = ()
|
|
150
|
+
expires_at: str | None = None
|
|
151
|
+
# pending / approved / rejected / expired (listings); None in a paused run's payload.
|
|
152
|
+
status: str | None = None
|
|
153
|
+
decided_at: str | None = None
|
|
154
|
+
comment: str | None = None
|
|
155
|
+
raw: Mapping[str, Any] = field(default_factory=dict, compare=False, repr=False)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def from_payload(cls, payload: Any, *, thread_id: str | None = None) -> Approval | None:
|
|
159
|
+
"""An approval from the server's JSON (a paused run's or a listing row); None without an id."""
|
|
160
|
+
if not isinstance(payload, Mapping):
|
|
161
|
+
return None
|
|
162
|
+
approval_id = _opt_str(payload.get("approval_id")) or _opt_str(payload.get("id"))
|
|
163
|
+
if approval_id is None:
|
|
164
|
+
return None
|
|
165
|
+
approvers = payload.get("approvers")
|
|
166
|
+
method = _opt_str(payload.get("method"))
|
|
167
|
+
return cls(
|
|
168
|
+
approval_id=approval_id,
|
|
169
|
+
thread_id=_opt_str(payload.get("thread_id")) or thread_id,
|
|
170
|
+
api=_opt_str(payload.get("api")),
|
|
171
|
+
method=method.upper() if method else None,
|
|
172
|
+
path=_opt_str(payload.get("path")),
|
|
173
|
+
query=payload.get("query"),
|
|
174
|
+
body=payload.get("body"),
|
|
175
|
+
operation_id=_opt_str(payload.get("operation_id")),
|
|
176
|
+
reason=_opt_str(payload.get("reason")),
|
|
177
|
+
approvers=tuple(str(a) for a in approvers) if isinstance(approvers, list) else (),
|
|
178
|
+
expires_at=_opt_str(payload.get("expires_at")),
|
|
179
|
+
status=_opt_str(payload.get("status")),
|
|
180
|
+
decided_at=_opt_str(payload.get("decided_at")),
|
|
181
|
+
comment=_opt_str(payload.get("comment")),
|
|
182
|
+
raw=dict(payload),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def pending(self) -> bool:
|
|
187
|
+
"""True unless a listing says it was decided or expired."""
|
|
188
|
+
return self.status in (None, STATUS_PENDING)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def requester_may_decide(self) -> bool:
|
|
192
|
+
"""Whether the principal who started the run is one of the approvers."""
|
|
193
|
+
return REQUESTER_APPROVER in self.approvers
|
|
194
|
+
|
|
195
|
+
def call_line(self) -> str:
|
|
196
|
+
"""``POST /orders/ORD-1/cancel (api orders, operation cancelOrder)``, terminal-safe."""
|
|
197
|
+
call = f"{safe_text(self.method or '?')} {safe_text(self.path or '?')}"
|
|
198
|
+
details = []
|
|
199
|
+
if self.api:
|
|
200
|
+
details.append(f"api {safe_text(self.api)}")
|
|
201
|
+
if self.operation_id:
|
|
202
|
+
details.append(f"operation {safe_text(self.operation_id)}")
|
|
203
|
+
return f"{call} ({', '.join(details)})" if details else call
|
|
204
|
+
|
|
205
|
+
def effect_lines(self) -> list[str]:
|
|
206
|
+
"""What a relayed decision will do (``effect``) and through which approvals (``nested``).
|
|
207
|
+
|
|
208
|
+
``orders (via billing) will POST /orders/ORD-1002/cancel (cancelOrder), as reported
|
|
209
|
+
by orders``, its body, and one ``via`` line per agent; empty for any other approval.
|
|
210
|
+
"""
|
|
211
|
+
effect = self.raw.get("effect")
|
|
212
|
+
if not isinstance(effect, Mapping):
|
|
213
|
+
return []
|
|
214
|
+
via = [str(v) for v in effect.get("via") or [] if v]
|
|
215
|
+
agent = str(effect.get("agent") or "?")
|
|
216
|
+
hops = via[:-1] if via and via[-1] == agent else via
|
|
217
|
+
who = safe_text(agent) + (f" (via {', '.join(safe_text(h) for h in hops)})" if hops else "")
|
|
218
|
+
what = f"{safe_text(effect.get('method') or '?')} {safe_text(effect.get('path') or '?')}"
|
|
219
|
+
if effect.get("operation_id"):
|
|
220
|
+
what += f" ({safe_text(effect['operation_id'])})"
|
|
221
|
+
reported = f", as reported by {safe_text(via[-1])}" if via else ""
|
|
222
|
+
lines = [f" effect: {who} will {what}{reported}"]
|
|
223
|
+
for key in ("query", "body"):
|
|
224
|
+
if effect.get(key) not in (None, {}, [], ""):
|
|
225
|
+
shown = json_lines(effect[key])
|
|
226
|
+
lines.append(f" {key + ':':<11}{shown[0]}")
|
|
227
|
+
lines.extend(f" {line}" for line in shown[1:])
|
|
228
|
+
if effect.get("expires_at"):
|
|
229
|
+
lines.append(
|
|
230
|
+
f" expires: {safe_text(effect['expires_at'])} (at {safe_text(agent)})"
|
|
231
|
+
)
|
|
232
|
+
level, depth = self.raw.get("nested"), 0
|
|
233
|
+
while isinstance(level, Mapping) and depth < 8:
|
|
234
|
+
decided = safe_text(level.get("decide_with") or "?")
|
|
235
|
+
lines.append(
|
|
236
|
+
f" via: {safe_text(level.get('agent') or '?')} approval "
|
|
237
|
+
f"{safe_text(level.get('approval_id') or '?')} (decided there: {decided})"
|
|
238
|
+
)
|
|
239
|
+
level, depth = level.get("nested"), depth + 1
|
|
240
|
+
return lines
|
|
241
|
+
|
|
242
|
+
def lines(self) -> list[str]:
|
|
243
|
+
"""The approval in full, as the human deciding it must see it (what it will do first)."""
|
|
244
|
+
lines = [*self.effect_lines(), f" call: {self.call_line()}"]
|
|
245
|
+
if self.query not in (None, {}, [], ""):
|
|
246
|
+
query = json_lines(self.query)
|
|
247
|
+
lines.append(f" query: {query[0]}")
|
|
248
|
+
lines.extend(f" {line}" for line in query[1:])
|
|
249
|
+
if self.body is not None:
|
|
250
|
+
body = json_lines(self.body)
|
|
251
|
+
lines.append(f" body: {body[0]}")
|
|
252
|
+
lines.extend(f" {line}" for line in body[1:])
|
|
253
|
+
else:
|
|
254
|
+
lines.append(" body: (none)")
|
|
255
|
+
if self.reason:
|
|
256
|
+
lines.append(f" reason: {safe_text(self.reason)}")
|
|
257
|
+
approvers = ", ".join(safe_text(a) for a in self.approvers) or "(not stated)"
|
|
258
|
+
lines.append(f" approvers: {approvers}")
|
|
259
|
+
if self.expires_at:
|
|
260
|
+
lines.append(f" expires at: {safe_text(self.expires_at)}")
|
|
261
|
+
if self.status:
|
|
262
|
+
status = safe_text(self.status)
|
|
263
|
+
if self.decided_at:
|
|
264
|
+
status += f" at {safe_text(self.decided_at)}"
|
|
265
|
+
lines.append(f" status: {status}")
|
|
266
|
+
if self.comment:
|
|
267
|
+
lines.append(f" comment: {safe_text(self.comment)}")
|
|
268
|
+
lines.append(f" approval id: {safe_text(self.approval_id)}")
|
|
269
|
+
if self.thread_id:
|
|
270
|
+
lines.append(f" thread: {safe_text(self.thread_id)}")
|
|
271
|
+
return lines
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# The marker `run` puts where it leaves a credential out of a printed command.
|
|
275
|
+
REDACTED_MARK = "<redacted>"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def decide_commands(
|
|
279
|
+
approval: Approval,
|
|
280
|
+
thread_id: str,
|
|
281
|
+
flags: str = "",
|
|
282
|
+
*,
|
|
283
|
+
verbs: Sequence[str] = ("approve",),
|
|
284
|
+
options: str = "",
|
|
285
|
+
) -> list[str]:
|
|
286
|
+
"""The ``graph-agents-cli approvals approve|reject ...`` commands that decide ``approval``.
|
|
287
|
+
|
|
288
|
+
Ids come from the server: they are shell-quoted so a pasted command runs
|
|
289
|
+
exactly one ``graph-agents-cli`` invocation, and an id that starts with
|
|
290
|
+
``-`` goes after ``--`` so it cannot be read as an option (``--url=...``
|
|
291
|
+
would send the decision elsewhere). ``flags`` (``--url``, headers with
|
|
292
|
+
credentials redacted) and ``options`` (``--comment ...``) start with a
|
|
293
|
+
space when not empty.
|
|
294
|
+
"""
|
|
295
|
+
approval_id = shlex.quote(approval.approval_id)
|
|
296
|
+
where = f"--thread-id {shlex.quote(thread_id)}{flags}{options}"
|
|
297
|
+
commands = []
|
|
298
|
+
for verb in verbs:
|
|
299
|
+
if approval.approval_id.startswith("-"):
|
|
300
|
+
commands.append(f"graph-agents-cli approvals {verb} {where} -- {approval_id}")
|
|
301
|
+
else:
|
|
302
|
+
commands.append(f"graph-agents-cli approvals {verb} {approval_id} {where}")
|
|
303
|
+
return commands
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def awaiting_lines(approval: Approval, thread_id: str | None, flags: str = "") -> list[str]:
|
|
307
|
+
"""What a paused run prints: who decides, and the exact commands to decide it."""
|
|
308
|
+
who = ", ".join(safe_text(a) for a in approval.approvers) or "an allowed approver"
|
|
309
|
+
lines = [
|
|
310
|
+
f"Awaiting approval by {who}: the call was not sent; the run is paused until it is decided."
|
|
311
|
+
]
|
|
312
|
+
if thread_id:
|
|
313
|
+
(approve,) = decide_commands(approval, thread_id, flags, verbs=(DECISION_APPROVE,))
|
|
314
|
+
(reject,) = decide_commands(
|
|
315
|
+
approval, thread_id, flags, verbs=(DECISION_REJECT,), options=' --comment "<why>"'
|
|
316
|
+
)
|
|
317
|
+
lines.append(f" Approve: {approve}")
|
|
318
|
+
lines.append(f" Reject: {reject}")
|
|
319
|
+
if REDACTED_MARK in flags:
|
|
320
|
+
lines.append(" (re-supply the redacted credential values)")
|
|
321
|
+
if not approval.approvers:
|
|
322
|
+
lines.append(
|
|
323
|
+
" The server did not say who may decide it: an allowed approver runs these "
|
|
324
|
+
"commands with their own credentials."
|
|
325
|
+
)
|
|
326
|
+
elif not approval.requester_may_decide:
|
|
327
|
+
lines.append(
|
|
328
|
+
" Only another principal can decide it (requester is not an approver): send "
|
|
329
|
+
"them these commands; they run them with their own credentials."
|
|
330
|
+
)
|
|
331
|
+
if approval.expires_at:
|
|
332
|
+
lines.append(f" It expires (rejected, nothing sent) at {safe_text(approval.expires_at)}.")
|
|
333
|
+
return lines
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Eval: matching a gate
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def match_problem(match: Any) -> str | None:
|
|
342
|
+
"""Why an eval ``match`` object cannot name a gated call, or None.
|
|
343
|
+
|
|
344
|
+
``{"operation_id": ...}`` or ``{"method": ..., "path": ...}`` (a path
|
|
345
|
+
template: ``{name}`` matches one segment), or all three; ``api`` narrows
|
|
346
|
+
it to one API.
|
|
347
|
+
"""
|
|
348
|
+
if not isinstance(match, Mapping) or not match:
|
|
349
|
+
return 'must be an object like {"operation_id": "..."} or {"method": "...", "path": "..."}'
|
|
350
|
+
unknown = sorted(set(match) - set(MATCH_KEYS), key=str)
|
|
351
|
+
if unknown:
|
|
352
|
+
return f"unknown key(s) {', '.join(map(str, unknown))} (known: {', '.join(MATCH_KEYS)})"
|
|
353
|
+
for key in MATCH_KEYS:
|
|
354
|
+
if key in match and not (isinstance(match[key], str) and match[key].strip()):
|
|
355
|
+
return f"{key} must be a non-empty string"
|
|
356
|
+
has_endpoint = "method" in match and "path" in match
|
|
357
|
+
if "operation_id" not in match and not has_endpoint:
|
|
358
|
+
return "needs operation_id, or method and path"
|
|
359
|
+
if ("method" in match) != ("path" in match):
|
|
360
|
+
return "method and path go together"
|
|
361
|
+
if "method" in match and match["method"].upper() not in HTTP_METHODS:
|
|
362
|
+
return f"unknown HTTP method {match['method']!r} (allowed: {', '.join(HTTP_METHODS)})"
|
|
363
|
+
if "path" in match:
|
|
364
|
+
problem = path_template_problem(match["path"])
|
|
365
|
+
if problem:
|
|
366
|
+
return f"path {problem}"
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def call_matches(match: Mapping[str, Any], call: Mapping[str, Any] | Approval) -> bool:
|
|
371
|
+
"""Whether a valid ``match`` names the call: every key it gives must agree."""
|
|
372
|
+
if isinstance(call, Approval):
|
|
373
|
+
call = {
|
|
374
|
+
"api": call.api,
|
|
375
|
+
"operation_id": call.operation_id,
|
|
376
|
+
"method": call.method,
|
|
377
|
+
"path": call.path,
|
|
378
|
+
}
|
|
379
|
+
if "api" in match and call.get("api") != match["api"]:
|
|
380
|
+
return False
|
|
381
|
+
if "operation_id" in match and call.get("operation_id") != match["operation_id"]:
|
|
382
|
+
return False
|
|
383
|
+
if "method" in match and str(call.get("method") or "").upper() != match["method"].upper():
|
|
384
|
+
return False
|
|
385
|
+
if "path" in match:
|
|
386
|
+
path = call.get("path")
|
|
387
|
+
if not isinstance(path, str) or not path_matches(match["path"], path):
|
|
388
|
+
return False
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def describe_match(match: Mapping[str, Any]) -> str:
|
|
393
|
+
"""``cancelOrder`` / ``POST /orders/{id}/cancel`` / ``orders: cancelOrder POST /...``."""
|
|
394
|
+
parts = []
|
|
395
|
+
if match.get("operation_id"):
|
|
396
|
+
parts.append(str(match["operation_id"]))
|
|
397
|
+
if match.get("method"):
|
|
398
|
+
parts.append(f"{str(match['method']).upper()} {match.get('path')}")
|
|
399
|
+
text = " ".join(parts)
|
|
400
|
+
return f"{match['api']}: {text}" if match.get("api") else text
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
"""Which build of graph-agents-cli is running.
|
|
16
|
+
|
|
17
|
+
A release and the builds made between two releases share one version string,
|
|
18
|
+
so the version alone cannot tell them apart. Every wheel therefore records the
|
|
19
|
+
source it was built from in ``graph_agents_cli/_build_info.json``, written by
|
|
20
|
+
``hatch_build.py`` at the repository root: the git commit, whether the files
|
|
21
|
+
that go into the wheel had uncommitted changes, and whether the commit carries
|
|
22
|
+
the release tag ``v<version>``. A source checkout (an editable install, ``uv
|
|
23
|
+
run`` in the repository) reads the same facts from git when asked.
|
|
24
|
+
|
|
25
|
+
The build id is the version for a release build, ``<version>+g<commit7>`` for
|
|
26
|
+
any other commit and ``<version>+g<commit7>.dirty`` when the tree had
|
|
27
|
+
uncommitted changes. A build whose source is unknown (built from a tree without
|
|
28
|
+
git and without the file) is identified by its version only and has no commit.
|
|
29
|
+
|
|
30
|
+
The build hook loads this module from the source tree by path, before the
|
|
31
|
+
package's dependencies exist, so it uses the standard library only.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import functools
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
import subprocess
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
PACKAGE_NAME = "graph-agents-cli"
|
|
45
|
+
BUILD_INFO_FILENAME = "_build_info.json"
|
|
46
|
+
BUILD_INFO_FORMAT = 1
|
|
47
|
+
# The paths whose content goes into the wheel (relative to the repository root):
|
|
48
|
+
# an uncommitted change under one of them makes the build "dirty".
|
|
49
|
+
SOURCE_PATHS = ("src", "pyproject.toml", "hatch_build.py")
|
|
50
|
+
_COMMIT_RE = re.compile(r"[0-9a-f]{40}([0-9a-f]{24})?") # SHA-1 or SHA-256 object names
|
|
51
|
+
_GIT_TIMEOUT_S = 15
|
|
52
|
+
# Variables that would point git at another repository than the one asked about.
|
|
53
|
+
_GIT_REDIRECTS = ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class BuildInfo:
|
|
58
|
+
"""The version plus the source facts of one build."""
|
|
59
|
+
|
|
60
|
+
version: str
|
|
61
|
+
commit: str | None = None
|
|
62
|
+
dirty: bool = False
|
|
63
|
+
release: bool = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def id(self) -> str:
|
|
67
|
+
"""``0.2.0`` (release or unknown source), ``0.2.0+g1a2b3c4`` or ``0.2.0+g1a2b3c4.dirty``."""
|
|
68
|
+
if self.commit is None or (self.release and not self.dirty):
|
|
69
|
+
return self.version
|
|
70
|
+
return f"{self.version}+g{self.commit[:7]}{'.dirty' if self.dirty else ''}"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_release(self) -> bool:
|
|
74
|
+
"""Built from the commit the release tag ``v<version>`` names, with no local change."""
|
|
75
|
+
return self.commit is not None and self.release and not self.dirty
|
|
76
|
+
|
|
77
|
+
def describe(self) -> str:
|
|
78
|
+
"""One line for ``info``: the id and where it comes from."""
|
|
79
|
+
if self.commit is None:
|
|
80
|
+
return f"{self.id} (source unknown: not built from a git checkout)"
|
|
81
|
+
if self.is_release:
|
|
82
|
+
return f"{self.id} (release, commit {self.commit[:12]})"
|
|
83
|
+
if self.dirty:
|
|
84
|
+
return f"{self.id} (commit {self.commit[:12]} with uncommitted changes)"
|
|
85
|
+
return f"{self.id} (commit {self.commit[:12]}, not a release)"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _git(root: Path, *args: str) -> str | None:
|
|
89
|
+
"""``git -C root <args>`` stdout (stripped), or None when git fails or is missing."""
|
|
90
|
+
env = {k: v for k, v in os.environ.items() if k not in _GIT_REDIRECTS}
|
|
91
|
+
env["GIT_OPTIONAL_LOCKS"] = "0" # a status must not rewrite the index
|
|
92
|
+
try:
|
|
93
|
+
proc = subprocess.run(
|
|
94
|
+
["git", "-C", str(root), *args],
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
timeout=_GIT_TIMEOUT_S,
|
|
98
|
+
env=env,
|
|
99
|
+
check=False,
|
|
100
|
+
)
|
|
101
|
+
except (OSError, subprocess.SubprocessError):
|
|
102
|
+
return None
|
|
103
|
+
if proc.returncode != 0:
|
|
104
|
+
return None
|
|
105
|
+
return proc.stdout.strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def git_facts(root: str | os.PathLike[str], version: str) -> dict[str, object] | None:
|
|
109
|
+
"""The build facts of the git checkout whose top level is ``root``; None when it is not one.
|
|
110
|
+
|
|
111
|
+
``dirty`` is true when a file under :data:`SOURCE_PATHS` differs from the
|
|
112
|
+
commit (untracked files included, ignored ones not); when git cannot say,
|
|
113
|
+
the build counts as dirty. ``release`` is true only for a clean tree whose
|
|
114
|
+
commit carries the tag ``v<version>``.
|
|
115
|
+
"""
|
|
116
|
+
root_path = Path(root).resolve()
|
|
117
|
+
top = _git(root_path, "rev-parse", "--show-toplevel")
|
|
118
|
+
if not top or Path(top).resolve() != root_path:
|
|
119
|
+
return None # not a checkout, or a directory inside another repository
|
|
120
|
+
commit = _git(root_path, "rev-parse", "--verify", "HEAD^{commit}")
|
|
121
|
+
if not commit or _COMMIT_RE.fullmatch(commit) is None:
|
|
122
|
+
return None
|
|
123
|
+
status = _git(root_path, "status", "--porcelain", "--untracked-files=all", "--", *SOURCE_PATHS)
|
|
124
|
+
dirty = status is None or bool(status)
|
|
125
|
+
tags = (_git(root_path, "tag", "--points-at", "HEAD") or "").split()
|
|
126
|
+
return {
|
|
127
|
+
"format": BUILD_INFO_FORMAT,
|
|
128
|
+
"version": version,
|
|
129
|
+
"commit": commit,
|
|
130
|
+
"dirty": dirty,
|
|
131
|
+
"release": not dirty and f"v{version}" in tags,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _from_facts(version: str, facts: object) -> BuildInfo | None:
|
|
136
|
+
if not isinstance(facts, dict):
|
|
137
|
+
return None
|
|
138
|
+
commit = facts.get("commit")
|
|
139
|
+
if not isinstance(commit, str) or _COMMIT_RE.fullmatch(commit) is None:
|
|
140
|
+
return None
|
|
141
|
+
dirty = facts.get("dirty") is not False # anything but an explicit false counts as dirty
|
|
142
|
+
return BuildInfo(
|
|
143
|
+
version=version,
|
|
144
|
+
commit=commit,
|
|
145
|
+
dirty=dirty,
|
|
146
|
+
release=facts.get("release") is True and not dirty,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _installed_version() -> str:
|
|
151
|
+
from importlib.metadata import PackageNotFoundError
|
|
152
|
+
from importlib.metadata import version as dist_version
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
return dist_version(PACKAGE_NAME)
|
|
156
|
+
except PackageNotFoundError:
|
|
157
|
+
return "0.0.0" # the unknown-version sentinel of scaffold.utils.version
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def build_of_package(package_dir: Path, version: str) -> BuildInfo:
|
|
161
|
+
"""The build of the package at ``package_dir``.
|
|
162
|
+
|
|
163
|
+
The recorded ``_build_info.json`` wins (an installed wheel); a package that
|
|
164
|
+
lives at ``<checkout>/src/graph_agents_cli`` reads the checkout's git facts;
|
|
165
|
+
anything else is a build of unknown source.
|
|
166
|
+
"""
|
|
167
|
+
info_file = package_dir / BUILD_INFO_FILENAME
|
|
168
|
+
if info_file.is_file():
|
|
169
|
+
try:
|
|
170
|
+
recorded = _from_facts(version, json.loads(info_file.read_text(encoding="utf-8")))
|
|
171
|
+
except (OSError, ValueError):
|
|
172
|
+
recorded = None
|
|
173
|
+
if recorded is not None:
|
|
174
|
+
return recorded
|
|
175
|
+
root = package_dir.parent.parent
|
|
176
|
+
if package_dir.parent.name == "src" and (root / "pyproject.toml").is_file():
|
|
177
|
+
from_git = _from_facts(version, git_facts(root, version))
|
|
178
|
+
if from_git is not None:
|
|
179
|
+
return from_git
|
|
180
|
+
return BuildInfo(version=version)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@functools.cache
|
|
184
|
+
def current_build() -> BuildInfo:
|
|
185
|
+
"""The running build (computed once per process)."""
|
|
186
|
+
return build_of_package(Path(__file__).resolve().parent, _installed_version())
|