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,497 @@
|
|
|
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
|
+
"""Agents calling this agent for a user: one agent never reaches or decides another's work.
|
|
16
|
+
|
|
17
|
+
This agent plays `orders`, behind the `jwt` policy, with a gated `cancelOrder`
|
|
18
|
+
call to a backend that records every request (`httpx.MockTransport`). A fake
|
|
19
|
+
issuer (`fake_issuer.py`: a JWKS and RFC 8693 token exchange with nested `act`)
|
|
20
|
+
mints alice's own token and exchanges it for the agents that call `orders` for
|
|
21
|
+
her: `concierge`, and `billing` (called by concierge). The tests send what
|
|
22
|
+
those agents would, with their exchanged tokens, over A2A JSON-RPC and HTTP.
|
|
23
|
+
|
|
24
|
+
* The concierge's request pauses at the gated cancel. Billing, acting for the
|
|
25
|
+
same user, gets GetTask -32001, ListTasks with no row, and its decisions are
|
|
26
|
+
refused; the backend receives no POST.
|
|
27
|
+
* With `decide_with: relayed` naming the concierge, the concierge delivers the
|
|
28
|
+
person's decision (naming the approval's digest): the call is sent once and
|
|
29
|
+
the approval records `decided_via`.
|
|
30
|
+
* With `decide_with: direct` (the default), the concierge's relay is refused
|
|
31
|
+
with `approval_direct_only`, and alice's own token decides.
|
|
32
|
+
* alice's own token reads, lists and cancels the concierge's task (canceled in
|
|
33
|
+
place: one row, the concierge's); continuing it stays the concierge's.
|
|
34
|
+
|
|
35
|
+
Every test runs with the in-memory checkpointer, and on Postgres when
|
|
36
|
+
`TEST_POSTGRES_DSN` is set.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
import json
|
|
43
|
+
import os
|
|
44
|
+
import uuid
|
|
45
|
+
from collections.abc import AsyncIterator, Iterator
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any
|
|
48
|
+
from urllib.parse import urlsplit
|
|
49
|
+
|
|
50
|
+
# The environment must be in place before the app (and the graph) is imported.
|
|
51
|
+
os.environ.update(
|
|
52
|
+
{
|
|
53
|
+
"MODEL_PROVIDER": "fake",
|
|
54
|
+
"MODEL_NAME": "fake",
|
|
55
|
+
"CHECKPOINTER": "memory",
|
|
56
|
+
"AUTH_POLICY": "shared-bearer",
|
|
57
|
+
"API_KEY": "test-key",
|
|
58
|
+
"APP_ENV": "dev",
|
|
59
|
+
"TRACING_ENABLED": "false",
|
|
60
|
+
"RUNTIME": "fastapi",
|
|
61
|
+
"APP_URL": "http://testserver",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
import httpx
|
|
66
|
+
import jwt
|
|
67
|
+
import pytest
|
|
68
|
+
from fake_issuer import FakeIssuer
|
|
69
|
+
from langchain.tools import ToolRuntime
|
|
70
|
+
from langchain_core.tools import tool
|
|
71
|
+
|
|
72
|
+
from {{cookiecutter.agent_directory}}.app_utils.api_client import get_client
|
|
73
|
+
from {{cookiecutter.agent_directory}}.app_utils.api_client import (
|
|
74
|
+
reset_policy_cache as reset_api_policy,
|
|
75
|
+
)
|
|
76
|
+
from {{cookiecutter.agent_directory}}.app_utils.auth import reset_policy_cache
|
|
77
|
+
from {{cookiecutter.agent_directory}}.fast_api_app import app
|
|
78
|
+
|
|
79
|
+
A2A_PATH = "/a2a/{{cookiecutter.agent_directory}}"
|
|
80
|
+
AUDIENCE = "orders"
|
|
81
|
+
PROMPT = "Cancel the order for 7"
|
|
82
|
+
SECRET = "s3cret"
|
|
83
|
+
|
|
84
|
+
POLICY = """
|
|
85
|
+
apis:
|
|
86
|
+
shop:
|
|
87
|
+
base_url_env: SHOP_API_BASE_URL
|
|
88
|
+
auth: none
|
|
89
|
+
allowed_methods: [GET, POST]
|
|
90
|
+
approval:
|
|
91
|
+
required_for:
|
|
92
|
+
operations:
|
|
93
|
+
- operationId: cancelOrder
|
|
94
|
+
path: /orders/{order_id}/cancel
|
|
95
|
+
methods: [POST]
|
|
96
|
+
approvers: [requester]
|
|
97
|
+
timeout_s: 60
|
|
98
|
+
"""
|
|
99
|
+
RELAYED = " decide_with: relayed\n relayers: [concierge]\n"
|
|
100
|
+
|
|
101
|
+
# What the backend received.
|
|
102
|
+
SENT: list[httpx.Request] = []
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _backend(request: httpx.Request) -> httpx.Response:
|
|
106
|
+
SENT.append(request)
|
|
107
|
+
return httpx.Response(200, json={"cancelled": request.url.path})
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@tool
|
|
111
|
+
async def cancel_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
|
|
112
|
+
"""Cancel an order by its id."""
|
|
113
|
+
context = getattr(runtime, "context", None)
|
|
114
|
+
client = get_client("shop", context=context, transport=httpx.MockTransport(_backend))
|
|
115
|
+
data = await client.post(
|
|
116
|
+
"/orders/{order_id}/cancel",
|
|
117
|
+
operation_id="cancelOrder",
|
|
118
|
+
path_params={"order_id": order_id},
|
|
119
|
+
json_body={"reason": "customer asked"},
|
|
120
|
+
)
|
|
121
|
+
return json.dumps(data)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.fixture(scope="module")
|
|
125
|
+
def issuer() -> Iterator[FakeIssuer]:
|
|
126
|
+
server = FakeIssuer(
|
|
127
|
+
{
|
|
128
|
+
"concierge": (SECRET, {"billing", AUDIENCE}),
|
|
129
|
+
"billing": (SECRET, {AUDIENCE}),
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
try:
|
|
133
|
+
yield server
|
|
134
|
+
finally:
|
|
135
|
+
server.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _exchange(issuer: FakeIssuer, client: str, subject_token: str, audience: str) -> str:
|
|
139
|
+
"""What `client` gets from the issuer for `subject_token` (RFC 8693), for `audience`."""
|
|
140
|
+
answer = httpx.post(
|
|
141
|
+
issuer.token_url,
|
|
142
|
+
data={
|
|
143
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
144
|
+
"subject_token": subject_token,
|
|
145
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
146
|
+
"audience": audience,
|
|
147
|
+
},
|
|
148
|
+
auth=(client, SECRET),
|
|
149
|
+
timeout=10,
|
|
150
|
+
)
|
|
151
|
+
assert answer.status_code == 200, answer.text
|
|
152
|
+
return str(answer.json()["access_token"])
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.fixture
|
|
156
|
+
def tokens(issuer: FakeIssuer) -> dict[str, str]:
|
|
157
|
+
"""alice's own token at orders, and the tokens her agents present there for her."""
|
|
158
|
+
user = f"alice-{uuid.uuid4().hex[:8]}" # the A2A task store lives as long as the app
|
|
159
|
+
at_concierge = issuer.login(user, audience="concierge")
|
|
160
|
+
concierge = _exchange(issuer, "concierge", at_concierge, AUDIENCE)
|
|
161
|
+
at_billing = _exchange(issuer, "concierge", at_concierge, "billing")
|
|
162
|
+
billing = _exchange(issuer, "billing", at_billing, AUDIENCE) # act: billing <- concierge
|
|
163
|
+
return {
|
|
164
|
+
"alice": issuer.login(user, audience=AUDIENCE),
|
|
165
|
+
"concierge": concierge,
|
|
166
|
+
"billing": billing,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@pytest.fixture(params=["memory", "postgres"])
|
|
174
|
+
async def database(
|
|
175
|
+
request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
|
|
176
|
+
) -> AsyncIterator[str]:
|
|
177
|
+
if request.param == "memory":
|
|
178
|
+
yield "memory"
|
|
179
|
+
return
|
|
180
|
+
if not ADMIN_DSN:
|
|
181
|
+
pytest.skip("TEST_POSTGRES_DSN is not set")
|
|
182
|
+
import psycopg
|
|
183
|
+
|
|
184
|
+
name = f"gac_test_{uuid.uuid4().hex[:12]}"
|
|
185
|
+
async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
|
|
186
|
+
await admin.execute(f'CREATE DATABASE "{name}"')
|
|
187
|
+
monkeypatch.setenv("CHECKPOINTER", "postgres")
|
|
188
|
+
monkeypatch.setenv("POSTGRES_DSN", urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
|
|
189
|
+
yield "postgres"
|
|
190
|
+
async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
|
|
191
|
+
await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def _orders(
|
|
195
|
+
issuer: FakeIssuer,
|
|
196
|
+
tmp_path: Path,
|
|
197
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
198
|
+
use_test_tools: Any,
|
|
199
|
+
decide: str,
|
|
200
|
+
) -> AsyncIterator[httpx.AsyncClient]:
|
|
201
|
+
policy = tmp_path / "api-policy.yaml"
|
|
202
|
+
policy.write_text(POLICY + decide, encoding="utf-8")
|
|
203
|
+
for name, value in {
|
|
204
|
+
"API_POLICY_PATH": str(policy),
|
|
205
|
+
"SHOP_API_BASE_URL": "http://shop.test",
|
|
206
|
+
"AUTH_POLICY": "jwt",
|
|
207
|
+
"AUTH_JWT_JWKS_URL": issuer.jwks_url,
|
|
208
|
+
"AUTH_JWT_ISSUER": issuer.issuer,
|
|
209
|
+
"AUTH_JWT_AUDIENCE": AUDIENCE,
|
|
210
|
+
"AUTH_ALLOWED_ACTORS": "concierge,billing",
|
|
211
|
+
}.items():
|
|
212
|
+
monkeypatch.setenv(name, value)
|
|
213
|
+
for name in ("AUTH_JWT_PUBLIC_KEY", "AUTH_READ_ACROSS_ROLES", "TRACE_CAPTURE"):
|
|
214
|
+
monkeypatch.delenv(name, raising=False)
|
|
215
|
+
reset_policy_cache()
|
|
216
|
+
reset_api_policy()
|
|
217
|
+
SENT.clear()
|
|
218
|
+
async with app.router.lifespan_context(app):
|
|
219
|
+
use_test_tools(cancel_order)
|
|
220
|
+
transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
|
|
221
|
+
async with httpx.AsyncClient(
|
|
222
|
+
transport=transport, base_url="http://testserver", timeout=30
|
|
223
|
+
) as client:
|
|
224
|
+
started = asyncio.get_running_loop().time()
|
|
225
|
+
while (await client.get("/ready")).status_code != 200: # postgres: the schema
|
|
226
|
+
assert asyncio.get_running_loop().time() - started < 30, "not ready"
|
|
227
|
+
await asyncio.sleep(0.1)
|
|
228
|
+
yield client
|
|
229
|
+
reset_policy_cache()
|
|
230
|
+
reset_api_policy()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.fixture
|
|
234
|
+
async def relayed(
|
|
235
|
+
database: str,
|
|
236
|
+
issuer: FakeIssuer,
|
|
237
|
+
tmp_path: Path,
|
|
238
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
239
|
+
use_test_tools: Any,
|
|
240
|
+
) -> AsyncIterator[httpx.AsyncClient]:
|
|
241
|
+
"""orders, its cancel gate opted into `decide_with: relayed` by the concierge."""
|
|
242
|
+
async for client in _orders(issuer, tmp_path, monkeypatch, use_test_tools, RELAYED):
|
|
243
|
+
yield client
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.fixture
|
|
247
|
+
async def direct(
|
|
248
|
+
database: str,
|
|
249
|
+
issuer: FakeIssuer,
|
|
250
|
+
tmp_path: Path,
|
|
251
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
252
|
+
use_test_tools: Any,
|
|
253
|
+
) -> AsyncIterator[httpx.AsyncClient]:
|
|
254
|
+
"""orders, its cancel gate at the default `decide_with: direct`."""
|
|
255
|
+
async for client in _orders(issuer, tmp_path, monkeypatch, use_test_tools, ""):
|
|
256
|
+
yield client
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def _rpc(client: httpx.AsyncClient, token: str, method: str, params: dict) -> dict:
|
|
260
|
+
r = await client.post(
|
|
261
|
+
A2A_PATH,
|
|
262
|
+
json={"jsonrpc": "2.0", "id": uuid.uuid4().hex, "method": method, "params": params},
|
|
263
|
+
headers={"Authorization": f"Bearer {token}", "A2A-Version": "1.0"},
|
|
264
|
+
)
|
|
265
|
+
assert r.status_code == 200, r.text
|
|
266
|
+
return r.json()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _message(task: dict[str, Any] | None = None, **part: Any) -> dict[str, Any]:
|
|
270
|
+
fields = {"taskId": task["id"], "contextId": task["contextId"]} if task else {}
|
|
271
|
+
return {
|
|
272
|
+
"message": {
|
|
273
|
+
"messageId": uuid.uuid4().hex,
|
|
274
|
+
"role": "ROLE_USER",
|
|
275
|
+
"parts": [part],
|
|
276
|
+
**fields,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _approval_part(task: dict[str, Any]) -> dict[str, Any]:
|
|
282
|
+
data = next(p["data"] for p in task["status"]["message"]["parts"] if "data" in p)
|
|
283
|
+
assert data["type"] == "approval_request"
|
|
284
|
+
return data["approval"]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _note(task: dict[str, Any]) -> str:
|
|
288
|
+
return "".join(p.get("text", "") for p in task["status"]["message"]["parts"])
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def _paused_by_concierge(client: httpx.AsyncClient, tokens: dict[str, str]) -> Any:
|
|
292
|
+
sent = await _rpc(client, tokens["concierge"], "SendMessage", _message(text=PROMPT))
|
|
293
|
+
task = sent["result"]["task"]
|
|
294
|
+
assert task["status"]["state"] == "TASK_STATE_INPUT_REQUIRED", task
|
|
295
|
+
approval = _approval_part(task)
|
|
296
|
+
assert approval["path"] == "/orders/7/cancel" and approval["status"] == "pending"
|
|
297
|
+
assert approval["requester_actor"] == "concierge"
|
|
298
|
+
assert approval["digest"].startswith("sha256:")
|
|
299
|
+
assert SENT == []
|
|
300
|
+
return task, approval
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _decision(approval: dict[str, Any], **extra: Any) -> dict[str, Any]:
|
|
304
|
+
return {"data": {"approval_id": approval["approval_id"], "decision": "approve", **extra}}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
async def test_another_agent_for_the_same_user_cannot_reach_or_decide(
|
|
308
|
+
relayed: httpx.AsyncClient, tokens: dict[str, str]
|
|
309
|
+
) -> None:
|
|
310
|
+
client = relayed
|
|
311
|
+
task, approval = await _paused_by_concierge(client, tokens)
|
|
312
|
+
billing = tokens["billing"]
|
|
313
|
+
# Billing's token: the task is not found, not listed, not continued, not decided.
|
|
314
|
+
got = await _rpc(client, billing, "GetTask", {"id": task["id"]})
|
|
315
|
+
assert got["error"]["code"] == -32001
|
|
316
|
+
listed = await _rpc(client, billing, "ListTasks", {})
|
|
317
|
+
assert listed["result"].get("tasks", []) == []
|
|
318
|
+
decided = await _rpc(
|
|
319
|
+
client, billing, "SendMessage", _message(task, **_decision(approval, digest="x"))
|
|
320
|
+
)
|
|
321
|
+
assert decided["error"]["code"] == -32001
|
|
322
|
+
on_context = await _rpc(
|
|
323
|
+
client,
|
|
324
|
+
billing,
|
|
325
|
+
"SendMessage",
|
|
326
|
+
{"message": {**_message(**_decision(approval))["message"], "contextId": task["contextId"]}},
|
|
327
|
+
)
|
|
328
|
+
assert on_context["result"]["task"]["status"]["state"] == "TASK_STATE_FAILED"
|
|
329
|
+
assert "another principal" in _note(on_context["result"]["task"])
|
|
330
|
+
http = await client.post(
|
|
331
|
+
f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
|
|
332
|
+
json={"decision": "approve", "digest": approval["digest"]},
|
|
333
|
+
headers={"Authorization": f"Bearer {billing}"},
|
|
334
|
+
)
|
|
335
|
+
assert http.status_code == 403 and http.json()["code"] == "not_an_approver"
|
|
336
|
+
seen = await client.get(
|
|
337
|
+
f"/threads/{task['contextId']}/approvals", headers={"Authorization": f"Bearer {billing}"}
|
|
338
|
+
)
|
|
339
|
+
assert seen.status_code == 403
|
|
340
|
+
# The order is unchanged: the backend received nothing.
|
|
341
|
+
assert SENT == []
|
|
342
|
+
# The concierge's own relay, naming the digest the person approved, is sent once.
|
|
343
|
+
missing = await _rpc(
|
|
344
|
+
client, tokens["concierge"], "SendMessage", _message(task, **_decision(approval))
|
|
345
|
+
)
|
|
346
|
+
assert missing["result"]["task"]["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"
|
|
347
|
+
assert "approval_digest_mismatch" in _note(missing["result"]["task"])
|
|
348
|
+
assert SENT == []
|
|
349
|
+
done = await _rpc(
|
|
350
|
+
client,
|
|
351
|
+
tokens["concierge"],
|
|
352
|
+
"SendMessage",
|
|
353
|
+
_message(task, **_decision(approval, digest=approval["digest"])),
|
|
354
|
+
)
|
|
355
|
+
assert done["result"]["task"]["status"]["state"] == "TASK_STATE_COMPLETED", done
|
|
356
|
+
assert [(r.method, r.url.path) for r in SENT] == [("POST", "/orders/7/cancel")]
|
|
357
|
+
# The person sees who relayed it.
|
|
358
|
+
listed = await client.get(
|
|
359
|
+
f"/threads/{task['contextId']}/approvals",
|
|
360
|
+
headers={"Authorization": f"Bearer {tokens['alice']}"},
|
|
361
|
+
)
|
|
362
|
+
[record] = listed.json()
|
|
363
|
+
assert (record["status"], record["decided_via"], record["requester_actor"]) == (
|
|
364
|
+
"approved",
|
|
365
|
+
"concierge",
|
|
366
|
+
"concierge",
|
|
367
|
+
)
|
|
368
|
+
again = await _rpc(
|
|
369
|
+
client,
|
|
370
|
+
tokens["concierge"],
|
|
371
|
+
"SendMessage",
|
|
372
|
+
_message(task, **_decision(approval, digest=approval["digest"])),
|
|
373
|
+
)
|
|
374
|
+
assert "error" in again or again["result"]["task"]["status"]["state"] != "TASK_STATE_COMPLETED"
|
|
375
|
+
assert len(SENT) == 1
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async def test_under_direct_the_relay_is_refused_and_the_person_decides(
|
|
379
|
+
direct: httpx.AsyncClient, tokens: dict[str, str]
|
|
380
|
+
) -> None:
|
|
381
|
+
client = direct
|
|
382
|
+
task, approval = await _paused_by_concierge(client, tokens)
|
|
383
|
+
assert approval["decide_with"] == "direct"
|
|
384
|
+
relayed = await _rpc(
|
|
385
|
+
client,
|
|
386
|
+
tokens["concierge"],
|
|
387
|
+
"SendMessage",
|
|
388
|
+
_message(task, **_decision(approval, digest=approval["digest"])),
|
|
389
|
+
)
|
|
390
|
+
refused = relayed["result"]["task"]
|
|
391
|
+
assert refused["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"
|
|
392
|
+
assert "approval_direct_only" in _note(refused)
|
|
393
|
+
assert "not relayed by agent concierge" in _note(refused)
|
|
394
|
+
http = await client.post(
|
|
395
|
+
f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
|
|
396
|
+
json={"decision": "approve", "digest": approval["digest"]},
|
|
397
|
+
headers={"Authorization": f"Bearer {tokens['concierge']}"},
|
|
398
|
+
)
|
|
399
|
+
assert http.status_code == 403 and http.json()["code"] == "approval_direct_only"
|
|
400
|
+
assert SENT == []
|
|
401
|
+
# alice, at orders with her own token, sees the approval her agent's run asked for ...
|
|
402
|
+
seen = await client.get(
|
|
403
|
+
f"/threads/{task['contextId']}/approvals",
|
|
404
|
+
headers={"Authorization": f"Bearer {tokens['alice']}"},
|
|
405
|
+
)
|
|
406
|
+
assert [a["approval_id"] for a in seen.json()] == [approval["approval_id"]]
|
|
407
|
+
# ... and decides it; a digest from another view of the call is refused first.
|
|
408
|
+
stale = await client.post(
|
|
409
|
+
f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
|
|
410
|
+
json={"decision": "approve", "digest": "sha256:" + "0" * 64},
|
|
411
|
+
headers={"Authorization": f"Bearer {tokens['alice']}"},
|
|
412
|
+
)
|
|
413
|
+
assert stale.status_code == 409 and stale.json()["code"] == "approval_digest_mismatch"
|
|
414
|
+
decided = await client.post(
|
|
415
|
+
f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
|
|
416
|
+
json={"decision": "approve"},
|
|
417
|
+
headers={"Authorization": f"Bearer {tokens['alice']}"},
|
|
418
|
+
)
|
|
419
|
+
assert decided.status_code == 200, decided.text
|
|
420
|
+
assert "message.end" in decided.text
|
|
421
|
+
assert [(r.method, r.url.path) for r in SENT] == [("POST", "/orders/7/cancel")]
|
|
422
|
+
[record] = (
|
|
423
|
+
await client.get(
|
|
424
|
+
f"/threads/{task['contextId']}/approvals",
|
|
425
|
+
headers={"Authorization": f"Bearer {tokens['alice']}"},
|
|
426
|
+
)
|
|
427
|
+
).json()
|
|
428
|
+
assert (record["status"], record["decided_via"]) == ("approved", None)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async def test_an_unlisted_agent_is_refused_before_anything_runs(
|
|
432
|
+
direct: httpx.AsyncClient, tokens: dict[str, str], monkeypatch: pytest.MonkeyPatch
|
|
433
|
+
) -> None:
|
|
434
|
+
monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "concierge")
|
|
435
|
+
r = await direct.post(
|
|
436
|
+
A2A_PATH,
|
|
437
|
+
json={
|
|
438
|
+
"jsonrpc": "2.0",
|
|
439
|
+
"id": "1",
|
|
440
|
+
"method": "SendMessage",
|
|
441
|
+
"params": _message(text=PROMPT),
|
|
442
|
+
},
|
|
443
|
+
headers={"Authorization": f"Bearer {tokens['billing']}", "A2A-Version": "1.0"},
|
|
444
|
+
)
|
|
445
|
+
assert r.status_code == 403
|
|
446
|
+
assert r.json() == {
|
|
447
|
+
"detail": "Delegated caller billing is not allowed here (AUTH_ALLOWED_ACTORS)."
|
|
448
|
+
}
|
|
449
|
+
assert SENT == []
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
async def _task_rows(database: str, task_id: str) -> list[tuple[str, str]]:
|
|
453
|
+
"""(owner, state) of every stored row of `task_id` (Postgres); [] under memory."""
|
|
454
|
+
if database != "postgres":
|
|
455
|
+
return []
|
|
456
|
+
import psycopg
|
|
457
|
+
|
|
458
|
+
async with await psycopg.AsyncConnection.connect(os.environ["POSTGRES_DSN"]) as conn:
|
|
459
|
+
cursor = await conn.execute(
|
|
460
|
+
"SELECT owner, state FROM a2a_tasks WHERE task_id = %s ORDER BY owner", (task_id,)
|
|
461
|
+
)
|
|
462
|
+
return [(str(owner), str(state)) for owner, state in await cursor.fetchall()]
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
async def test_the_person_reads_lists_and_cancels_their_agents_task(
|
|
466
|
+
direct: httpx.AsyncClient, tokens: dict[str, str], database: str
|
|
467
|
+
) -> None:
|
|
468
|
+
"""The owner's decision of 2026-09-28: with their own token, the person reads, lists and
|
|
469
|
+
cancels the task the concierge started for them; continuing it stays the concierge's, and
|
|
470
|
+
billing (another agent for the same person) still sees nothing."""
|
|
471
|
+
client = direct
|
|
472
|
+
task, _approval = await _paused_by_concierge(client, tokens)
|
|
473
|
+
alice = tokens["alice"]
|
|
474
|
+
subject = jwt.decode(alice, options={"verify_signature": False})["sub"]
|
|
475
|
+
got = await _rpc(client, alice, "GetTask", {"id": task["id"]})
|
|
476
|
+
assert got["result"]["id"] == task["id"]
|
|
477
|
+
assert got["result"]["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"
|
|
478
|
+
listed = await _rpc(client, alice, "ListTasks", {})
|
|
479
|
+
assert [t["id"] for t in listed["result"]["tasks"]] == [task["id"]]
|
|
480
|
+
continued = await _rpc(client, alice, "SendMessage", _message(task, text="hello"))
|
|
481
|
+
assert continued["error"]["code"] == -32001
|
|
482
|
+
for method in ("GetTask", "CancelTask"):
|
|
483
|
+
assert (await _rpc(client, tokens["billing"], method, {"id": task["id"]}))["error"][
|
|
484
|
+
"code"
|
|
485
|
+
] == -32001
|
|
486
|
+
assert (await _rpc(client, tokens["billing"], "ListTasks", {}))["result"].get("tasks", []) == []
|
|
487
|
+
|
|
488
|
+
canceled = await _rpc(client, alice, "CancelTask", {"id": task["id"]})
|
|
489
|
+
assert canceled["result"]["status"]["state"] == "TASK_STATE_CANCELED", canceled
|
|
490
|
+
seen = await _rpc(client, tokens["concierge"], "GetTask", {"id": task["id"]})
|
|
491
|
+
assert seen["result"]["status"]["state"] == "TASK_STATE_CANCELED"
|
|
492
|
+
if database == "postgres":
|
|
493
|
+
# One row, the concierge's, canceled where it is: no copy under the person's key.
|
|
494
|
+
assert await _task_rows(database, task["id"]) == [
|
|
495
|
+
(f"{subject}\x1fconcierge", "TASK_STATE_CANCELED")
|
|
496
|
+
]
|
|
497
|
+
assert SENT == []
|