petfishframework 0.3.1__tar.gz → 0.3.2__tar.gz
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.
- {petfishframework-0.3.1 → petfishframework-0.3.2}/PKG-INFO +3 -3
- {petfishframework-0.3.1 → petfishframework-0.3.2}/README.md +2 -2
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/api.md +1 -1
- {petfishframework-0.3.1 → petfishframework-0.3.2}/pyproject.toml +1 -1
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/__init__.py +1 -1
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/environment.py +4 -1
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/session.py +16 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/credentials/broker.py +34 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/credentials/token.py +28 -3
- petfishframework-0.3.2/src/petfishframework/policies/conditions.py +436 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/policies/engine.py +4 -1
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/policies/rule.py +4 -1
- petfishframework-0.3.2/src/petfishframework/policies/validator.py +100 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/base.py +1 -1
- petfishframework-0.3.2/tests/test_credential_broker.py +231 -0
- petfishframework-0.3.2/tests/test_yaml_policy.py +650 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/uv.lock +5 -5
- petfishframework-0.3.1/src/petfishframework/policies/conditions.py +0 -176
- petfishframework-0.3.1/tests/test_credential_broker.py +0 -81
- petfishframework-0.3.1/tests/test_yaml_policy.py +0 -325
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.env.example +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.github/workflows/ci.yml +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.github/workflows/publish.yml +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.gitignore +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.sisyphus/plans/v0.1.6-plan.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.sisyphus/plans/v0.2.0-plan.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/.sisyphus/plans/v0.3.0-plan.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/AGENTS.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/AGENTS.md.new +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/CHANGELOG.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/CONTRIBUTING.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/LICENSE +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/SECURITY.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/conftest.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/architecture.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/benchmark-results.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/development-plan.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/development.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/enterprise-expense-demo.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_0_1_4_playground_recommendations.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_playground_report.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_production_readiness_roadmap.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_recommendations.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_v0_1_6_playground_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_v0_1_7_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_v0_1_8_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_v0_2_1_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/archives/petfishframework_v0_2_2_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/feedbacks/petfishframework_v0_3_0_review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/release-checklist.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/competitor-analysis/README.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/competitor-analysis/competitor-matrix.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/competitor-analysis/market-brief.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/competitor-analysis/positioning-map.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/competitor-analysis/swot-analysis.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/literature-review/inclusion-exclusion-criteria.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/literature-review/literature-matrix.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/literature-review/literature-review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/literature-review/search-strategy.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/research/reference-repos-absorption.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/skeleton-completeness-checklist.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/test-results-report.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/usage-guide.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/docs/validation-roadmap.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/initialization-report.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/mcp/README.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/mcp/connection-checklist.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/mcp/mcp-config.example.json +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/opencode.json +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/qa/code-review-checklist.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/qa/qa-review.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/qa/qc-gate-decision.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/qa/test-plan.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/config.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/agent.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/compiled.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/contracts.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/conversation.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/events.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/structured.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/core/types.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/credentials/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/mcp/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/mcp/client.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/mcp/server.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/mcp/stdio_transport.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/mcp/wrapper.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/models/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/models/anthropic.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/models/fake.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/models/openai.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/observability/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/observability/sinks.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/permissions/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/permissions/model.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/policies/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/policies/test_runner.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reasoning/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reasoning/lats.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reasoning/llm_plus_p.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reasoning/react.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/audit_report.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/cost.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/cost_report.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/pass_at_k.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/replay.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/retry.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/reliability/timeout.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/retrieval/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/retrieval/adaptive.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/retrieval/crag.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/retrieval/memory_store.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/agent_tool.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/calculator.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/path_planner.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/registry.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/tools/word_sorter.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tasks/backlog.md +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/integration/__init__.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/integration/test_anthropic_integration.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/integration/test_openai_integration.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/policies/enterprise-expense.tests.yaml +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_agent_tool.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_anthropic_adapter.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_api_agent_lifecycle.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_api_edge_cases.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_api_integration.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_api_public_surface.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_api_type_safety.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_async.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_audit_report_v2.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_conversation.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_credential_integration.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_degrade.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_degrade_failclosed.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_enterprise_demo.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_m2_m3_m4.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_mcp.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_openai_adapter.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_pass_at_k.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_permission_semantics.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_quickstart_smoke.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_replay.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_retrieval.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_retry.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_skeleton.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_stdio_transport.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_streaming.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_structured.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_tool_registry.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v016_semantics.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v018.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v019.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v2_interface_compatibility.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v2_lats.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_v2_llm_plus_p.py +0 -0
- {petfishframework-0.3.1 → petfishframework-0.3.2}/tests/test_yaml_policy_suite.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: petfishframework
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A lightweight runtime framework for reliable, auditable, budget-aware, and permission-aware AI agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/kylecui/petfishFramework
|
|
6
6
|
Project-URL: Documentation, https://github.com/kylecui/petfishFramework/tree/master/docs
|
|
@@ -34,7 +34,7 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|
[](https://www.python.org/downloads/)
|
|
36
36
|
[](https://github.com/kylecui/petfishFramework/blob/master/LICENSE)
|
|
37
|
-
[](https://github.com/kylecui/petfishFramework/tree/master/tests/)
|
|
38
38
|
|
|
39
39
|
**Status: Alpha** — API may change. Core runtime works; see [Roadmap](#roadmap).
|
|
40
40
|
|
|
@@ -279,7 +279,7 @@ petfishFramework is **Alpha**. API may change before v1.0.
|
|
|
279
279
|
git clone https://github.com/kylecui/petfishFramework.git
|
|
280
280
|
cd petfishFramework
|
|
281
281
|
uv sync --all-extras
|
|
282
|
-
uv run pytest #
|
|
282
|
+
uv run pytest # 284 tests
|
|
283
283
|
uv run ruff check src/ tests/
|
|
284
284
|
```
|
|
285
285
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](https://github.com/kylecui/petfishFramework/blob/master/LICENSE)
|
|
7
|
-
[](https://github.com/kylecui/petfishFramework/tree/master/tests/)
|
|
8
8
|
|
|
9
9
|
**Status: Alpha** — API may change. Core runtime works; see [Roadmap](#roadmap).
|
|
10
10
|
|
|
@@ -249,7 +249,7 @@ petfishFramework is **Alpha**. API may change before v1.0.
|
|
|
249
249
|
git clone https://github.com/kylecui/petfishFramework.git
|
|
250
250
|
cd petfishFramework
|
|
251
251
|
uv sync --all-extras
|
|
252
|
-
uv run pytest #
|
|
252
|
+
uv run pytest # 284 tests
|
|
253
253
|
uv run ruff check src/ tests/
|
|
254
254
|
```
|
|
255
255
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# petfishFramework API Reference
|
|
2
2
|
|
|
3
|
-
This document is the authoritative reference for the public API of `petfishFramework` v0.3.
|
|
3
|
+
This document is the authoritative reference for the public API of `petfishFramework` v0.3.2. Every signature, field, and example below is derived from the source code and from the tests that exercise it.
|
|
4
4
|
|
|
5
5
|
## 1. Overview
|
|
6
6
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "petfishframework"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "A lightweight runtime framework for reliable, auditable, budget-aware, and permission-aware AI agents."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -334,7 +334,10 @@ class RuntimeEnvironment(Environment):
|
|
|
334
334
|
return
|
|
335
335
|
if self.credential_broker is None:
|
|
336
336
|
return
|
|
337
|
-
|
|
337
|
+
credential_name = getattr(tool, "credential_name", None) or tool.name
|
|
338
|
+
token = self.credential_broker.issue_token(
|
|
339
|
+
credential_name, tool_name=tool.name, max_uses=1
|
|
340
|
+
)
|
|
338
341
|
args["_credential_token"] = token
|
|
339
342
|
|
|
340
343
|
def _prepare_tool_call(self, ref: ToolRef, args: dict) -> tuple[Tool | None, Any]:
|
|
@@ -133,6 +133,7 @@ class Session:
|
|
|
133
133
|
self._result = result
|
|
134
134
|
|
|
135
135
|
self._save_conversation_history(result)
|
|
136
|
+
self._cleanup_credentials()
|
|
136
137
|
|
|
137
138
|
self.events.emit(
|
|
138
139
|
"session.end",
|
|
@@ -150,6 +151,21 @@ class Session:
|
|
|
150
151
|
)
|
|
151
152
|
return result
|
|
152
153
|
|
|
154
|
+
def _cleanup_credentials(self) -> None:
|
|
155
|
+
"""Clean up scoped credentials at session end."""
|
|
156
|
+
broker = self.credential_broker
|
|
157
|
+
if broker is None:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
cleanup_expired = getattr(broker, "cleanup_expired", None)
|
|
161
|
+
if callable(cleanup_expired):
|
|
162
|
+
cleanup_expired()
|
|
163
|
+
|
|
164
|
+
revoke_all_for_tool = getattr(broker, "revoke_all_for_tool", None)
|
|
165
|
+
if callable(revoke_all_for_tool):
|
|
166
|
+
for tool in self.tools:
|
|
167
|
+
revoke_all_for_tool(tool.name)
|
|
168
|
+
|
|
153
169
|
def _save_conversation_history(self, result: Result) -> None:
|
|
154
170
|
"""Persist previous history plus the current user/assistant turn."""
|
|
155
171
|
if self.conversation_store is None or self.conversation_id is None:
|
{petfishframework-0.3.1 → petfishframework-0.3.2}/src/petfishframework/credentials/broker.py
RENAMED
|
@@ -28,6 +28,7 @@ class CredentialBroker:
|
|
|
28
28
|
name: str,
|
|
29
29
|
tool_name: str,
|
|
30
30
|
ttl_s: float | None = None,
|
|
31
|
+
max_uses: int = 0,
|
|
31
32
|
) -> ScopedToken:
|
|
32
33
|
"""Issue a scoped, time-limited token for a specific tool."""
|
|
33
34
|
if name not in self._credentials:
|
|
@@ -42,6 +43,7 @@ class CredentialBroker:
|
|
|
42
43
|
token_id=token_id,
|
|
43
44
|
tool_name=tool_name,
|
|
44
45
|
expires_at=expires_at,
|
|
46
|
+
max_uses=max_uses,
|
|
45
47
|
_secret=secret,
|
|
46
48
|
)
|
|
47
49
|
self._active_tokens[token_id] = token
|
|
@@ -58,6 +60,38 @@ class CredentialBroker:
|
|
|
58
60
|
"""Revoke a token immediately."""
|
|
59
61
|
self._active_tokens.pop(token_id, None)
|
|
60
62
|
|
|
63
|
+
def revoke_all_for_tool(self, tool_name: str) -> int:
|
|
64
|
+
"""Revoke all active tokens issued for a specific tool.
|
|
65
|
+
|
|
66
|
+
Returns the number of tokens revoked.
|
|
67
|
+
"""
|
|
68
|
+
token_ids = [
|
|
69
|
+
token_id
|
|
70
|
+
for token_id, token in self._active_tokens.items()
|
|
71
|
+
if token.tool_name == tool_name
|
|
72
|
+
]
|
|
73
|
+
for token_id in token_ids:
|
|
74
|
+
del self._active_tokens[token_id]
|
|
75
|
+
return len(token_ids)
|
|
76
|
+
|
|
77
|
+
def revoke_all(self) -> int:
|
|
78
|
+
"""Revoke all active tokens.
|
|
79
|
+
|
|
80
|
+
Returns the number of tokens revoked.
|
|
81
|
+
"""
|
|
82
|
+
count = len(self._active_tokens)
|
|
83
|
+
self._active_tokens.clear()
|
|
84
|
+
return count
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def active_token_count(self) -> int:
|
|
88
|
+
"""Return the number of currently active tokens."""
|
|
89
|
+
return len(self._active_tokens)
|
|
90
|
+
|
|
91
|
+
def list_active_tokens(self) -> list[str]:
|
|
92
|
+
"""Return token IDs for all active tokens (secrets are not exposed)."""
|
|
93
|
+
return list(self._active_tokens.keys())
|
|
94
|
+
|
|
61
95
|
def cleanup_expired(self) -> int:
|
|
62
96
|
"""Remove all expired tokens. Returns count removed."""
|
|
63
97
|
expired = [
|
|
@@ -5,7 +5,7 @@ import time
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@dataclass
|
|
8
|
+
@dataclass
|
|
9
9
|
class ScopedToken:
|
|
10
10
|
"""A time-limited, tool-scoped credential token.
|
|
11
11
|
|
|
@@ -15,23 +15,48 @@ class ScopedToken:
|
|
|
15
15
|
token_id: str # unique ID
|
|
16
16
|
tool_name: str # which tool this token is for
|
|
17
17
|
expires_at: float # unix timestamp
|
|
18
|
+
max_uses: int = 0 # 0 = unlimited uses
|
|
18
19
|
_secret: str = field(default="", repr=False, compare=False) # hidden from repr
|
|
20
|
+
_uses: int = field(default=0, repr=False)
|
|
19
21
|
|
|
20
22
|
def is_valid(self) -> bool:
|
|
21
23
|
"""Check if token is still valid (not expired)."""
|
|
22
24
|
return time.time() < self.expires_at
|
|
23
25
|
|
|
26
|
+
def use(self) -> bool:
|
|
27
|
+
"""Consume one use of the token.
|
|
28
|
+
|
|
29
|
+
Returns True if the use is allowed, False if max_uses exceeded.
|
|
30
|
+
"""
|
|
31
|
+
self._uses += 1
|
|
32
|
+
if self.max_uses > 0 and self._uses > self.max_uses:
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def uses_remaining(self) -> int | None:
|
|
38
|
+
"""Remaining allowed uses, or None when unlimited."""
|
|
39
|
+
if self.max_uses <= 0:
|
|
40
|
+
return None
|
|
41
|
+
return max(0, self.max_uses - self._uses)
|
|
42
|
+
|
|
24
43
|
def get_secret(self) -> str:
|
|
25
44
|
"""Get the actual secret. Only callable by trusted code."""
|
|
26
45
|
if not self.is_valid():
|
|
27
46
|
raise ValueError("Token expired")
|
|
47
|
+
if not self.use():
|
|
48
|
+
raise ValueError("Token exceeded max uses")
|
|
28
49
|
return self._secret
|
|
29
50
|
|
|
30
51
|
def __repr__(self) -> str:
|
|
31
52
|
return (
|
|
32
53
|
f"ScopedToken(token_id={self.token_id!r}, tool_name={self.tool_name!r}, "
|
|
33
|
-
f"expires_at={self.expires_at},
|
|
54
|
+
f"expires_at={self.expires_at}, max_uses={self.max_uses}, "
|
|
55
|
+
f"_secret='[REDACTED]')"
|
|
34
56
|
)
|
|
35
57
|
|
|
36
58
|
def __str__(self) -> str:
|
|
37
|
-
return
|
|
59
|
+
return (
|
|
60
|
+
f"ScopedToken({self.token_id}, tool={self.tool_name}, "
|
|
61
|
+
f"valid={self.is_valid()}, uses_remaining={self.uses_remaining})"
|
|
62
|
+
)
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Condition matchers for YAML policy rules (v0.3.2).
|
|
2
|
+
|
|
3
|
+
Each matcher receives the value declared in the YAML ``when:`` block plus the
|
|
4
|
+
SARC evaluation context (Subject, Action, Resource, AccessContext) and the
|
|
5
|
+
metadata of the tool being invoked. Matchers return ``True`` when the condition
|
|
6
|
+
is satisfied.
|
|
7
|
+
|
|
8
|
+
Unknown condition keys are fail-closed (return ``False``).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from petfishframework.core.contracts import RiskLevel
|
|
15
|
+
from petfishframework.permissions.model import AccessContext, Action, Resource, Subject
|
|
16
|
+
|
|
17
|
+
ConditionMatcher = Callable[
|
|
18
|
+
[Any, Subject, Action, Resource, AccessContext, dict[str, Any]],
|
|
19
|
+
bool,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _match_action_tool_name(
|
|
24
|
+
value: Any,
|
|
25
|
+
subject: Subject, # noqa: ARG001
|
|
26
|
+
action: Action,
|
|
27
|
+
resource: Resource, # noqa: ARG001
|
|
28
|
+
context: AccessContext, # noqa: ARG001
|
|
29
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
30
|
+
) -> bool:
|
|
31
|
+
"""Exact string match against ``action.tool_name``."""
|
|
32
|
+
return action.tool_name == value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _match_subject_role_in(
|
|
36
|
+
value: Any,
|
|
37
|
+
subject: Subject,
|
|
38
|
+
action: Action, # noqa: ARG001
|
|
39
|
+
resource: Resource, # noqa: ARG001
|
|
40
|
+
context: AccessContext, # noqa: ARG001
|
|
41
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""True if any role in ``value`` is present in ``subject.roles``."""
|
|
44
|
+
return any(role in subject.roles for role in value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _match_subject_role_not_in(
|
|
48
|
+
value: Any,
|
|
49
|
+
subject: Subject,
|
|
50
|
+
action: Action, # noqa: ARG001
|
|
51
|
+
resource: Resource, # noqa: ARG001
|
|
52
|
+
context: AccessContext, # noqa: ARG001
|
|
53
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""True if NO role in ``value`` is present in ``subject.roles``."""
|
|
56
|
+
return not any(role in subject.roles for role in value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _match_action_args_amount_gt(
|
|
60
|
+
value: Any,
|
|
61
|
+
subject: Subject, # noqa: ARG001
|
|
62
|
+
action: Action,
|
|
63
|
+
resource: Resource, # noqa: ARG001
|
|
64
|
+
context: AccessContext, # noqa: ARG001
|
|
65
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
66
|
+
) -> bool:
|
|
67
|
+
"""Numeric greater-than comparison against ``action.args["amount"]``."""
|
|
68
|
+
amount = action.args.get("amount", 0)
|
|
69
|
+
try:
|
|
70
|
+
return float(amount) > float(value)
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _match_action_args_amount_lt(
|
|
76
|
+
value: Any,
|
|
77
|
+
subject: Subject, # noqa: ARG001
|
|
78
|
+
action: Action,
|
|
79
|
+
resource: Resource, # noqa: ARG001
|
|
80
|
+
context: AccessContext, # noqa: ARG001
|
|
81
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
82
|
+
) -> bool:
|
|
83
|
+
"""Numeric less-than comparison against ``action.args["amount"]``."""
|
|
84
|
+
amount = action.args.get("amount", 0)
|
|
85
|
+
try:
|
|
86
|
+
return float(amount) < float(value)
|
|
87
|
+
except (TypeError, ValueError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _match_tool_side_effect(
|
|
92
|
+
value: Any,
|
|
93
|
+
subject: Subject, # noqa: ARG001
|
|
94
|
+
action: Action, # noqa: ARG001
|
|
95
|
+
resource: Resource, # noqa: ARG001
|
|
96
|
+
context: AccessContext, # noqa: ARG001
|
|
97
|
+
tool_metadata: dict[str, Any],
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Boolean match against ``tool_metadata["side_effect"]``."""
|
|
100
|
+
if not tool_metadata:
|
|
101
|
+
return False
|
|
102
|
+
return tool_metadata.get("side_effect") == value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _match_tool_external_egress(
|
|
106
|
+
value: Any,
|
|
107
|
+
subject: Subject, # noqa: ARG001
|
|
108
|
+
action: Action, # noqa: ARG001
|
|
109
|
+
resource: Resource, # noqa: ARG001
|
|
110
|
+
context: AccessContext, # noqa: ARG001
|
|
111
|
+
tool_metadata: dict[str, Any],
|
|
112
|
+
) -> bool:
|
|
113
|
+
"""Boolean match against ``tool_metadata["external_egress"]``."""
|
|
114
|
+
if not tool_metadata:
|
|
115
|
+
return False
|
|
116
|
+
return tool_metadata.get("external_egress") == value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _match_action_args_amount_eq(
|
|
120
|
+
value: Any,
|
|
121
|
+
subject: Subject, # noqa: ARG001
|
|
122
|
+
action: Action,
|
|
123
|
+
resource: Resource, # noqa: ARG001
|
|
124
|
+
context: AccessContext, # noqa: ARG001
|
|
125
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
126
|
+
) -> bool:
|
|
127
|
+
"""Numeric equality comparison against ``action.args["amount"]``."""
|
|
128
|
+
amount = action.args.get("amount", 0)
|
|
129
|
+
try:
|
|
130
|
+
return float(amount) == float(value)
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _match_action_args_amount_gte(
|
|
136
|
+
value: Any,
|
|
137
|
+
subject: Subject, # noqa: ARG001
|
|
138
|
+
action: Action,
|
|
139
|
+
resource: Resource, # noqa: ARG001
|
|
140
|
+
context: AccessContext, # noqa: ARG001
|
|
141
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
142
|
+
) -> bool:
|
|
143
|
+
"""Numeric greater-than-or-equal comparison against ``action.args["amount"]``."""
|
|
144
|
+
amount = action.args.get("amount", 0)
|
|
145
|
+
try:
|
|
146
|
+
return float(amount) >= float(value)
|
|
147
|
+
except (TypeError, ValueError):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _match_action_args_amount_lte(
|
|
152
|
+
value: Any,
|
|
153
|
+
subject: Subject, # noqa: ARG001
|
|
154
|
+
action: Action,
|
|
155
|
+
resource: Resource, # noqa: ARG001
|
|
156
|
+
context: AccessContext, # noqa: ARG001
|
|
157
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""Numeric less-than-or-equal comparison against ``action.args["amount"]``."""
|
|
160
|
+
amount = action.args.get("amount", 0)
|
|
161
|
+
try:
|
|
162
|
+
return float(amount) <= float(value)
|
|
163
|
+
except (TypeError, ValueError):
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _match_subject_role_count_gte(
|
|
168
|
+
value: Any,
|
|
169
|
+
subject: Subject,
|
|
170
|
+
action: Action, # noqa: ARG001
|
|
171
|
+
resource: Resource, # noqa: ARG001
|
|
172
|
+
context: AccessContext, # noqa: ARG001
|
|
173
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
174
|
+
) -> bool:
|
|
175
|
+
"""True if ``subject.roles`` has at least ``value`` entries."""
|
|
176
|
+
try:
|
|
177
|
+
return len(subject.roles) >= int(value)
|
|
178
|
+
except (TypeError, ValueError):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _match_subject_clearance_eq(
|
|
183
|
+
value: Any,
|
|
184
|
+
subject: Subject,
|
|
185
|
+
action: Action, # noqa: ARG001
|
|
186
|
+
resource: Resource, # noqa: ARG001
|
|
187
|
+
context: AccessContext, # noqa: ARG001
|
|
188
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
189
|
+
) -> bool:
|
|
190
|
+
"""Exact match against ``subject.clearance``."""
|
|
191
|
+
return subject.clearance == value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _match_subject_tenant_id_eq(
|
|
195
|
+
value: Any,
|
|
196
|
+
subject: Subject,
|
|
197
|
+
action: Action, # noqa: ARG001
|
|
198
|
+
resource: Resource, # noqa: ARG001
|
|
199
|
+
context: AccessContext, # noqa: ARG001
|
|
200
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""Exact match against ``subject.tenant_id``."""
|
|
203
|
+
return subject.tenant_id == value
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _match_resource_classification_eq(
|
|
207
|
+
value: Any,
|
|
208
|
+
subject: Subject, # noqa: ARG001
|
|
209
|
+
action: Action, # noqa: ARG001
|
|
210
|
+
resource: Resource,
|
|
211
|
+
context: AccessContext, # noqa: ARG001
|
|
212
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
213
|
+
) -> bool:
|
|
214
|
+
"""Exact match against ``resource.classification``."""
|
|
215
|
+
return resource.classification == value
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _match_resource_tags_contains(
|
|
219
|
+
value: Any,
|
|
220
|
+
subject: Subject, # noqa: ARG001
|
|
221
|
+
action: Action, # noqa: ARG001
|
|
222
|
+
resource: Resource,
|
|
223
|
+
context: AccessContext, # noqa: ARG001
|
|
224
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
225
|
+
) -> bool:
|
|
226
|
+
"""True if any tag in ``value`` is present in ``resource.tags``."""
|
|
227
|
+
return any(tag in resource.tags for tag in value)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _match_tool_risk_level_eq(
|
|
231
|
+
value: Any,
|
|
232
|
+
subject: Subject, # noqa: ARG001
|
|
233
|
+
action: Action, # noqa: ARG001
|
|
234
|
+
resource: Resource, # noqa: ARG001
|
|
235
|
+
context: AccessContext, # noqa: ARG001
|
|
236
|
+
tool_metadata: dict[str, Any],
|
|
237
|
+
) -> bool:
|
|
238
|
+
"""Match against ``tool_metadata["risk_level"]`` (string or ``RiskLevel`` enum)."""
|
|
239
|
+
if not tool_metadata:
|
|
240
|
+
return False
|
|
241
|
+
risk_level = tool_metadata.get("risk_level")
|
|
242
|
+
if risk_level is None:
|
|
243
|
+
return False
|
|
244
|
+
if isinstance(value, RiskLevel):
|
|
245
|
+
return risk_level == value
|
|
246
|
+
if isinstance(risk_level, RiskLevel):
|
|
247
|
+
return risk_level.value == str(value).lower()
|
|
248
|
+
return str(risk_level).lower() == str(value).lower()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _match_tool_capabilities_contains(
|
|
252
|
+
value: Any,
|
|
253
|
+
subject: Subject, # noqa: ARG001
|
|
254
|
+
action: Action, # noqa: ARG001
|
|
255
|
+
resource: Resource, # noqa: ARG001
|
|
256
|
+
context: AccessContext, # noqa: ARG001
|
|
257
|
+
tool_metadata: dict[str, Any],
|
|
258
|
+
) -> bool:
|
|
259
|
+
"""True if any capability in ``value`` is present in tool capabilities."""
|
|
260
|
+
if not tool_metadata:
|
|
261
|
+
return False
|
|
262
|
+
capabilities = tool_metadata.get("capabilities", ())
|
|
263
|
+
return any(capability in capabilities for capability in value)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _match_tool_requires_credentials(
|
|
267
|
+
value: Any,
|
|
268
|
+
subject: Subject, # noqa: ARG001
|
|
269
|
+
action: Action, # noqa: ARG001
|
|
270
|
+
resource: Resource, # noqa: ARG001
|
|
271
|
+
context: AccessContext, # noqa: ARG001
|
|
272
|
+
tool_metadata: dict[str, Any],
|
|
273
|
+
) -> bool:
|
|
274
|
+
"""Boolean match against ``tool_metadata["requires_credentials"]``."""
|
|
275
|
+
if not tool_metadata:
|
|
276
|
+
return False
|
|
277
|
+
return tool_metadata.get("requires_credentials") == value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _match_context_session_risk_gt(
|
|
281
|
+
value: Any,
|
|
282
|
+
subject: Subject, # noqa: ARG001
|
|
283
|
+
action: Action, # noqa: ARG001
|
|
284
|
+
resource: Resource, # noqa: ARG001
|
|
285
|
+
context: AccessContext,
|
|
286
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
287
|
+
) -> bool:
|
|
288
|
+
"""Numeric greater-than comparison against ``context.session_risk``."""
|
|
289
|
+
try:
|
|
290
|
+
return float(context.session_risk) > float(value)
|
|
291
|
+
except (TypeError, ValueError):
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _match_context_prompt_risk_gt(
|
|
296
|
+
value: Any,
|
|
297
|
+
subject: Subject, # noqa: ARG001
|
|
298
|
+
action: Action, # noqa: ARG001
|
|
299
|
+
resource: Resource, # noqa: ARG001
|
|
300
|
+
context: AccessContext,
|
|
301
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
302
|
+
) -> bool:
|
|
303
|
+
"""Numeric greater-than comparison against ``context.prompt_risk``."""
|
|
304
|
+
try:
|
|
305
|
+
return float(context.prompt_risk) > float(value)
|
|
306
|
+
except (TypeError, ValueError):
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _match_unknown(
|
|
311
|
+
value: Any, # noqa: ARG001
|
|
312
|
+
subject: Subject, # noqa: ARG001
|
|
313
|
+
action: Action, # noqa: ARG001
|
|
314
|
+
resource: Resource, # noqa: ARG001
|
|
315
|
+
context: AccessContext, # noqa: ARG001
|
|
316
|
+
tool_metadata: dict[str, Any], # noqa: ARG001
|
|
317
|
+
) -> bool:
|
|
318
|
+
"""Fail-closed default for unrecognized condition keys."""
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
_MATCHERS: dict[str, ConditionMatcher] = {
|
|
323
|
+
"action.tool_name": _match_action_tool_name,
|
|
324
|
+
"subject.role_in": _match_subject_role_in,
|
|
325
|
+
"subject.role_not_in": _match_subject_role_not_in,
|
|
326
|
+
"action.args.amount_gt": _match_action_args_amount_gt,
|
|
327
|
+
"action.args.amount_lt": _match_action_args_amount_lt,
|
|
328
|
+
"action.args.amount_eq": _match_action_args_amount_eq,
|
|
329
|
+
"action.args.amount_gte": _match_action_args_amount_gte,
|
|
330
|
+
"action.args.amount_lte": _match_action_args_amount_lte,
|
|
331
|
+
"tool.side_effect": _match_tool_side_effect,
|
|
332
|
+
"tool.external_egress": _match_tool_external_egress,
|
|
333
|
+
"subject.role_count_gte": _match_subject_role_count_gte,
|
|
334
|
+
"subject.clearance_eq": _match_subject_clearance_eq,
|
|
335
|
+
"subject.tenant_id_eq": _match_subject_tenant_id_eq,
|
|
336
|
+
"resource.classification_eq": _match_resource_classification_eq,
|
|
337
|
+
"resource.tags_contains": _match_resource_tags_contains,
|
|
338
|
+
"tool.risk_level_eq": _match_tool_risk_level_eq,
|
|
339
|
+
"tool.capabilities_contains": _match_tool_capabilities_contains,
|
|
340
|
+
"tool.requires_credentials": _match_tool_requires_credentials,
|
|
341
|
+
"context.session_risk_gt": _match_context_session_risk_gt,
|
|
342
|
+
"context.prompt_risk_gt": _match_context_prompt_risk_gt,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def match_condition(
|
|
347
|
+
key: str,
|
|
348
|
+
value: Any,
|
|
349
|
+
subject: Subject,
|
|
350
|
+
action: Action,
|
|
351
|
+
resource: Resource,
|
|
352
|
+
context: AccessContext,
|
|
353
|
+
tool_metadata: dict[str, Any],
|
|
354
|
+
) -> bool:
|
|
355
|
+
"""Evaluate a single condition key/value pair.
|
|
356
|
+
|
|
357
|
+
Unknown keys fail closed (return ``False``). Empty condition sets are
|
|
358
|
+
handled by the caller and match everything.
|
|
359
|
+
|
|
360
|
+
Supports the generic prefix matcher ``action.args.<field>_eq`` for equality
|
|
361
|
+
checks against any key in ``action.args``.
|
|
362
|
+
"""
|
|
363
|
+
matcher = _MATCHERS.get(key)
|
|
364
|
+
if matcher is not None:
|
|
365
|
+
return matcher(value, subject, action, resource, context, tool_metadata)
|
|
366
|
+
|
|
367
|
+
if key.startswith("action.args.") and key.endswith("_eq"):
|
|
368
|
+
field = key[len("action.args.") : -len("_eq")]
|
|
369
|
+
return action.args.get(field) == value
|
|
370
|
+
|
|
371
|
+
return _match_unknown(value, subject, action, resource, context, tool_metadata)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def match_conditions_with_combinators(
|
|
375
|
+
conditions: dict[str, Any],
|
|
376
|
+
subject: Subject,
|
|
377
|
+
action: Action,
|
|
378
|
+
resource: Resource,
|
|
379
|
+
context: AccessContext,
|
|
380
|
+
tool_metadata: dict[str, Any],
|
|
381
|
+
) -> bool:
|
|
382
|
+
"""Evaluate conditions supporting ``any``/``all``/``not`` combinators.
|
|
383
|
+
|
|
384
|
+
Flat dictionaries continue to act as implicit AND (backward compatible).
|
|
385
|
+
An empty condition dict matches everything (default-allow semantics).
|
|
386
|
+
"""
|
|
387
|
+
if not conditions:
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
if "any" in conditions:
|
|
391
|
+
sub_conditions = conditions["any"]
|
|
392
|
+
if not isinstance(sub_conditions, list):
|
|
393
|
+
return False
|
|
394
|
+
return any(
|
|
395
|
+
match_conditions_with_combinators(sub, subject, action, resource, context, tool_metadata)
|
|
396
|
+
for sub in sub_conditions
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
if "all" in conditions:
|
|
400
|
+
sub_conditions = conditions["all"]
|
|
401
|
+
if not isinstance(sub_conditions, list):
|
|
402
|
+
return False
|
|
403
|
+
return all(
|
|
404
|
+
match_conditions_with_combinators(sub, subject, action, resource, context, tool_metadata)
|
|
405
|
+
for sub in sub_conditions
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if "not" in conditions:
|
|
409
|
+
inner = conditions["not"]
|
|
410
|
+
if not isinstance(inner, dict):
|
|
411
|
+
return False
|
|
412
|
+
return not match_conditions_with_combinators(inner, subject, action, resource, context, tool_metadata)
|
|
413
|
+
|
|
414
|
+
return all(
|
|
415
|
+
match_condition(key, value, subject, action, resource, context, tool_metadata)
|
|
416
|
+
for key, value in conditions.items()
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def match_all_conditions(
|
|
421
|
+
conditions: dict[str, Any],
|
|
422
|
+
subject: Subject,
|
|
423
|
+
action: Action,
|
|
424
|
+
resource: Resource,
|
|
425
|
+
context: AccessContext,
|
|
426
|
+
tool_metadata: dict[str, Any],
|
|
427
|
+
) -> bool:
|
|
428
|
+
"""Return True if every condition in the rule matches.
|
|
429
|
+
|
|
430
|
+
An empty condition dict matches everything (default-allow semantics).
|
|
431
|
+
Delegates to ``match_conditions_with_combinators`` for ``any``/``all``/``not``
|
|
432
|
+
support while preserving the legacy flat-dictionary AND behavior.
|
|
433
|
+
"""
|
|
434
|
+
return match_conditions_with_combinators(
|
|
435
|
+
conditions, subject, action, resource, context, tool_metadata
|
|
436
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""YAML Policy Engine (v0.3.
|
|
1
|
+
"""YAML Policy Engine (v0.3.2).
|
|
2
2
|
|
|
3
3
|
Loads authorization rules from YAML and evaluates them in priority order.
|
|
4
4
|
Implements the PermissionPolicy protocol so it can be plugged directly into
|
|
@@ -58,6 +58,9 @@ class YamlPolicy:
|
|
|
58
58
|
self._tool_metadata[name] = {
|
|
59
59
|
"side_effect": getattr(tool, "side_effect", False),
|
|
60
60
|
"external_egress": getattr(tool, "external_egress", False),
|
|
61
|
+
"risk_level": getattr(tool, "risk_level", None),
|
|
62
|
+
"capabilities": getattr(tool, "capabilities", ()),
|
|
63
|
+
"requires_credentials": getattr(tool, "requires_credentials", False),
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
def evaluate(self, subject: Subject, action: Action, resource: Resource, context: AccessContext) -> Decision:
|