diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 555ad52..0511796 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -41,12 +41,12 @@ jobs: - name: Ruff lint run: uv run ruff check . - name: Mypy strict - run: uv run mypy --strict packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src + run: uv run mypy --strict packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-governance/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src - name: Bandit # -c pyproject.toml so [tool.bandit] (skips B101) is honoured; # without it, the conformance suite's asserts in # agentforge_core.testing.conformance trip B101. - run: uv run bandit -q -c pyproject.toml -r packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src + run: uv run bandit -q -c pyproject.toml -r packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-governance/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src test: name: Test (Linux, Python ${{ matrix.python-version }}) @@ -66,7 +66,7 @@ jobs: - name: Sync dependencies run: uv sync --all-extras --dev - name: Pytest unit - run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit + run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-governance/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit - name: Pytest integration run: uv run pytest -q -m "not live" packages/agentforge-bedrock/tests/integration tests/integration - name: Coverage diff --git a/.github/workflows/ci-mac.yml b/.github/workflows/ci-mac.yml index ce81822..f99c48b 100644 --- a/.github/workflows/ci-mac.yml +++ b/.github/workflows/ci-mac.yml @@ -34,7 +34,7 @@ jobs: - name: Sync dependencies run: uv sync --all-extras --dev - name: Pytest unit - run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit + run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-governance/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit - name: Pytest integration run: uv run pytest -q -m "not live" packages/agentforge-bedrock/tests/integration tests/integration diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml index a879795..5ed4e6d 100644 --- a/.github/workflows/ci-windows.yml +++ b/.github/workflows/ci-windows.yml @@ -34,6 +34,6 @@ jobs: - name: Sync dependencies run: uv sync --all-extras --dev - name: Pytest unit - run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit + run: uv run pytest -q -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-governance/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit - name: Pytest integration run: uv run pytest -q -m "not live" packages/agentforge-bedrock/tests/integration tests/integration diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4c0276..039d4f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,7 +76,7 @@ repos: # errors that file-scope mypy misses. - id: mypy-strict name: mypy --strict - entry: uv run mypy --strict packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src + entry: uv run mypy --strict packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-governance/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src language: system types: [python] pass_filenames: false @@ -84,7 +84,7 @@ repos: - id: bandit name: bandit security lint # -c pyproject.toml so bandit reads [tool.bandit] (skips B101 etc.) - entry: uv run bandit -q -c pyproject.toml -r packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src + entry: uv run bandit -q -c pyproject.toml -r packages/agentforge-core/src packages/agentforge/src packages/agentforge-bedrock/src packages/agentforge-memory-sqlite/src packages/agentforge-memory-neo4j/src packages/agentforge-memory-kuzu/src packages/agentforge-memory-surrealdb/src packages/agentforge-memory-postgres/src packages/agentforge-eval-geval/src packages/agentforge-otel/src packages/agentforge-testing/src packages/agentforge-guard-llmguard/src packages/agentforge-guard-presidio/src packages/agentforge-guard-nemo/src packages/agentforge-guard-llamaguard/src packages/agentforge-mcp/src packages/agentforge-chat/src packages/agentforge-chat-http/src packages/agentforge-chat-history-postgres/src packages/agentforge-chat-history-redis/src packages/agentforge-chat-slack/src packages/agentforge-a2a/src packages/agentforge-statsd/src packages/agentforge-langfuse/src packages/agentforge-phoenix/src packages/agentforge-evidently/src packages/agentforge-governance/src packages/agentforge-reranker-sentence-transformers/src packages/agentforge-reranker-cohere/src packages/agentforge-reranker-voyage/src packages/agentforge-reranker-mixedbread/src packages/agentforge-anthropic/src packages/agentforge-openai/src packages/agentforge-voyage/src packages/agentforge-litellm/src packages/agentforge-ollama/src language: system types: [python] pass_filenames: false @@ -94,7 +94,7 @@ repos: # are still empty. Real failures (1, 2, 3, 4) still block. - id: pytest-unit name: pytest unit - entry: bash -c 'uv run pytest -q -x -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit; code=$?; [ $code -eq 0 ] || [ $code -eq 5 ] && exit 0 || exit $code' + entry: bash -c 'uv run pytest -q -x -m "not live" packages/agentforge-core/tests packages/agentforge/tests packages/agentforge-bedrock/tests/unit packages/agentforge-memory-sqlite/tests/unit packages/agentforge-memory-neo4j/tests/unit packages/agentforge-memory-kuzu/tests/unit packages/agentforge-memory-surrealdb/tests/unit packages/agentforge-memory-postgres/tests/unit packages/agentforge-eval-geval/tests/unit packages/agentforge-otel/tests/unit packages/agentforge-testing/tests/unit packages/agentforge-guard-llmguard/tests/unit packages/agentforge-guard-presidio/tests/unit packages/agentforge-guard-nemo/tests/unit packages/agentforge-guard-llamaguard/tests/unit packages/agentforge-mcp/tests/unit packages/agentforge-chat/tests/unit packages/agentforge-chat-http/tests/unit packages/agentforge-chat-history-postgres/tests/unit packages/agentforge-chat-history-redis/tests/unit packages/agentforge-chat-slack/tests/unit packages/agentforge-a2a/tests/unit packages/agentforge-statsd/tests/unit packages/agentforge-langfuse/tests/unit packages/agentforge-phoenix/tests/unit packages/agentforge-evidently/tests/unit packages/agentforge-governance/tests/unit packages/agentforge-reranker-sentence-transformers/tests/unit packages/agentforge-reranker-cohere/tests/unit packages/agentforge-reranker-voyage/tests/unit packages/agentforge-reranker-mixedbread/tests/unit packages/agentforge-anthropic/tests/unit packages/agentforge-openai/tests/unit packages/agentforge-voyage/tests/unit packages/agentforge-litellm/tests/unit packages/agentforge-ollama/tests/unit tests/unit; code=$?; [ $code -eq 0 ] || [ $code -eq 5 ] && exit 0 || exit $code' language: system always_run: true pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index b76b986..354eea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,19 @@ slice between two versions to see which of your filed issues a bump fixes. ### Added +- **feat-029 — governance: identity (pillar 1).** First pillar of the governance + spine (ADR-0023): every agent gets a stable, portable `Principal` so every + action has a name. New `IdentityProvider` contract (`issue` / `resolve` / + `verify` / `credential` / `rotate`) + `run_identity_conformance` in + `agentforge-core`; the existing `Principal` is widened additively with `kind` + / `owner` (backward compatible — one principal for `auth` + identity). New + `agentforge-governance` package ships the offline `local` driver + (`LocalIdentityProvider`, in-process HMAC-signed credentials, URN ids + `agentforge:agent:/@`). `governance.identity` config block + + `build_identity_from_config`. The `Agent(identity=)` wiring + runtime + stamping follow with the audit pillar. Registry / policy / audit pillars land + on the 0.5 train. + - **feat-027 — embedded `KuzuGraphStore`.** A zero-ops, file-backed, in-process `GraphStore` driver — `path: .ckg` and the store exists, no server (the graph analogue of the SQLite `MemoryStore`). New `agentforge-memory-kuzu` package, diff --git a/docs/adr/0023-governance-spine-contracts-and-umbrella-package.md b/docs/adr/0023-governance-spine-contracts-and-umbrella-package.md new file mode 100644 index 0000000..9a608a2 --- /dev/null +++ b/docs/adr/0023-governance-spine-contracts-and-umbrella-package.md @@ -0,0 +1,102 @@ +# ADR-0023: Governance spine — locked contracts in core, one umbrella driver package + +## Metadata + +| Field | Value | +|---|---| +| **Number** | 0023 | +| **Title** | Governance spine — locked contracts in core, one umbrella driver package | +| **Status** | Accepted | +| **Date** | 2026-06-22 | +| **Deciders** | kjoshi | +| **Tags** | architecture, governance, contracts | + +--- + +## 1. Context and problem statement + +The framework assembles agents (providers, memory, RAG, MCP/A2A, eval, +guardrails, budgets). It does not yet give an operator the means to *govern* +a fleet of those agents: a stable identity per agent, an inventory of what +each agent is, enforceable rules over what it may do, and a tamper-evident +record of what it did. These four concerns — **identity, registry, policy, +audit** — are universal production needs (every org running agents needs +them) and are load-bearing/hard to get right, so they belong in the +framework, not re-implemented per agent. + +This ADR fixes the architecture for the whole governance epic (feat-029 +identity, and the registry/policy/audit features that follow). Each pillar +introduces a **new locked core contract**, so the cross-cutting decisions — +where the contracts live, how the drivers are packaged, and how identity +relates to the existing `auth` `Principal` — must be settled once, here. + +## 2. Decision drivers + +- **Contracts-and-drivers** (the framework's universal rule, ADR-0003/0007): + a locked ABC in `agentforge-core` + swappable drivers discovered via entry + points. +- **Offline-first**: every pillar must be deterministically testable with no + network and no cloud account (file/sqlite drivers ship first). +- **Vendor-neutral by construction**: native primitives that *map onto* + OIDC / SPIFFE / cloud IAM / OPA / SIEM, never a hard dependency on one. +- **Additive, not a rewrite**: build on what exists (`auth`, config spine, + observability, budgets, persistence) — minor bumps, no breakage. + +## 3. Decisions + +1. **The four contracts live in `agentforge-core`.** `IdentityProvider` + (feat-029), `Registry`, `PolicyEngine`, and `AuditSink` are locked ABCs in + `agentforge_core.contracts`, each with a conformance suite in + `agentforge_core.testing`, exactly like `MemoryStore` / `GraphStore`. + +2. **Default drivers ship in ONE umbrella package, `agentforge-governance`.** + Rather than four micro-packages (`agentforge-identity`, `-registry`, + `-policy`, `-audit`), the offline default drivers (`local` identity, file / + sqlite registry, native policy, jsonl / sqlite audit) live in a single + package with one dependency surface. The pillars are tightly related and + usually adopted together; one package is a simpler install and release unit. + Heavier vendor drivers (OIDC, OPA, SIEM, postgres) may still ship as their + own packages later. The contracts stay in core regardless. + +3. **Identity reuses the existing `Principal`, widened additively.** feat-014 + already ships `agentforge_core.values.auth.Principal` (`id` + `metadata`). + Rather than introduce a second, colliding `Principal`, we **widen** it with + `kind: str = "agent"` and `owner: str | None = None` (both defaulted → + backward compatible), and treat `metadata` as the governance `attributes` + bag. One `Principal` flows through both `AuthPolicy.authenticate(...)` and + `IdentityProvider`. This resolves the identity draft's open question and + keeps `auth` working unchanged. + +4. **New entry-point categories**, resolved automatically by the existing + resolver: `agentforge.identity_providers`, `agentforge.registries`, + `agentforge.policy_engines`, `agentforge.audit_sinks`. + +5. **Config under a reserved `governance:` block**, strict (`extra="forbid"`) + like the rest of config, with `identity` / `registry` / `policy` / `audit` + sub-blocks added as each pillar lands. + +6. **Build-dependency order: Identity → Registry → Policy → Audit.** Identity + is foundational (the other three reference a principal); Audit ships last + (it records identity-attributed policy decisions). Each pillar is its own + `feat-NNN` + ships on the coordinated release train (ADR-0015). + +## 4. Consequences + +- **Positive:** one coherent spine; consumers turn governance on by config; + the contracts are swap-compatible (file/sqlite today, cloud later); no + breaking change to `auth`; each pillar is independently reviewable. +- **Negative / trade-offs:** the umbrella package couples the four drivers' + release cadence (acceptable — they version in lockstep anyway); widening + `Principal` adds two fields to a locked value type (a minor bump, but it is a + locked-surface change recorded here). +- **Deferred to the per-pillar specs:** policy rule-precedence + fail-mode, + audit tamper-evidence strength, the registry `RegistryDiff` shape, and + whether a dedicated `CheckpointStore`-style ABC is ever needed. + +## 5. References + +- feat-029 (identity, the first pillar) and the registry/policy/audit specs. +- ADR-0003 (modules as entry points), ADR-0007 (locked surfaces), + ADR-0015 (coordinated release train). +- Builds on: feat-014 (`auth` / `Principal`), feat-026 (config spine), + feat-007 (budgets), feat-009 (observability), feat-005/024 (persistence). diff --git a/docs/features/README.md b/docs/features/README.md index 183c00d..06286f9 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -44,6 +44,7 @@ | **feat-026** | Application config extension — reserved `app:` namespace + typed `app_as()` accessor (Phase 1), registered typed sections validated via the module-schema engine + entry points (Phase 2), pluggable config sources / separate files via `imports:` (Phase 3). Lets agents built on AgentForge reuse the config machinery (interpolation, layering, `--resolved`, uniform validation). Reported via #86 | shipped (all 3 phases → 0.3.0) | 0.3.0 | both | `agentforge-core`, `agentforge` | | **feat-027** | Embedded `GraphStore` — `KuzuGraphStore`, a zero-ops, file-backed, in-process graph driver (the graph analogue of the SQLite `MemoryStore`). Implements the locked `GraphStore` ABC and passes `run_graph_conformance`, so it is swap-compatible with Neo4j/SurrealDB; `path: .ckg` and the store exists — no server. Makes the whole graph + GraphRAG path testable offline. Drives the `agentforge-graph` code-graph dogfood | implemented (0.4) | 0.4 | python (TS deferred) | new `agentforge-memory-kuzu` | | **feat-028** | Durable execution + human-in-the-loop — checkpoint-and-resume so a run survives process death (resumes at the failed step, no double-spend / double-fire), plus approval gates that suspend before irreversible tool calls and resume on human approve/deny/edit. Promotes the feat-017 record/replay seam into a checkpoint seam (`__checkpoint` claims on a `MemoryStore`); budget snapshot + idempotency-seed restore for correctness; `execution:` / `human_in_the_loop:` config blocks; additive `Agent` kwargs. Foundational execution infrastructure every production agent eventually needs | proposed | 0.5 | python (TS deferred) | `agentforge-core`, `agentforge`, checkpoint driver pkg(s) | +| **feat-029** | Governance pillar 1 — identity. Stable, portable `Principal` per agent so every action has a name (the foundation the registry / policy / audit pillars attribute to). New `IdentityProvider` contract (issue / resolve / verify / credential / rotate) + conformance in core; `Principal` widened additively (`kind` / `owner`) so one principal serves `auth` + identity. Offline `local` driver (HMAC, URN ids) in the new `agentforge-governance` umbrella pkg; `governance.identity` config. Per ADR-0023 | implemented (0.4) | 0.4 | python (TS deferred) | `agentforge-core`, `agentforge-governance` | ### Safety & security diff --git a/docs/features/feat-029-governance-identity.md b/docs/features/feat-029-governance-identity.md new file mode 100644 index 0000000..96dbc9d --- /dev/null +++ b/docs/features/feat-029-governance-identity.md @@ -0,0 +1,147 @@ +# feat-029: Governance — identity (`Principal` / `IdentityProvider`) + +## Metadata + +| Field | Value | +|---|---| +| **ID** | feat-029 | +| **Title** | Governance pillar 1 — identity: `IdentityProvider` + the widened `Principal` | +| **Status** | implemented (not yet released) | +| **Owner** | kjoshi | +| **Created** | 2026-06-22 | +| **Target version** | 0.4 (foundation); registry/policy/audit follow on 0.5 | +| **Languages** | `python` (TS deferred) | +| **Module package(s)** | `agentforge-core` (contract + widened value), `agentforge-governance` (`local` driver) | +| **Depends on** | feat-014 (`auth` / `Principal`), feat-026 (config), ADR-0023 (epic architecture) | +| **Blocks** | registry / policy / audit (all reference a principal) | + +--- + +## 1. Why this feature + +The first pillar of the governance spine (ADR-0023): every agent — and the +tools / services it talks to — needs a **stable, portable identity** so every +action has a name. Without it there is nothing to attribute a registry entry, +a policy decision, or an audit event to. Identity is foundational: the other +three pillars all key on a principal, so it ships first. + +## 2. Why it must ship as framework + +- **Universal.** Every governed agent needs a verifiable identity; none of it + is domain-specific. +- **Builds on what exists.** The framework already has a `Principal` value and + an `AuthPolicy` contract (feat-014). Identity extends that surface rather + than inventing a parallel one. +- **Hard to get right, vendor-neutral.** A stable id scheme, credential + issue/verify/rotate, and mappings onto OIDC / SPIFFE / cloud IAM are + framework concerns; the `local` driver makes the whole thing offline-testable. + +### 2.5 Framework-level vs derived-agent-level + +**Framework.** The `Principal` value, the `IdentityProvider` contract, the URN +id scheme, the offline `local` driver, the conformance suite, and (later) the +OIDC/SPIFFE/cloud mappings. + +- **Derived-agent test:** a consumer can't make its agent's identity portable + or verifiable from its own code without re-implementing issue/verify/rotate + and an id scheme — framework work. +- **How it helps derived agents:** an agent declares *who it is* in config + (`governance.identity`); the framework makes that a verifiable, stable + `Principal` every other pillar can attribute to. Config, not code. + +## 3. How derived agents benefit + +```yaml +governance: + identity: + provider: local # local | oidc | spiffe | aws-iam … + name: invoice-reconciler + owner: finance-platform + attributes: { env: prod, region: eu-west-1 } +``` + +The framework issues the agent's principal +`agentforge:agent:local/invoice-reconciler@1` — stable across deploys, +portable, and ready to be referenced by the registry, scoped by policy, and +attributed in the audit log. + +## 4. Design + +### 4.1 The widened `Principal` (ADR-0023 decision 3) + +`agentforge_core.values.auth.Principal` gains `kind: str = "agent"` and +`owner: str | None = None` (both defaulted → backward compatible); `metadata` +serves as the `attributes` bag. One `Principal` flows through both +`AuthPolicy.authenticate(...)` and `IdentityProvider`. + +### 4.2 The `IdentityProvider` contract + +```python +class IdentityProvider(ABC): + async def issue(self, *, name: str, owner: str, + attributes: Mapping[str, str] | None = None) -> Principal: ... + async def resolve(self, principal_id: str) -> Principal | None: ... + async def verify(self, token: str) -> Principal: ... # inbound: prove who's calling + async def credential(self, principal: Principal) -> str: ... # outbound: prove who we are + async def rotate(self, principal_id: str) -> Principal: ... + def capabilities(self) -> set[str]: ... +``` + +`issue` is idempotent on `name` (stable id); `resolve` returns `None` for an +unknown id; `verify` raises `IdentityError` on an invalid credential; `rotate` +keeps the id but invalidates prior credentials. Locked per ADR-0007. + +### 4.3 The id scheme + +`agentforge:agent:/@` — a URN, stable across deploys, +portable, with a version component a registry entry can pin to. Vendor drivers +map this onto OIDC subjects / SPIFFE IDs / IAM ARNs without the framework +depending on any of them. + +### 4.4 The `local` driver + +`agentforge_governance.identity.LocalIdentityProvider`: in-process, +deterministic, zero-dependency. Issues/resolves principals in memory and signs +credentials with per-principal HMAC; `rotate` swaps the secret so prior +credentials stop verifying. The identity analogue of the SQLite `MemoryStore` +— the whole pillar is testable offline with no cloud account. + +### 4.5 Config + wiring + +`governance.identity` (strict, `extra="forbid"`); `build_identity_from_config` +resolves the `identity_providers` driver, constructs it, and issues the +agent's principal when `name` + `owner` are set. The `local` provider +registers under `[project.entry-points."agentforge.identity_providers"]`. + +## 5. Backward compatibility + +Fully additive. Widening `Principal` with two defaulted fields keeps every +existing `Principal(id=…)` / `Principal(id=…, metadata=…)` call working and +`AuthPolicy.authenticate(...)` unchanged. No `governance:` block → no identity, +identical behaviour to today. + +## 6. Test strategy + +- `run_identity_conformance` (in core) — issue→resolve round-trip + idempotency, + credential/verify round-trip, rotation invalidating old credentials, and the + unknown/invalid paths — run against `LocalIdentityProvider`, offline. +- Driver units: URN scheme, idempotent issue, rotation, capabilities, + `build_identity_from_config` resolving the entry point + issuing the principal. + +## 7. Out of scope (this pillar) +- OIDC / SPIFFE / cloud-IAM drivers (map onto the same contract; later). +- Deep runtime stamping of the principal onto every step/tool call — lands with + the audit pillar, which is what consumes it. +- TypeScript port. + +## 8. Implementation status (Python) +**Status: implemented (not yet released).** Ships the widened `Principal`, the +`IdentityProvider` ABC + `IdentityError`, `run_identity_conformance`, the +`agentforge-governance` package with `LocalIdentityProvider`, the +`governance.identity` config block, and `build_identity_from_config`. +Remaining for the pillar's full value (later PRs): the `Agent(identity=)` +constructor kwarg + run-time principal stamping (best done alongside audit). + +## 9. References +- ADR-0023 (governance epic architecture), feat-014 (`auth` / `Principal`), + feat-026 (config). Registry / policy / audit specs follow. diff --git a/packages/agentforge-core/src/agentforge_core/config/schema.py b/packages/agentforge-core/src/agentforge_core/config/schema.py index e3bbc29..23b472b 100644 --- a/packages/agentforge-core/src/agentforge_core/config/schema.py +++ b/packages/agentforge-core/src/agentforge_core/config/schema.py @@ -460,6 +460,34 @@ class OutputConfig(BaseModel): thresholds: dict[str, list[str]] = Field(default_factory=dict) +class IdentityConfig(BaseModel): + """`governance.identity:` — the agent's identity (feat-029). + + `provider` resolves against the `identity_providers` entry-point + category; `name` / `owner` / `attributes` describe the principal the + framework issues for this agent; `config` is provider-specific (issuer + URL, trust domain, role_arn, …).""" + + model_config = ConfigDict(strict=True, extra="forbid") + + provider: str = "local" + name: str | None = None + owner: str | None = None + attributes: dict[str, str] = Field(default_factory=dict) + config: dict[str, Any] = Field(default_factory=dict) + + +class GovernanceConfig(BaseModel): + """`governance:` — the governance spine (feat-029+). + + Identity ships first; `registry` / `policy` / `audit` sub-blocks are + added as those pillars land.""" + + model_config = ConfigDict(strict=True, extra="forbid") + + identity: IdentityConfig | None = None + + class AgentForgeConfig(BaseModel): """Root model — `agentforge.yaml` shape. @@ -476,6 +504,7 @@ class AgentForgeConfig(BaseModel): logging: LoggingConfig = Field(default_factory=LoggingConfig) output: OutputConfig = Field(default_factory=OutputConfig) guardrail_policy: GuardrailPolicy = Field(default_factory=GuardrailPolicy) + governance: GovernanceConfig = Field(default_factory=GovernanceConfig) app: dict[str, Any] = Field(default_factory=dict) """Reserved namespace for **application** config (enh-002, feat-026 Phase 1). The framework accepts this subtree but does not interpret diff --git a/packages/agentforge-core/src/agentforge_core/contracts/__init__.py b/packages/agentforge-core/src/agentforge_core/contracts/__init__.py index eff61ba..aa59f1f 100644 --- a/packages/agentforge-core/src/agentforge_core/contracts/__init__.py +++ b/packages/agentforge-core/src/agentforge_core/contracts/__init__.py @@ -13,6 +13,7 @@ from agentforge_core.contracts.evaluator import EvalResult, Evaluator from agentforge_core.contracts.finding import Finding from agentforge_core.contracts.graph_store import GraphStore +from agentforge_core.contracts.identity import IdentityError, IdentityProvider from agentforge_core.contracts.llm import LLMClient from agentforge_core.contracts.memory import MemoryStore from agentforge_core.contracts.migrator import ( @@ -38,6 +39,8 @@ "FindingRenderer", "GraphStore", "HistoryTruncationStrategy", + "IdentityError", + "IdentityProvider", "LLMClient", "MemoryStore", "Migration", diff --git a/packages/agentforge-core/src/agentforge_core/contracts/identity.py b/packages/agentforge-core/src/agentforge_core/contracts/identity.py new file mode 100644 index 0000000..85c999b --- /dev/null +++ b/packages/agentforge-core/src/agentforge_core/contracts/identity.py @@ -0,0 +1,88 @@ +"""`IdentityProvider` — locked governance-identity contract (feat-029). + +Every agent (and the tools / services it speaks to) gets a stable, portable +`Principal` so every action has a name. A provider: + +- **issues** a principal for an agent from its declared name + owner, +- **resolves** a principal by id, +- **verifies** an inbound credential into a principal (prove who's calling), +- mints an outbound **credential** for a principal (prove who we are), +- **rotates** a principal's credential. + +Per ADR-0007 the methods on this ABC are locked once the feature ships; +adding a method is a major-version bump. Optional behaviour goes behind +`capabilities()` / `supports()`. + +The contract is vendor-neutral by construction: the `local` driver is +self-contained and offline (deterministic, no network); OIDC / SPIFFE / +cloud-IAM drivers map onto the same surface without the framework taking a +hard dependency on any of them. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Mapping + +from agentforge_core.values.auth import Principal + + +class IdentityError(ValueError): + """Raised when a credential cannot be verified into a principal, or a + principal cannot be resolved for an operation that requires one.""" + + +class IdentityProvider(ABC): + """Issues, resolves, verifies, and rotates `Principal` identities.""" + + @abstractmethod + async def issue( + self, + *, + name: str, + owner: str, + attributes: Mapping[str, str] | None = None, + ) -> Principal: + """Mint (or return the existing) principal for an agent. + + `name` is the agent's logical name; `owner` the accountable team / + human; `attributes` free-form string tags (env, region, …). The + returned `Principal.id` is stable across calls for the same + `name` (idempotent issue).""" + + @abstractmethod + async def resolve(self, principal_id: str) -> Principal | None: + """Return the principal with this id, or ``None`` if unknown.""" + + @abstractmethod + async def verify(self, token: str) -> Principal: + """Verify an inbound credential and return its principal. + + Raises: + IdentityError: the token is missing, malformed, expired, or + not issued by this provider. + """ + + @abstractmethod + async def credential(self, principal: Principal) -> str: + """Mint an outbound credential (token / assertion) that another + party can `verify()` back into `principal`.""" + + @abstractmethod + async def rotate(self, principal_id: str) -> Principal: + """Rotate the principal's signing material / credential. The + principal id is unchanged; previously minted credentials stop + verifying. + + Raises: + IdentityError: `principal_id` is unknown. + """ + + def capabilities(self) -> set[str]: + """Optional capabilities this provider honours, from a closed + vocabulary (e.g. ``"rotation"``, ``"oidc"``, ``"spiffe"``). + Default: none.""" + return set() + + def supports(self, capability: str) -> bool: + return capability in self.capabilities() diff --git a/packages/agentforge-core/src/agentforge_core/testing/__init__.py b/packages/agentforge-core/src/agentforge_core/testing/__init__.py index 6646dd6..b93c605 100644 --- a/packages/agentforge-core/src/agentforge_core/testing/__init__.py +++ b/packages/agentforge-core/src/agentforge_core/testing/__init__.py @@ -17,6 +17,7 @@ run_embedding_conformance, run_graph_conformance, run_hybrid_search_conformance, + run_identity_conformance, run_input_validator_conformance, run_memory_conformance, run_output_validator_conformance, @@ -33,6 +34,7 @@ "run_embedding_conformance", "run_graph_conformance", "run_hybrid_search_conformance", + "run_identity_conformance", "run_input_validator_conformance", "run_memory_conformance", "run_output_validator_conformance", diff --git a/packages/agentforge-core/src/agentforge_core/testing/conformance.py b/packages/agentforge-core/src/agentforge_core/testing/conformance.py index bdd23c5..d737fb9 100644 --- a/packages/agentforge-core/src/agentforge_core/testing/conformance.py +++ b/packages/agentforge-core/src/agentforge_core/testing/conformance.py @@ -32,6 +32,7 @@ async def test_my_driver_conforms() -> None: OutputValidator, ToolCallGate, ) +from agentforge_core.contracts.identity import IdentityError, IdentityProvider from agentforge_core.contracts.memory import MemoryStore from agentforge_core.contracts.reranker import Reranker from agentforge_core.contracts.strategy import ReasoningStrategy @@ -1162,3 +1163,74 @@ async def run_hybrid_search_conformance(store: VectorStore) -> None: ) finally: await store.delete(["a", "b", "c"]) + + +# ---------------------------------------------------------------------- +# Identity (feat-029) +# ---------------------------------------------------------------------- + + +async def run_identity_conformance(provider: IdentityProvider) -> None: + """Assert an `IdentityProvider` honours the locked contract. + + The provider must start empty of the principals issued here. Covers + issue→resolve round-trip + idempotency, credential/verify round-trip, + rotation, and the unknown/invalid paths. + """ + # 1. issue → resolve round-trip; issued fields are preserved. + p = await provider.issue( + name="invoice-reconciler", + owner="finance-platform", + attributes={"env": "prod"}, + ) + assert p.id, "issued principal must have a non-empty id" + assert p.owner == "finance-platform", "issue must preserve owner" + assert p.metadata.get("env") == "prod", "issue must preserve attributes" + + resolved = await provider.resolve(p.id) + assert resolved is not None, "resolve must find an issued principal" + assert resolved.id == p.id, "resolve must return the same id" + + # 2. issue is idempotent on name — same name → same id. + again = await provider.issue(name="invoice-reconciler", owner="finance-platform") + assert again.id == p.id, "issue must be idempotent on name (stable id)" + + # 3. resolve of an unknown id returns None (no raise). + assert await provider.resolve("agentforge:agent:nobody@0") is None, ( + "resolve of an unknown id must return None" + ) + + # 4. credential → verify round-trip yields the same principal. + token = await provider.credential(p) + assert isinstance(token, str), "credential must return a string" + assert token, "credential must return a non-empty token" + verified = await provider.verify(token) + assert verified.id == p.id, "verify(credential(p)) must round-trip to p" + + # 5. verify of a bogus credential raises IdentityError. + raised = False + try: + await provider.verify("not-a-real-credential") + except IdentityError: + raised = True + assert raised, "verify of an invalid credential must raise IdentityError" + + # 6. rotation keeps the id but invalidates prior credentials. + rotated = await provider.rotate(p.id) + assert rotated.id == p.id, "rotate must keep the principal id" + stale = False + try: + await provider.verify(token) + except IdentityError: + stale = True + assert stale, "a credential minted before rotate() must stop verifying" + fresh = await provider.credential(rotated) + assert (await provider.verify(fresh)).id == p.id, "post-rotation credential must verify" + + # 7. rotate of an unknown principal raises IdentityError. + unknown = False + try: + await provider.rotate("agentforge:agent:nobody@0") + except IdentityError: + unknown = True + assert unknown, "rotate of an unknown principal must raise IdentityError" diff --git a/packages/agentforge-core/src/agentforge_core/values/auth.py b/packages/agentforge-core/src/agentforge_core/values/auth.py index eb6a4d7..ef1aada 100644 --- a/packages/agentforge-core/src/agentforge_core/values/auth.py +++ b/packages/agentforge-core/src/agentforge_core/values/auth.py @@ -1,10 +1,13 @@ -"""Auth value types (feat-014). +"""Auth value types (feat-014; widened by feat-029). -`Principal` is the identity returned by an `AuthPolicy` after a -successful authentication. Carries an opaque ``id`` (the token -itself by default; an opaque user id once a real identity -provider is wired) plus optional metadata for downstream use -(role tags, tenant id, etc.). +`Principal` is the identity carried through the framework — returned by an +`AuthPolicy` after a successful authentication (feat-014) and issued / +resolved by an `IdentityProvider` (feat-029, governance identity). It +carries an opaque ``id`` (the token itself by default; a stable URN once a +real identity provider is wired — e.g. +``agentforge:agent:/@``) plus optional ``kind`` / +``owner`` and a free-form ``metadata`` attribute bag (role tags, tenant +id, env, region, …). """ from __future__ import annotations @@ -14,7 +17,20 @@ @dataclass(frozen=True) class Principal: - """Identity returned by `AuthPolicy.authenticate(...)`.""" + """Identity returned by `AuthPolicy.authenticate(...)` and issued / + resolved by `IdentityProvider` (feat-029). + + Attributes: + id: Stable identifier — a bearer token for env-backed auth, or a + URN for a governance identity provider. + kind: What the principal *is* — ``"agent"`` (default), ``"tool"``, + ``"service"``, or ``"human"``. + owner: The team / human accountable for the principal, if known. + metadata: Free-form string attributes (env, region, role tags, + tenant id) — the governance ``attributes`` bag. + """ id: str + kind: str = "agent" + owner: str | None = None metadata: dict[str, str] = field(default_factory=dict) diff --git a/packages/agentforge-governance/LICENSE b/packages/agentforge-governance/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/packages/agentforge-governance/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/agentforge-governance/NOTICE b/packages/agentforge-governance/NOTICE new file mode 100644 index 0000000..bedf327 --- /dev/null +++ b/packages/agentforge-governance/NOTICE @@ -0,0 +1,11 @@ +AgentForge +Copyright 2026 The AgentForge Authors + +This product includes software developed at the AgentForge project +(https://github.com/Scaffoldic/agentforge-py). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 diff --git a/packages/agentforge-governance/README.md b/packages/agentforge-governance/README.md new file mode 100644 index 0000000..a38045a --- /dev/null +++ b/packages/agentforge-governance/README.md @@ -0,0 +1,32 @@ +# agentforge-governance + +The governance spine for [AgentForge](https://github.com/Scaffoldic/agentforge-py). +The contracts live in `agentforge-core`; this package ships the default, +offline, zero-dependency drivers. + +**feat-029 — identity.** `LocalIdentityProvider` issues, resolves, and +verifies `Principal`s in-process with HMAC-signed credentials (no network, +deterministic for tests). Principal ids use the portable URN scheme +`agentforge:agent:/@`. + +```python +from agentforge_governance import LocalIdentityProvider + +idp = await LocalIdentityProvider.from_config(org="finance") +p = await idp.issue(name="invoice-reconciler", owner="finance-platform") +token = await idp.credential(p) +assert (await idp.verify(token)).id == p.id +``` + +Via YAML: + +```yaml +governance: + identity: + provider: local + name: invoice-reconciler + owner: finance-platform + attributes: { env: prod } +``` + +Registry, policy, and audit drivers land here as their pillars ship. diff --git a/packages/agentforge-governance/pyproject.toml b/packages/agentforge-governance/pyproject.toml new file mode 100644 index 0000000..6fc6dcb --- /dev/null +++ b/packages/agentforge-governance/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "agentforge-governance" +version = "0.3.1" +description = "Governance spine for AgentForge — identity (feat-029) and, as they land, registry / policy / audit drivers" +readme = "README.md" +requires-python = ">=3.13" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [ + {name = "The AgentForge Authors"}, +] +keywords = ["ai", "agent", "governance", "identity", "registry", "policy", "audit"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +dependencies = [ + "agentforge-core ~= 0.3.1", +] + +[project.urls] +Homepage = "https://github.com/Scaffoldic/agentforge-py" +Repository = "https://github.com/Scaffoldic/agentforge-py" +Documentation = "https://github.com/Scaffoldic/agentforge-py" +Changelog = "https://github.com/Scaffoldic/agentforge-py/blob/main/CHANGELOG.md" +Issues = "https://github.com/Scaffoldic/agentforge-py/issues" + +[project.entry-points."agentforge.identity_providers"] +local = "agentforge_governance.identity.local:LocalIdentityProvider" + +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/agentforge_governance"] + +[tool.hatch.build.targets.sdist] +include = [ + "src/agentforge_governance", + "tests", + "README.md", + "LICENSE", +] diff --git a/packages/agentforge-governance/src/agentforge_governance/__init__.py b/packages/agentforge-governance/src/agentforge_governance/__init__.py new file mode 100644 index 0000000..1855368 --- /dev/null +++ b/packages/agentforge-governance/src/agentforge_governance/__init__.py @@ -0,0 +1,27 @@ +"""AgentForge — governance spine drivers. + +The contracts live in `agentforge-core`; this package ships the +default, offline, zero-dependency drivers for each governance pillar. + +feat-029 ships the identity pillar (`LocalIdentityProvider`); registry, +policy, and audit drivers land here as their pillars ship. +""" + +from __future__ import annotations + +# Version is sourced from the installed distribution metadata so it can +# never drift from pyproject.toml (bug-024). +from importlib.metadata import PackageNotFoundError as _PkgNotFound +from importlib.metadata import version as _dist_version + +from agentforge_governance.identity.local import LocalIdentityProvider + +try: + __version__ = _dist_version("agentforge-governance") +except _PkgNotFound: # pragma: no cover - source tree without installed metadata + __version__ = "0.0.0+unknown" + +__all__ = [ + "LocalIdentityProvider", + "__version__", +] diff --git a/packages/agentforge-governance/src/agentforge_governance/identity/__init__.py b/packages/agentforge-governance/src/agentforge_governance/identity/__init__.py new file mode 100644 index 0000000..95dc2ac --- /dev/null +++ b/packages/agentforge-governance/src/agentforge_governance/identity/__init__.py @@ -0,0 +1,7 @@ +"""Identity-pillar drivers (feat-029).""" + +from __future__ import annotations + +from agentforge_governance.identity.local import LocalIdentityProvider + +__all__ = ["LocalIdentityProvider"] diff --git a/packages/agentforge-governance/src/agentforge_governance/identity/local.py b/packages/agentforge-governance/src/agentforge_governance/identity/local.py new file mode 100644 index 0000000..fa2680c --- /dev/null +++ b/packages/agentforge-governance/src/agentforge_governance/identity/local.py @@ -0,0 +1,100 @@ +"""`LocalIdentityProvider` — offline, self-contained identity (feat-029). + +The zero-ops default driver: issues, resolves, and verifies `Principal`s +entirely in-process with HMAC-signed credentials — no network, no cloud +account, deterministic for tests. The analogue of the SQLite `MemoryStore` +for the identity pillar. + +Principal ids use the stable URN scheme +``agentforge:agent:/@`` so they are portable and a +later OIDC / SPIFFE / cloud-IAM driver can map onto the same shape. +""" + +from __future__ import annotations + +import hashlib +import hmac +import secrets +from collections.abc import Mapping + +from agentforge_core.contracts.identity import IdentityError, IdentityProvider +from agentforge_core.values.auth import Principal + +_SEP = "~" # credential = "~"; absent from the URN charset + + +class LocalIdentityProvider(IdentityProvider): + """In-process identity provider with HMAC-signed credentials.""" + + def __init__(self, *, org: str = "local", version: str = "1") -> None: + self._org = org + self._version = version + self._principals: dict[str, Principal] = {} + self._secrets: dict[str, str] = {} + + @classmethod + async def from_config(cls, *, org: str = "local", version: str = "1") -> LocalIdentityProvider: + return cls(org=org, version=version) + + def _urn(self, name: str) -> str: + return f"agentforge:agent:{self._org}/{name}@{self._version}" + + @staticmethod + def _mac(principal_id: str, secret: str) -> str: + return hmac.new(secret.encode(), principal_id.encode(), hashlib.sha256).hexdigest() + + async def issue( + self, + *, + name: str, + owner: str, + attributes: Mapping[str, str] | None = None, + ) -> Principal: + principal_id = self._urn(name) + existing = self._principals.get(principal_id) + if existing is not None: + return existing # idempotent on name + principal = Principal( + id=principal_id, + kind="agent", + owner=owner, + metadata=dict(attributes or {}), + ) + self._principals[principal_id] = principal + self._secrets[principal_id] = secrets.token_hex(16) + return principal + + async def resolve(self, principal_id: str) -> Principal | None: + return self._principals.get(principal_id) + + async def credential(self, principal: Principal) -> str: + secret = self._secrets.get(principal.id) + if secret is None: + msg = f"credential: unknown principal {principal.id!r}; issue it first" + raise IdentityError(msg) + return f"{principal.id}{_SEP}{self._mac(principal.id, secret)}" + + async def verify(self, token: str) -> Principal: + principal_id, sep, signature = token.rpartition(_SEP) + if not sep: + raise IdentityError("verify: malformed credential") + secret = self._secrets.get(principal_id) + principal = self._principals.get(principal_id) + if secret is None or principal is None: + raise IdentityError("verify: unknown or revoked principal") + if not hmac.compare_digest(signature, self._mac(principal_id, secret)): + raise IdentityError("verify: credential signature mismatch") + return principal + + async def rotate(self, principal_id: str) -> Principal: + principal = self._principals.get(principal_id) + if principal is None: + raise IdentityError(f"rotate: unknown principal {principal_id!r}") + self._secrets[principal_id] = secrets.token_hex(16) # invalidates prior credentials + return principal + + def capabilities(self) -> set[str]: + return {"rotation"} + + +__all__ = ["LocalIdentityProvider"] diff --git a/packages/agentforge-governance/src/agentforge_governance/py.typed b/packages/agentforge-governance/src/agentforge_governance/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/packages/agentforge-governance/tests/unit/test_local_identity.py b/packages/agentforge-governance/tests/unit/test_local_identity.py new file mode 100644 index 0000000..2907fa1 --- /dev/null +++ b/packages/agentforge-governance/tests/unit/test_local_identity.py @@ -0,0 +1,82 @@ +"""Unit + conformance tests for `LocalIdentityProvider` (feat-029).""" + +from __future__ import annotations + +import pytest +from agentforge.cli._build import build_identity_from_config +from agentforge_core.config.schema import AgentForgeConfig +from agentforge_core.contracts.identity import IdentityError +from agentforge_core.testing import run_identity_conformance +from agentforge_core.values.auth import Principal +from agentforge_governance import LocalIdentityProvider + + +@pytest.mark.asyncio +async def test_identity_conformance() -> None: + provider = await LocalIdentityProvider.from_config(org="finance") + await run_identity_conformance(provider) + + +@pytest.mark.asyncio +async def test_urn_scheme_and_idempotent_issue() -> None: + idp = LocalIdentityProvider(org="acme", version="2") + p = await idp.issue(name="reconciler", owner="platform", attributes={"env": "prod"}) + assert p.id == "agentforge:agent:acme/reconciler@2" + assert p.kind == "agent" + assert p.owner == "platform" + assert p.metadata == {"env": "prod"} + again = await idp.issue(name="reconciler", owner="someone-else") + assert again is p # idempotent: same instance, owner not overwritten + + +@pytest.mark.asyncio +async def test_rotation_invalidates_old_credential() -> None: + idp = LocalIdentityProvider() + p = await idp.issue(name="a", owner="o") + old = await idp.credential(p) + await idp.rotate(p.id) + with pytest.raises(IdentityError, match=r"signature mismatch|revoked"): + await idp.verify(old) + assert (await idp.verify(await idp.credential(p))).id == p.id + + +@pytest.mark.asyncio +async def test_credential_for_unknown_principal_raises() -> None: + idp = LocalIdentityProvider() + with pytest.raises(IdentityError, match="unknown principal"): + await idp.credential(Principal(id="agentforge:agent:x/y@1")) + + +def test_capabilities() -> None: + idp = LocalIdentityProvider() + assert idp.capabilities() == {"rotation"} + assert idp.supports("rotation") + assert not idp.supports("oidc") + + +@pytest.mark.asyncio +async def test_build_identity_from_config() -> None: + """The `governance.identity` block resolves the `local` driver via its + entry point and issues the agent's principal.""" + cfg = AgentForgeConfig.model_validate( + { + "governance": { + "identity": { + "provider": "local", + "name": "reconciler", + "owner": "finance", + "attributes": {"env": "prod"}, + } + } + } + ) + provider = await build_identity_from_config(cfg) + assert isinstance(provider, LocalIdentityProvider) + issued = await provider.resolve("agentforge:agent:local/reconciler@1") + assert issued is not None + assert issued.owner == "finance" + + +def test_no_governance_block_is_valid_and_inert() -> None: + cfg = AgentForgeConfig() + assert cfg.governance.identity is None diff --git a/packages/agentforge/src/agentforge/cli/_build.py b/packages/agentforge/src/agentforge/cli/_build.py index f934f76..e85eac2 100644 --- a/packages/agentforge/src/agentforge/cli/_build.py +++ b/packages/agentforge/src/agentforge/cli/_build.py @@ -30,6 +30,7 @@ from agentforge_core.contracts.embedding import EmbeddingClient from agentforge_core.contracts.evaluator import Evaluator from agentforge_core.contracts.graph_store import GraphStore +from agentforge_core.contracts.identity import IdentityProvider from agentforge_core.contracts.llm import LLMClient from agentforge_core.contracts.memory import MemoryStore from agentforge_core.contracts.protocol_bridge import ProtocolBridge @@ -116,6 +117,31 @@ async def build_agent_from_config( raise +async def build_identity_from_config( + config: AgentForgeConfig, +) -> IdentityProvider | None: + """Resolve + construct `governance.identity` (feat-029). + + Returns the configured `IdentityProvider`, or None when no identity is + declared. When `name` + `owner` are set, the agent's principal is issued + eagerly so it exists before the first action. + """ + ident = config.governance.identity + if ident is None: + return None + cls = _resolve_class("identity_providers", ident.provider) + provider = await _ainstantiate_memory(cls, ident.config) + if not isinstance(provider, IdentityProvider): + msg = ( + f"Resolved identity provider {ident.provider!r} " + f"({cls.__name__}) does not implement IdentityProvider." + ) + raise ModuleError(msg) + if ident.name and ident.owner: + await provider.issue(name=ident.name, owner=ident.owner, attributes=ident.attributes) + return provider + + async def build_memory_from_config(config: AgentForgeConfig) -> MemoryStore | None: """Resolve + instantiate `modules.memory`. Returns None when absent. diff --git a/packages/agentforge/src/agentforge/cli/release_notes.json b/packages/agentforge/src/agentforge/cli/release_notes.json index f5b3b23..7f34f9b 100644 --- a/packages/agentforge/src/agentforge/cli/release_notes.json +++ b/packages/agentforge/src/agentforge/cli/release_notes.json @@ -6,6 +6,13 @@ "date": null, "entries": { "Added": [ + { + "label": "feat-029 — governance: identity (pillar 1).", + "closes": [], + "refs": [ + "feat-029" + ] + }, { "label": "feat-027 — embedded `KuzuGraphStore`.", "closes": [], diff --git a/pyproject.toml b/pyproject.toml index 54a87f4..f18b93e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ "agentforge-langfuse", "agentforge-phoenix", "agentforge-evidently", + "agentforge-governance", "agentforge-reranker-sentence-transformers", "agentforge-reranker-cohere", "agentforge-reranker-voyage", @@ -88,6 +89,7 @@ agentforge-statsd = { workspace = true } agentforge-langfuse = { workspace = true } agentforge-phoenix = { workspace = true } agentforge-evidently = { workspace = true } +agentforge-governance = { workspace = true } agentforge-reranker-sentence-transformers = { workspace = true } agentforge-reranker-cohere = { workspace = true } agentforge-reranker-voyage = { workspace = true } @@ -508,6 +510,7 @@ testpaths = [ "packages/agentforge-langfuse/tests", "packages/agentforge-phoenix/tests", "packages/agentforge-evidently/tests", + "packages/agentforge-governance/tests", "packages/agentforge-reranker-sentence-transformers/tests", "packages/agentforge-reranker-cohere/tests", "packages/agentforge-reranker-voyage/tests", @@ -577,6 +580,7 @@ source = [ "packages/agentforge-langfuse/src", "packages/agentforge-phoenix/src", "packages/agentforge-evidently/src", + "packages/agentforge-governance/src", "packages/agentforge-reranker-sentence-transformers/src", "packages/agentforge-reranker-cohere/src", "packages/agentforge-reranker-voyage/src", diff --git a/uv.lock b/uv.lock index 9f9d9fb..ff7f0ba 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,7 @@ members = [ "agentforge-core", "agentforge-eval-geval", "agentforge-evidently", + "agentforge-governance", "agentforge-guard-llamaguard", "agentforge-guard-llmguard", "agentforge-guard-nemo", @@ -251,6 +252,17 @@ requires-dist = [ ] provides-extras = ["evidently"] +[[package]] +name = "agentforge-governance" +version = "0.3.1" +source = { editable = "packages/agentforge-governance" } +dependencies = [ + { name = "agentforge-core" }, +] + +[package.metadata] +requires-dist = [{ name = "agentforge-core", editable = "packages/agentforge-core" }] + [[package]] name = "agentforge-guard-llamaguard" version = "0.3.1" @@ -878,6 +890,7 @@ dependencies = [ { name = "agentforge-core" }, { name = "agentforge-eval-geval" }, { name = "agentforge-evidently" }, + { name = "agentforge-governance" }, { name = "agentforge-guard-llamaguard" }, { name = "agentforge-guard-llmguard" }, { name = "agentforge-guard-nemo" }, @@ -933,6 +946,7 @@ requires-dist = [ { name = "agentforge-core", editable = "packages/agentforge-core" }, { name = "agentforge-eval-geval", editable = "packages/agentforge-eval-geval" }, { name = "agentforge-evidently", editable = "packages/agentforge-evidently" }, + { name = "agentforge-governance", editable = "packages/agentforge-governance" }, { name = "agentforge-guard-llamaguard", editable = "packages/agentforge-guard-llamaguard" }, { name = "agentforge-guard-llmguard", editable = "packages/agentforge-guard-llmguard" }, { name = "agentforge-guard-nemo", editable = "packages/agentforge-guard-nemo" },