From bdf3f4c538c18f8a25e439cecae51476d54275ee Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:33:04 +1200 Subject: [PATCH 01/15] docs: add agent-framework promotion plan spec Three-part plan: hdp-agent-framework PyPI package, thin PR to microsoft/agent-framework security samples, and community discussion thread cross-referencing the AutoGen validation discussion. --- docs/2026-05-09-hdp-agent-framework-design.md | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 docs/2026-05-09-hdp-agent-framework-design.md diff --git a/docs/2026-05-09-hdp-agent-framework-design.md b/docs/2026-05-09-hdp-agent-framework-design.md new file mode 100644 index 0000000..f39e1a1 --- /dev/null +++ b/docs/2026-05-09-hdp-agent-framework-design.md @@ -0,0 +1,240 @@ +# HDP × Microsoft agent-framework — Promotion Plan + +**Date:** 2026-05-09 +**Author:** Siri Dalugoda +**Status:** Approved + +--- + +## Overview + +Three coordinated deliverables that introduce HDP (Human Delegation Provenance) to the Microsoft agent-framework community: + +1. **`hdp-agent-framework`** — a new Python package published to PyPI, following the same structure and public API as `hdp-autogen` +2. **Thin PR to `microsoft/agent-framework`** — a single sample file in the existing `security/` directory that demos the integration without exposing HDP internals +3. **Discussion thread in `microsoft/agent-framework`** — community post cross-referencing the AutoGen validation thread and inviting feedback + +--- + +## Part 1 — `hdp-agent-framework` Python Package + +### Location + +`packages/hdp-agent-framework/` in the HDP monorepo, following the same layout as `packages/hdp-autogen/`. + +### Licence + +Apache License 2.0 (`license = { text = "Apache-2.0" }` in `pyproject.toml`). Every source file carries the SPDX header: + +```python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Helixar Limited +``` + +### Python version + +`requires-python = ">=3.10"` — matches `hdp-autogen` and agent-framework-core. + +### Dependencies + +```toml +dependencies = [ + "agent-framework-core>=1.0", + "cryptography>=42.0.0", + "jcs>=0.2.1", +] +``` + +### Directory structure + +``` +packages/hdp-agent-framework/ +├── pyproject.toml +├── README.md +└── src/ + └── hdp_agent_framework/ + ├── __init__.py + ├── _crypto.py # copied verbatim from hdp-autogen + ├── _types.py # copied verbatim from hdp-autogen + ├── middleware.py # NEW — ChatMiddleware + FunctionMiddleware integration + └── verify.py # copied verbatim from hdp-autogen +└── tests/ + ├── test_middleware.py + └── test_verify.py +``` + +`_crypto.py`, `_types.py`, and `verify.py` are copied verbatim from `hdp-autogen` — no logic divergence, no new IP surface. + +### Integration surface + +agent-framework exposes two middleware abstractions: + +| Class | Hook point | Used for | +|---|---|---| +| `ChatMiddleware` | Every chat client call (`ChatContext`) | Record each agent turn as a delegation hop | +| `FunctionMiddleware` | Every tool invocation (`FunctionInvocationContext`) | Enforce `authorized_tools`; record violations | + +Both hook points are attached to an `Agent` via `Agent(middleware=[...])`. + +### `middleware.py` design + +**`HdpMiddleware(ChatMiddleware)`** + +```python +class HdpMiddleware(ChatMiddleware): + def __init__( + self, + signing_key: bytes, + session_id: str, + principal: HdpPrincipal, + scope: ScopePolicy, + key_id: str = "default", + expires_in_ms: int = 86_400_000, + strict: bool = False, + ) -> None: ... + + async def process(self, context: ChatContext, call_next) -> None: + # Before call_next: extend_chain(agent_id from context metadata or class name) + # await call_next() + # After: no-op (violations already recorded by HdpFunctionMiddleware) + ... + + def configure(self, agent: Agent) -> None: + # Injects self (ChatMiddleware) + HdpFunctionMiddleware into agent.middleware + ... + + def export_token(self) -> dict | None: ... + def export_token_json(self) -> str | None: ... +``` + +**`HdpFunctionMiddleware`** (internal, injected by `configure()`) + +```python +class HdpFunctionMiddleware: + async def __call__( + self, context: FunctionInvocationContext, call_next + ) -> None: + # Check context.function.name against scope.authorized_tools + # In strict mode: raise HDPScopeViolationError + # Default mode: log + attach violation to current hop + await call_next() +``` + +**`ScopePolicy`** — identical to `hdp-autogen`: + +```python +ScopePolicy( + intent: str, + data_classification: str = "internal", + network_egress: bool = True, + persistence: bool = False, + authorized_tools: list[str] | None = None, + authorized_resources: list[str] | None = None, + max_hops: int | None = None, +) +``` + +### Public API (`__init__.py`) + +```python +from hdp_agent_framework import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain +``` + +Identical shape to `hdp-autogen` — users migrating between frameworks face zero API difference. + +### README + +Full README matching `hdp-autogen` style: + +- One-liner and `pip install hdp-agent-framework` +- Quick-start code block (configure + run + verify) +- Five design considerations table (same headings as `hdp-autogen`) +- Full API reference (`HdpMiddleware`, `ScopePolicy`, `verify_chain`) +- Error handling section (strict vs. non-blocking) +- Cross-language compatibility note (Python ↔ TypeScript token wire format) +- Releasing section (tag pattern `python/hdp-agent-framework/v*`) +- References: + - [HDP protocol spec and docs](https://helixar.ai/about/labs/hdp/) + - [arXiv paper (2604.04522)](https://arxiv.org/abs/2604.04522) + - [HDP GitHub repository](https://github.com/Helixar-AI/HDP) + - [IETF draft: draft-helixar-hdp-agentic-delegation](https://datatracker.ietf.org/doc/draft-helixar-hdp-agentic-delegation/) + - [hdp-agent-framework on PyPI](https://pypi.org/project/hdp-agent-framework/) + +### CI / release + +Tag pattern: `python/hdp-agent-framework/v*` +Pipeline: `test-hdp-agent-framework` → `vet-hdp-agent-framework` (ReleaseGuard) → `publish-hdp-agent-framework` + +--- + +## Part 2 — Thin PR to `microsoft/agent-framework` + +### Target file + +`python/samples/02-agents/security/hdp_provenance.py` + +No changes to existing files. No new directory. + +### Contents (~40 lines) + +- Apache 2.0 + Microsoft copyright header (as per repo convention) +- `pip install hdp-agent-framework` comment at top +- Single `Agent` with `HdpMiddleware` attached via `middleware.configure(agent)` +- One `agent.run(task)` call +- Offline `verify_chain()` + print result +- Links to `helixar.ai/about/labs/hdp/` and PyPI in comments + +**No crypto. No token format. No HDP internals.** The entire HDP implementation lives behind the PyPI package import. + +### PR description + +- Summary: what HDP is (one paragraph), why it matters for agent-framework +- Link to the PR we already raised against `microsoft/autogen` (#7667) as prior art +- Link to autogen community discussion #7485 for validation evidence +- Link to `helixar.ai/about/labs/hdp/` and the arXiv paper +- Test plan (install, run, verify chain prints `Valid: True`) + +--- + +## Part 3 — Discussion Thread in `microsoft/agent-framework` + +### Target + +`https://github.com/microsoft/agent-framework/discussions` — new discussion. + +### Framing + +*"HDP delegation provenance for agent-framework — same integration we built for AutoGen"* + +Content: +- One-paragraph problem statement (agents can't prove downstream actions were human-authorised) +- Link to the AutoGen community thread (#7485) as prior validation by the community +- Link to `hdp-agent-framework` on PyPI and the sample PR +- Link to `helixar.ai/about/labs/hdp/` and arXiv:2604.04522 +- Open question: *"What agent-framework patterns would benefit most from provenance tracking — single-agent tool use, hierarchical `as_tool()` delegation, or workflow orchestration?"* + +--- + +## Build sequence + +``` +1. packages/hdp-agent-framework/pyproject.toml + src/ skeleton +2. _crypto.py, _types.py, verify.py (copy from hdp-autogen) +3. middleware.py (HdpMiddleware + HdpFunctionMiddleware) +4. README.md +5. tests/ +6. CI workflow entry in .github/workflows/release.yml +7. Publish to PyPI (tag python/hdp-agent-framework/v0.1.0) +8. Fork microsoft/agent-framework, add hdp_provenance.py, open PR +9. Post discussion thread in microsoft/agent-framework +``` + +--- + +## Acceptance criteria + +| Deliverable | Done when | +|---|---| +| Package | `pip install hdp-agent-framework` works; `middleware.configure(agent)` attaches; `verify_chain()` returns `valid=True` on a real agent run | +| PR | Opens against `microsoft/agent-framework` main; CI passes; no HDP internals inlined | +| Discussion | Posted with all links; references autogen discussion and arXiv paper | From 0287f30b42266c501c724a9f6d05585fe11ae5d0 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:35:34 +1200 Subject: [PATCH 02/15] chore: gitignore spec/design docs and remove tracked spec --- .gitignore | 1 + docs/2026-05-09-hdp-agent-framework-design.md | 240 ------------------ 2 files changed, 1 insertion(+), 240 deletions(-) delete mode 100644 docs/2026-05-09-hdp-agent-framework-design.md diff --git a/.gitignore b/.gitignore index 0948065..ec38f3b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ # Claude / AI planning artifacts — excluded from repo docs/superpowers/ +docs/*-design.md .superpowers/ .DS_Store .worktrees/ diff --git a/docs/2026-05-09-hdp-agent-framework-design.md b/docs/2026-05-09-hdp-agent-framework-design.md deleted file mode 100644 index f39e1a1..0000000 --- a/docs/2026-05-09-hdp-agent-framework-design.md +++ /dev/null @@ -1,240 +0,0 @@ -# HDP × Microsoft agent-framework — Promotion Plan - -**Date:** 2026-05-09 -**Author:** Siri Dalugoda -**Status:** Approved - ---- - -## Overview - -Three coordinated deliverables that introduce HDP (Human Delegation Provenance) to the Microsoft agent-framework community: - -1. **`hdp-agent-framework`** — a new Python package published to PyPI, following the same structure and public API as `hdp-autogen` -2. **Thin PR to `microsoft/agent-framework`** — a single sample file in the existing `security/` directory that demos the integration without exposing HDP internals -3. **Discussion thread in `microsoft/agent-framework`** — community post cross-referencing the AutoGen validation thread and inviting feedback - ---- - -## Part 1 — `hdp-agent-framework` Python Package - -### Location - -`packages/hdp-agent-framework/` in the HDP monorepo, following the same layout as `packages/hdp-autogen/`. - -### Licence - -Apache License 2.0 (`license = { text = "Apache-2.0" }` in `pyproject.toml`). Every source file carries the SPDX header: - -```python -# SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Helixar Limited -``` - -### Python version - -`requires-python = ">=3.10"` — matches `hdp-autogen` and agent-framework-core. - -### Dependencies - -```toml -dependencies = [ - "agent-framework-core>=1.0", - "cryptography>=42.0.0", - "jcs>=0.2.1", -] -``` - -### Directory structure - -``` -packages/hdp-agent-framework/ -├── pyproject.toml -├── README.md -└── src/ - └── hdp_agent_framework/ - ├── __init__.py - ├── _crypto.py # copied verbatim from hdp-autogen - ├── _types.py # copied verbatim from hdp-autogen - ├── middleware.py # NEW — ChatMiddleware + FunctionMiddleware integration - └── verify.py # copied verbatim from hdp-autogen -└── tests/ - ├── test_middleware.py - └── test_verify.py -``` - -`_crypto.py`, `_types.py`, and `verify.py` are copied verbatim from `hdp-autogen` — no logic divergence, no new IP surface. - -### Integration surface - -agent-framework exposes two middleware abstractions: - -| Class | Hook point | Used for | -|---|---|---| -| `ChatMiddleware` | Every chat client call (`ChatContext`) | Record each agent turn as a delegation hop | -| `FunctionMiddleware` | Every tool invocation (`FunctionInvocationContext`) | Enforce `authorized_tools`; record violations | - -Both hook points are attached to an `Agent` via `Agent(middleware=[...])`. - -### `middleware.py` design - -**`HdpMiddleware(ChatMiddleware)`** - -```python -class HdpMiddleware(ChatMiddleware): - def __init__( - self, - signing_key: bytes, - session_id: str, - principal: HdpPrincipal, - scope: ScopePolicy, - key_id: str = "default", - expires_in_ms: int = 86_400_000, - strict: bool = False, - ) -> None: ... - - async def process(self, context: ChatContext, call_next) -> None: - # Before call_next: extend_chain(agent_id from context metadata or class name) - # await call_next() - # After: no-op (violations already recorded by HdpFunctionMiddleware) - ... - - def configure(self, agent: Agent) -> None: - # Injects self (ChatMiddleware) + HdpFunctionMiddleware into agent.middleware - ... - - def export_token(self) -> dict | None: ... - def export_token_json(self) -> str | None: ... -``` - -**`HdpFunctionMiddleware`** (internal, injected by `configure()`) - -```python -class HdpFunctionMiddleware: - async def __call__( - self, context: FunctionInvocationContext, call_next - ) -> None: - # Check context.function.name against scope.authorized_tools - # In strict mode: raise HDPScopeViolationError - # Default mode: log + attach violation to current hop - await call_next() -``` - -**`ScopePolicy`** — identical to `hdp-autogen`: - -```python -ScopePolicy( - intent: str, - data_classification: str = "internal", - network_egress: bool = True, - persistence: bool = False, - authorized_tools: list[str] | None = None, - authorized_resources: list[str] | None = None, - max_hops: int | None = None, -) -``` - -### Public API (`__init__.py`) - -```python -from hdp_agent_framework import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain -``` - -Identical shape to `hdp-autogen` — users migrating between frameworks face zero API difference. - -### README - -Full README matching `hdp-autogen` style: - -- One-liner and `pip install hdp-agent-framework` -- Quick-start code block (configure + run + verify) -- Five design considerations table (same headings as `hdp-autogen`) -- Full API reference (`HdpMiddleware`, `ScopePolicy`, `verify_chain`) -- Error handling section (strict vs. non-blocking) -- Cross-language compatibility note (Python ↔ TypeScript token wire format) -- Releasing section (tag pattern `python/hdp-agent-framework/v*`) -- References: - - [HDP protocol spec and docs](https://helixar.ai/about/labs/hdp/) - - [arXiv paper (2604.04522)](https://arxiv.org/abs/2604.04522) - - [HDP GitHub repository](https://github.com/Helixar-AI/HDP) - - [IETF draft: draft-helixar-hdp-agentic-delegation](https://datatracker.ietf.org/doc/draft-helixar-hdp-agentic-delegation/) - - [hdp-agent-framework on PyPI](https://pypi.org/project/hdp-agent-framework/) - -### CI / release - -Tag pattern: `python/hdp-agent-framework/v*` -Pipeline: `test-hdp-agent-framework` → `vet-hdp-agent-framework` (ReleaseGuard) → `publish-hdp-agent-framework` - ---- - -## Part 2 — Thin PR to `microsoft/agent-framework` - -### Target file - -`python/samples/02-agents/security/hdp_provenance.py` - -No changes to existing files. No new directory. - -### Contents (~40 lines) - -- Apache 2.0 + Microsoft copyright header (as per repo convention) -- `pip install hdp-agent-framework` comment at top -- Single `Agent` with `HdpMiddleware` attached via `middleware.configure(agent)` -- One `agent.run(task)` call -- Offline `verify_chain()` + print result -- Links to `helixar.ai/about/labs/hdp/` and PyPI in comments - -**No crypto. No token format. No HDP internals.** The entire HDP implementation lives behind the PyPI package import. - -### PR description - -- Summary: what HDP is (one paragraph), why it matters for agent-framework -- Link to the PR we already raised against `microsoft/autogen` (#7667) as prior art -- Link to autogen community discussion #7485 for validation evidence -- Link to `helixar.ai/about/labs/hdp/` and the arXiv paper -- Test plan (install, run, verify chain prints `Valid: True`) - ---- - -## Part 3 — Discussion Thread in `microsoft/agent-framework` - -### Target - -`https://github.com/microsoft/agent-framework/discussions` — new discussion. - -### Framing - -*"HDP delegation provenance for agent-framework — same integration we built for AutoGen"* - -Content: -- One-paragraph problem statement (agents can't prove downstream actions were human-authorised) -- Link to the AutoGen community thread (#7485) as prior validation by the community -- Link to `hdp-agent-framework` on PyPI and the sample PR -- Link to `helixar.ai/about/labs/hdp/` and arXiv:2604.04522 -- Open question: *"What agent-framework patterns would benefit most from provenance tracking — single-agent tool use, hierarchical `as_tool()` delegation, or workflow orchestration?"* - ---- - -## Build sequence - -``` -1. packages/hdp-agent-framework/pyproject.toml + src/ skeleton -2. _crypto.py, _types.py, verify.py (copy from hdp-autogen) -3. middleware.py (HdpMiddleware + HdpFunctionMiddleware) -4. README.md -5. tests/ -6. CI workflow entry in .github/workflows/release.yml -7. Publish to PyPI (tag python/hdp-agent-framework/v0.1.0) -8. Fork microsoft/agent-framework, add hdp_provenance.py, open PR -9. Post discussion thread in microsoft/agent-framework -``` - ---- - -## Acceptance criteria - -| Deliverable | Done when | -|---|---| -| Package | `pip install hdp-agent-framework` works; `middleware.configure(agent)` attaches; `verify_chain()` returns `valid=True` on a real agent run | -| PR | Opens against `microsoft/agent-framework` main; CI passes; no HDP internals inlined | -| Discussion | Posted with all links; references autogen discussion and arXiv paper | From cc96117be644123651f22af9bdf67c504acea168 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:42:36 +1200 Subject: [PATCH 03/15] feat(hdp-agent-framework): scaffold package structure --- .../hdp-agent-framework/.releaseguard.yml | 54 +++++++++++++++++++ packages/hdp-agent-framework/pyproject.toml | 34 ++++++++++++ .../src/hdp_agent_framework/__init__.py | 3 ++ 3 files changed, 91 insertions(+) create mode 100644 packages/hdp-agent-framework/.releaseguard.yml create mode 100644 packages/hdp-agent-framework/pyproject.toml create mode 100644 packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py diff --git a/packages/hdp-agent-framework/.releaseguard.yml b/packages/hdp-agent-framework/.releaseguard.yml new file mode 100644 index 0000000..14152db --- /dev/null +++ b/packages/hdp-agent-framework/.releaseguard.yml @@ -0,0 +1,54 @@ +# ReleaseGuard policy for hdp-agent-framework Python artifacts +version: 2 + +project: + name: hdp-agent-framework + mode: release + +inputs: + - path: ./dist + type: directory + +sbom: + enabled: true + ecosystems: [python] + formats: [cyclonedx] + enrich_cve: false + +scanning: + secrets: + enabled: true + metadata: + enabled: true + fail_on_source_maps: false + fail_on_internal_urls: false + fail_on_build_paths: false + unexpected_files: + enabled: true + deny: + - ".env" + - "*.bak" + - "*.tmp" + - "*.key" + - "*.pem" + licenses: + enabled: true + require: + - LICENSE + +transforms: + add_checksums: false + add_manifest: false + +policy: + fail_on: + - severity: critical + - category: secret + warn_on: + - severity: high + +output: + reports: + - cli + - sarif + directory: ./.releaseguard diff --git a/packages/hdp-agent-framework/pyproject.toml b/packages/hdp-agent-framework/pyproject.toml new file mode 100644 index 0000000..a6099d0 --- /dev/null +++ b/packages/hdp-agent-framework/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "hdp-agent-framework" +version = "0.1.0" +description = "HDP (Human Delegation Provenance) middleware for Microsoft agent-framework — cryptographic audit trail for multi-agent delegation" +readme = "README.md" +license = { text = "Apache-2.0" } +requires-python = ">=3.10" +dependencies = [ + "agent-framework-core>=1.0", + "cryptography>=42.0.0", + "jcs>=0.2.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", +] + +[project.urls] +Homepage = "https://github.com/Helixar-AI/HDP" +Repository = "https://github.com/Helixar-AI/HDP" +Documentation = "https://helixar.ai/about/labs/hdp/" + +[tool.hatch.build.targets.wheel] +packages = ["src/hdp_agent_framework"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py new file mode 100644 index 0000000..ee092d9 --- /dev/null +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) Helixar Limited +"""hdp-agent-framework — HDP delegation provenance middleware for Microsoft agent-framework.""" From 97d5ade6f55b04bbc20d68c77cb7a9460e86f3ae Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:44:34 +1200 Subject: [PATCH 04/15] feat(hdp-agent-framework): add tests directory placeholder --- packages/hdp-agent-framework/tests/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/hdp-agent-framework/tests/__init__.py diff --git a/packages/hdp-agent-framework/tests/__init__.py b/packages/hdp-agent-framework/tests/__init__.py new file mode 100644 index 0000000..9881313 --- /dev/null +++ b/packages/hdp-agent-framework/tests/__init__.py @@ -0,0 +1 @@ +# SPDX-License-Identifier: Apache-2.0 From 6c58cabe3e4eb5e310fa3ab9c84a41bedac187d7 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:45:55 +1200 Subject: [PATCH 05/15] chore(hdp-agent-framework): fix releaseguard comment block and copyright year --- packages/hdp-agent-framework/.releaseguard.yml | 6 +++++- .../hdp-agent-framework/src/hdp_agent_framework/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/hdp-agent-framework/.releaseguard.yml b/packages/hdp-agent-framework/.releaseguard.yml index 14152db..935982b 100644 --- a/packages/hdp-agent-framework/.releaseguard.yml +++ b/packages/hdp-agent-framework/.releaseguard.yml @@ -1,4 +1,8 @@ # ReleaseGuard policy for hdp-agent-framework Python artifacts +# https://github.com/Helixar-AI/ReleaseGuard +# +# Scans the built wheel and sdist before they are published to PyPI. +# Run locally: releaseguard check ./dist version: 2 project: @@ -37,7 +41,7 @@ scanning: - LICENSE transforms: - add_checksums: false + add_checksums: false # PyPI rejects non-dist files — checksums are handled by PyPI itself add_manifest: false policy: diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py index ee092d9..49d323e 100644 --- a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py @@ -1,3 +1,3 @@ # SPDX-License-Identifier: Apache-2.0 -# Copyright (c) Helixar Limited +# Copyright (c) 2026 Helixar Limited """hdp-agent-framework — HDP delegation provenance middleware for Microsoft agent-framework.""" From 69f0ccbadbeee9c0396ed5deb21aba6c5d7acfeb Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:47:09 +1200 Subject: [PATCH 06/15] feat(hdp-agent-framework): copy shared crypto files from hdp-autogen --- .../src/hdp_agent_framework/_crypto.py | 80 ++++++++++ .../src/hdp_agent_framework/_types.py | 71 +++++++++ .../src/hdp_agent_framework/verify.py | 141 ++++++++++++++++++ 3 files changed, 292 insertions(+) create mode 100644 packages/hdp-agent-framework/src/hdp_agent_framework/_crypto.py create mode 100644 packages/hdp-agent-framework/src/hdp_agent_framework/_types.py create mode 100644 packages/hdp-agent-framework/src/hdp_agent_framework/verify.py diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/_crypto.py b/packages/hdp-agent-framework/src/hdp_agent_framework/_crypto.py new file mode 100644 index 0000000..5017202 --- /dev/null +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/_crypto.py @@ -0,0 +1,80 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""Cryptographic primitives for HDP — Ed25519 signing/verification with RFC 8785 canonical JSON. + +Matches the signing scheme in the TypeScript SDK (src/crypto/sign.ts + src/crypto/verify.ts): + - Root: canonicalize({header, principal, scope}) → Ed25519 → base64url + - Hop: canonicalize({chain: [...], root_sig: }) → Ed25519 → base64url +""" + +from __future__ import annotations + +import base64 +from typing import Any + +import jcs +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + + +def _b64url(sig_bytes: bytes) -> str: + """Encode bytes as unpadded base64url (matches Buffer.toString('base64url') in Node).""" + return base64.urlsafe_b64encode(sig_bytes).rstrip(b"=").decode() + + +def _canonicalize(obj: Any) -> bytes: + """RFC 8785 canonical JSON bytes.""" + return jcs.canonicalize(obj) + + +def sign_root(unsigned_token: dict, private_key_bytes: bytes, kid: str) -> dict: + """Sign the root token over {header, principal, scope} and return a signature dict.""" + subset = {f: unsigned_token[f] for f in ["header", "principal", "scope"] if f in unsigned_token} + message = _canonicalize(subset) + key = Ed25519PrivateKey.from_private_bytes(private_key_bytes) + sig_bytes = key.sign(message) + return { + "alg": "Ed25519", + "kid": kid, + "value": _b64url(sig_bytes), + "signed_fields": ["header", "principal", "scope"], + } + + +def sign_hop(cumulative_chain: list[dict], root_sig_value: str, private_key_bytes: bytes) -> str: + """Sign a hop over the cumulative chain + root signature value.""" + payload = {"chain": cumulative_chain, "root_sig": root_sig_value} + message = _canonicalize(payload) + key = Ed25519PrivateKey.from_private_bytes(private_key_bytes) + sig_bytes = key.sign(message) + return _b64url(sig_bytes) + + +def _b64url_decode(s: str) -> bytes: + """Decode unpadded base64url string to bytes.""" + padding = 4 - len(s) % 4 + return base64.urlsafe_b64decode(s + "=" * padding) + + +def verify_root(token: dict, public_key: Ed25519PublicKey) -> bool: + """Verify the root signature over {header, principal, scope}.""" + try: + subset = {f: token[f] for f in ["header", "principal", "scope"] if f in token} + message = _canonicalize(subset) + sig_bytes = _b64url_decode(token["signature"]["value"]) + public_key.verify(sig_bytes, message) + return True + except (InvalidSignature, KeyError, Exception): + return False + + +def verify_hop(cumulative_chain: list[dict], root_sig_value: str, hop_signature: str, public_key: Ed25519PublicKey) -> bool: + """Verify a single hop signature over the cumulative chain + root sig value.""" + try: + payload = {"chain": cumulative_chain, "root_sig": root_sig_value} + message = _canonicalize(payload) + sig_bytes = _b64url_decode(hop_signature) + public_key.verify(sig_bytes, message) + return True + except (InvalidSignature, Exception): + return False diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/_types.py b/packages/hdp-agent-framework/src/hdp_agent_framework/_types.py new file mode 100644 index 0000000..d64cb10 --- /dev/null +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/_types.py @@ -0,0 +1,71 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""Python types mirroring the HDP TypeScript SDK schema.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Literal, Optional + +DataClassification = Literal["public", "internal", "confidential", "restricted"] +AgentType = Literal["orchestrator", "sub-agent", "tool-executor", "custom"] +PrincipalIdType = Literal["email", "uuid", "did", "poh", "opaque"] + + +@dataclass +class HdpHeader: + token_id: str + issued_at: int + expires_at: int + session_id: str + version: str = "0.1" + parent_token_id: Optional[str] = None + + +@dataclass +class HdpPrincipal: + id: str + id_type: PrincipalIdType + display_name: Optional[str] = None + metadata: Optional[dict[str, Any]] = None + + +@dataclass +class HdpScope: + intent: str + data_classification: DataClassification + network_egress: bool + persistence: bool + authorized_tools: Optional[list[str]] = None + authorized_resources: Optional[list[str]] = None + max_hops: Optional[int] = None + + +@dataclass +class HdpSignature: + alg: str + kid: str + value: str + signed_fields: list[str] = field(default_factory=lambda: ["header", "principal", "scope"]) + + +@dataclass +class HopRecord: + seq: int + agent_id: str + agent_type: AgentType + timestamp: int + action_summary: str + parent_hop: int + hop_signature: str + agent_fingerprint: Optional[str] = None + + +@dataclass +class HdpToken: + hdp: str + header: HdpHeader + principal: HdpPrincipal + scope: HdpScope + chain: list[HopRecord] + signature: HdpSignature diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/verify.py b/packages/hdp-agent-framework/src/hdp_agent_framework/verify.py new file mode 100644 index 0000000..c4ba859 --- /dev/null +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/verify.py @@ -0,0 +1,141 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""Offline chain verification utilities for HDP tokens. + +Design consideration #4: Verification Endpoint +Enables downstream systems to validate a complete delegation chain using +only the human's public key. Returns a structured result with validity +status, per-hop outcomes, violations, and depth metrics. + +Usage: + from hdp_agent_framework import verify_chain + + result = verify_chain(token_dict, public_key_bytes) + if result.valid: + print(f"Chain verified: {result.hop_count} hops") + else: + print(f"Violations: {result.violations}") +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass, field + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from ._crypto import verify_hop, verify_root + + +@dataclass +class HopVerification: + """Per-hop verification outcome.""" + seq: int + agent_id: str + valid: bool + reason: str = "" + + +@dataclass +class VerificationResult: + """Result of verifying an HDP token's complete delegation chain.""" + valid: bool + token_id: str + session_id: str + hop_count: int + hop_results: list[HopVerification] = field(default_factory=list) + violations: list[str] = field(default_factory=list) + + @property + def depth(self) -> int: + return self.hop_count + + +def verify_chain(token: dict, public_key: Ed25519PublicKey | bytes) -> VerificationResult: + """Verify a complete HDP token — root signature and every hop in the chain. + + Args: + token: A token dict as returned by ``HdpMiddleware.export_token()``. + public_key: The human's Ed25519 public key. Pass either an + ``Ed25519PublicKey`` instance or the raw 32-byte public key + bytes (as produced by ``Ed25519PrivateKey.public_key().public_bytes_raw()``). + + Returns: + VerificationResult with ``valid=True`` only if every signature checks out + and no structural violations are detected. + """ + if isinstance(public_key, (bytes, bytearray)): + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey as _PK + from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + # Accept raw 32-byte public key + pub = _load_raw_public_key(public_key) + else: + pub = public_key + + token_id = token.get("header", {}).get("token_id", "unknown") + session_id = token.get("header", {}).get("session_id", "unknown") + chain: list[dict] = token.get("chain", []) + violations: list[str] = [] + hop_results: list[HopVerification] = [] + + # 1. Verify root signature + if not verify_root(token, pub): + violations.append("Root signature invalid") + return VerificationResult( + valid=False, + token_id=token_id, + session_id=session_id, + hop_count=len(chain), + violations=violations, + ) + + # 2. Check token expiry + expires_at = token.get("header", {}).get("expires_at", 0) + now_ms = int(time.time() * 1000) + if expires_at and now_ms > expires_at: + violations.append(f"Token expired at {expires_at}") + + # 3. Check max_hops + max_hops = token.get("scope", {}).get("max_hops") + if max_hops is not None and len(chain) > max_hops: + violations.append(f"Chain depth {len(chain)} exceeds max_hops {max_hops}") + + # 4. Verify each hop signature over the cumulative chain + root_sig_value: str = token["signature"]["value"] + for i, hop in enumerate(chain): + hop_sig = hop.get("hop_signature", "") + unsigned_hop = {k: v for k, v in hop.items() if k != "hop_signature"} + # Cumulative = all hops up to and including this one (unsigned version of current) + prior_signed = chain[:i] + cumulative = [*prior_signed, unsigned_hop] + + ok = verify_hop(cumulative, root_sig_value, hop_sig, pub) + hop_results.append(HopVerification( + seq=hop.get("seq", i + 1), + agent_id=hop.get("agent_id", "unknown"), + valid=ok, + reason="" if ok else "Hop signature invalid", + )) + if not ok: + violations.append(f"Hop {hop.get('seq', i + 1)} ({hop.get('agent_id', '?')}) signature invalid") + + # 5. Check sequential seq numbers + for j, hop in enumerate(chain): + if hop.get("seq") != j + 1: + violations.append(f"Non-sequential seq at position {j}: expected {j + 1}, got {hop.get('seq')}") + + valid = len(violations) == 0 + return VerificationResult( + valid=valid, + token_id=token_id, + session_id=session_id, + hop_count=len(chain), + hop_results=hop_results, + violations=violations, + ) + + +def _load_raw_public_key(raw_bytes: bytes) -> Ed25519PublicKey: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + import cryptography.hazmat.primitives.asymmetric.ed25519 as _ed + return _ed.Ed25519PublicKey.from_public_bytes(raw_bytes) From 72b35056943f972df27ea5d25bfd4c9cbf5c18d6 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:50:37 +1200 Subject: [PATCH 07/15] test(hdp-agent-framework): write failing tests for middleware and verify --- packages/hdp-agent-framework/README.md | 3 + .../tests/test_middleware.py | 298 ++++++++++++++++++ .../hdp-agent-framework/tests/test_verify.py | 242 ++++++++++++++ 3 files changed, 543 insertions(+) create mode 100644 packages/hdp-agent-framework/README.md create mode 100644 packages/hdp-agent-framework/tests/test_middleware.py create mode 100644 packages/hdp-agent-framework/tests/test_verify.py diff --git a/packages/hdp-agent-framework/README.md b/packages/hdp-agent-framework/README.md new file mode 100644 index 0000000..5a74642 --- /dev/null +++ b/packages/hdp-agent-framework/README.md @@ -0,0 +1,3 @@ +# hdp-agent-framework + +HDP (Human Delegation Provenance) middleware for Microsoft agent-framework. diff --git a/packages/hdp-agent-framework/tests/test_middleware.py b/packages/hdp-agent-framework/tests/test_middleware.py new file mode 100644 index 0000000..3c471b9 --- /dev/null +++ b/packages/hdp-agent-framework/tests/test_middleware.py @@ -0,0 +1,298 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""Failing tests for HdpMiddleware (agent-framework). + +All tests in this file MUST FAIL until middleware.py is implemented (Task 4). +Expected failure reason: ImportError — HdpMiddleware, ScopePolicy, +HDPScopeViolationError are not yet exported from hdp_agent_framework. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from unittest.mock import AsyncMock + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + +from hdp_agent_framework import ( + HdpMiddleware, + HdpPrincipal, + HDPScopeViolationError, + ScopePolicy, + verify_chain, +) + + +# --------------------------------------------------------------------------- +# Fakes — agent-framework duck-typed stand-ins (no real installation needed) +# --------------------------------------------------------------------------- + +@dataclass +class FakeFunctionInfo: + name: str + + +@dataclass +class FakeFunctionContext: + function: FakeFunctionInfo + + +@dataclass +class FakeChatContext: + metadata: dict = field(default_factory=dict) + + +class FakeAgent: + def __init__(self, name: str = "agent-1"): + self.name = name + self.middleware: list = [] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _generate_key() -> tuple[bytes, Ed25519PublicKey]: + priv = Ed25519PrivateKey.generate() + pub = priv.public_key() + return priv.private_bytes_raw(), pub + + +def _make_middleware( + scope: ScopePolicy | None = None, + **kwargs, +) -> tuple[HdpMiddleware, bytes, Ed25519PublicKey]: + key, pub = _generate_key() + mw = HdpMiddleware( + signing_key=key, + session_id="test-session", + principal=HdpPrincipal(id="user@test.com", id_type="email"), + scope=scope or ScopePolicy(intent="Test intent"), + **kwargs, + ) + return mw, key, pub + + +async def _process(mw: HdpMiddleware, agent_name: str = "agent-1") -> None: + """Run mw.process() with a fake context carrying the given agent_name.""" + ctx = FakeChatContext(metadata={"agent_name": agent_name}) + await mw.process(ctx, AsyncMock()) + + +async def _function_middleware_call(mw: HdpMiddleware, tool_name: str) -> None: + """Invoke mw._function_middleware with a fake function context.""" + ctx = FakeFunctionContext(function=FakeFunctionInfo(name=tool_name)) + await mw._function_middleware(ctx, AsyncMock()) + + +# --------------------------------------------------------------------------- +# configure() +# --------------------------------------------------------------------------- + +class TestConfigure: + def test_configure_appends_middleware_to_agent(self): + mw, _, _ = _make_middleware() + agent = FakeAgent() + mw.configure(agent) + assert mw in agent.middleware + + def test_configure_appends_function_middleware_to_agent(self): + mw, _, _ = _make_middleware() + agent = FakeAgent() + mw.configure(agent) + assert mw._function_middleware in agent.middleware + + def test_configure_total_two_items_added(self): + mw, _, _ = _make_middleware() + agent = FakeAgent() + mw.configure(agent) + assert len(agent.middleware) == 2 + + def test_configure_is_idempotent(self): + """Calling configure() twice must not add duplicates.""" + mw, _, _ = _make_middleware() + agent = FakeAgent() + mw.configure(agent) + mw.configure(agent) + assert len(agent.middleware) == 2 + + +# --------------------------------------------------------------------------- +# Lazy root issuance +# --------------------------------------------------------------------------- + +class TestLazyRootIssuance: + def test_export_token_none_before_process(self): + mw, _, _ = _make_middleware() + assert mw.export_token() is None + + @pytest.mark.asyncio + async def test_export_token_valid_after_first_process(self): + mw, _, _ = _make_middleware() + await _process(mw) + token = mw.export_token() + assert token is not None + assert token["hdp"] == "0.1" + + @pytest.mark.asyncio + async def test_export_token_has_session_id_after_process(self): + mw, _, _ = _make_middleware() + await _process(mw) + token = mw.export_token() + assert token["header"]["session_id"] == "test-session" + + +# --------------------------------------------------------------------------- +# process() — chain extension +# --------------------------------------------------------------------------- + +class TestProcessChainExtension: + @pytest.mark.asyncio + async def test_each_process_call_appends_one_hop(self): + mw, _, _ = _make_middleware() + await _process(mw, "agent-1") + await _process(mw, "agent-2") + assert len(mw.export_token()["chain"]) == 2 + + @pytest.mark.asyncio + async def test_agent_name_from_context_metadata_used_as_agent_id(self): + mw, _, _ = _make_middleware() + await _process(mw, "my-worker-agent") + hop = mw.export_token()["chain"][0] + assert hop["agent_id"] == "my-worker-agent" + + @pytest.mark.asyncio + async def test_hop_seq_values_are_sequential_from_one(self): + mw, _, _ = _make_middleware() + for i in range(3): + await _process(mw, f"agent-{i}") + seqs = [h["seq"] for h in mw.export_token()["chain"]] + assert seqs == [1, 2, 3] + + @pytest.mark.asyncio + async def test_hop_signatures_are_verifiable(self): + mw, _, pub = _make_middleware() + await _process(mw, "signer-agent") + token = mw.export_token() + result = verify_chain(token, pub) + assert result.valid + + @pytest.mark.asyncio + async def test_call_next_is_called_during_process(self): + mw, _, _ = _make_middleware() + ctx = FakeChatContext(metadata={"agent_name": "a"}) + call_next = AsyncMock() + await mw.process(ctx, call_next) + call_next.assert_awaited_once() + + +# --------------------------------------------------------------------------- +# max_hops enforcement +# --------------------------------------------------------------------------- + +class TestMaxHopsEnforcement: + @pytest.mark.asyncio + async def test_chain_capped_at_max_hops(self): + mw, _, _ = _make_middleware(scope=ScopePolicy(intent="x", max_hops=2)) + for i in range(5): + await _process(mw, f"agent-{i}") + assert len(mw.export_token()["chain"]) == 2 + + @pytest.mark.asyncio + async def test_chain_does_not_grow_beyond_max_hops(self): + mw, _, _ = _make_middleware(scope=ScopePolicy(intent="x", max_hops=1)) + await _process(mw, "first") + await _process(mw, "second") + await _process(mw, "third") + assert len(mw.export_token()["chain"]) == 1 + + +# --------------------------------------------------------------------------- +# _function_middleware — scope enforcement +# --------------------------------------------------------------------------- + +class TestFunctionMiddlewareScopeEnforcement: + @pytest.mark.asyncio + async def test_none_authorized_tools_allows_any_tool(self): + mw, _, _ = _make_middleware(scope=ScopePolicy(intent="x", authorized_tools=None)) + await _process(mw) + call_next = AsyncMock() + ctx = FakeFunctionContext(function=FakeFunctionInfo(name="any_tool")) + await mw._function_middleware(ctx, call_next) + call_next.assert_awaited_once() + + @pytest.mark.asyncio + async def test_unauthorized_tool_recorded_as_violation(self): + mw, _, _ = _make_middleware( + scope=ScopePolicy(intent="x", authorized_tools=["allowed_tool"]), + ) + await _process(mw) + await _function_middleware_call(mw, "forbidden_tool") + token = mw.export_token() + violations = ( + token.get("scope", {}) + .get("extensions", {}) + .get("scope_violations", []) + ) + assert len(violations) >= 1 + assert any(v.get("tool") == "forbidden_tool" for v in violations) + + @pytest.mark.asyncio + async def test_strict_mode_raises_on_unauthorized_tool(self): + mw, _, _ = _make_middleware( + scope=ScopePolicy(intent="x", authorized_tools=["allowed_tool"]), + strict=True, + ) + await _process(mw) + with pytest.raises(HDPScopeViolationError): + await _function_middleware_call(mw, "forbidden_tool") + + @pytest.mark.asyncio + async def test_strict_mode_does_not_raise_on_authorized_tool(self): + mw, _, _ = _make_middleware( + scope=ScopePolicy(intent="x", authorized_tools=["safe_tool"]), + strict=True, + ) + await _process(mw) + # Must not raise + await _function_middleware_call(mw, "safe_tool") + + +# --------------------------------------------------------------------------- +# export_token_json() +# --------------------------------------------------------------------------- + +class TestExportTokenJson: + def test_returns_none_when_no_token_issued(self): + mw, _, _ = _make_middleware() + assert mw.export_token_json() is None + + @pytest.mark.asyncio + async def test_returns_parseable_json_string_after_process(self): + mw, _, _ = _make_middleware() + await _process(mw) + raw = mw.export_token_json() + assert raw is not None + parsed = json.loads(raw) + assert parsed["hdp"] == "0.1" + + +# --------------------------------------------------------------------------- +# Bad key — graceful failure (non-blocking) +# --------------------------------------------------------------------------- + +class TestBadKeyGracefulFailure: + @pytest.mark.asyncio + async def test_bad_key_does_not_raise(self): + mw = HdpMiddleware( + signing_key=b"\x00" * 5, + session_id="s", + principal=HdpPrincipal(id="u", id_type="opaque"), + scope=ScopePolicy(intent="x"), + ) + ctx = FakeChatContext(metadata={"agent_name": "bad-key-agent"}) + # Should not raise — non-blocking design + await mw.process(ctx, AsyncMock()) + assert mw.export_token() is None diff --git a/packages/hdp-agent-framework/tests/test_verify.py b/packages/hdp-agent-framework/tests/test_verify.py new file mode 100644 index 0000000..807b8d6 --- /dev/null +++ b/packages/hdp-agent-framework/tests/test_verify.py @@ -0,0 +1,242 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""Unit tests for verify_chain() — pure verification layer tests. + +These tests build tokens directly with _crypto primitives and do NOT use +HdpMiddleware, so they are independent of the middleware.py implementation. +Most tests here can pass before Task 4 is complete. +""" + +from __future__ import annotations + +import time +import uuid + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey + +from hdp_agent_framework._crypto import sign_hop, sign_root +from hdp_agent_framework.verify import verify_chain + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _generate_key() -> tuple[bytes, Ed25519PublicKey]: + priv = Ed25519PrivateKey.generate() + pub = priv.public_key() + return priv.private_bytes_raw(), pub + + +def _build_root_token( + priv_bytes: bytes, + session_id: str = "test-session", + expires_offset_ms: int = 24 * 60 * 60 * 1000, + kid: str = "default", +) -> dict: + """Build and sign a root token dict.""" + now = int(time.time() * 1000) + unsigned: dict = { + "hdp": "0.1", + "header": { + "token_id": str(uuid.uuid4()), + "issued_at": now, + "expires_at": now + expires_offset_ms, + "session_id": session_id, + "version": "0.1", + }, + "principal": { + "id": "user@test.com", + "id_type": "email", + }, + "scope": { + "intent": "Test intent", + "data_classification": "internal", + "network_egress": True, + "persistence": False, + }, + "chain": [], + } + signature = sign_root(unsigned, priv_bytes, kid) + return {**unsigned, "signature": signature} + + +def _append_hop(token: dict, priv_bytes: bytes, agent_id: str) -> dict: + """Return a new token dict with one more signed hop appended.""" + seq = len(token["chain"]) + 1 + now = int(time.time() * 1000) + unsigned_hop: dict = { + "seq": seq, + "agent_id": agent_id, + "agent_type": "sub-agent", + "timestamp": now, + "action_summary": f"hop {seq}", + "parent_hop": seq - 1, + } + cumulative = [*token["chain"], unsigned_hop] + hop_sig = sign_hop(cumulative, token["signature"]["value"], priv_bytes) + signed_hop = {**unsigned_hop, "hop_signature": hop_sig} + new_chain = [*token["chain"], signed_hop] + return {**token, "chain": new_chain} + + +# --------------------------------------------------------------------------- +# Valid chain +# --------------------------------------------------------------------------- + +class TestValidChain: + def test_root_only_chain_is_valid(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + result = verify_chain(token, pub) + assert result.valid + assert result.hop_count == 0 + assert result.violations == [] + + def test_valid_two_hop_chain(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token = _append_hop(token, priv, "agent-alpha") + token = _append_hop(token, priv, "agent-beta") + result = verify_chain(token, pub) + assert result.valid + assert result.hop_count == 2 + assert result.violations == [] + + def test_hop_count_matches_chain_length(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + for i in range(4): + token = _append_hop(token, priv, f"agent-{i}") + result = verify_chain(token, pub) + assert result.hop_count == 4 + + +# --------------------------------------------------------------------------- +# Tampered root signature +# --------------------------------------------------------------------------- + +class TestTamperedRootSignature: + def test_tampered_root_sig_is_invalid(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token["signature"]["value"] = token["signature"]["value"][:-4] + "XXXX" + result = verify_chain(token, pub) + assert result.valid is False + + def test_tampered_root_sig_mentions_root_in_violation(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token["signature"]["value"] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + result = verify_chain(token, pub) + assert any("Root" in v for v in result.violations) + + +# --------------------------------------------------------------------------- +# Tampered hop signature +# --------------------------------------------------------------------------- + +class TestTamperedHopSignature: + def test_tampered_hop_sig_is_invalid(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token = _append_hop(token, priv, "agent-one") + token["chain"][0]["hop_signature"] = "AAAA" + result = verify_chain(token, pub) + assert result.valid is False + + def test_tampered_second_hop_sig_is_invalid(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token = _append_hop(token, priv, "agent-one") + token = _append_hop(token, priv, "agent-two") + token["chain"][1]["hop_signature"] = "AAAA" + result = verify_chain(token, pub) + assert result.valid is False + + +# --------------------------------------------------------------------------- +# Wrong public key +# --------------------------------------------------------------------------- + +class TestWrongPublicKey: + def test_wrong_key_fails_root_verification(self): + priv, _ = _generate_key() + _, other_pub = _generate_key() + token = _build_root_token(priv) + result = verify_chain(token, other_pub) + assert result.valid is False + + def test_wrong_key_with_hops_still_fails(self): + priv, _ = _generate_key() + _, other_pub = _generate_key() + token = _build_root_token(priv) + token = _append_hop(token, priv, "agent-x") + result = verify_chain(token, other_pub) + assert result.valid is False + + +# --------------------------------------------------------------------------- +# Empty chain +# --------------------------------------------------------------------------- + +class TestEmptyChain: + def test_empty_chain_is_valid(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + result = verify_chain(token, pub) + assert result.valid is True + assert result.hop_count == 0 + + def test_empty_chain_depth_is_zero(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + result = verify_chain(token, pub) + assert result.depth == 0 + + +# --------------------------------------------------------------------------- +# Expired token +# --------------------------------------------------------------------------- + +class TestExpiredToken: + def test_expired_token_has_violation(self): + priv, pub = _generate_key() + # expires_at 1 second in the past + token = _build_root_token(priv, expires_offset_ms=-1000) + result = verify_chain(token, pub) + assert any("expired" in v.lower() for v in result.violations) + + def test_expired_token_valid_flag_is_false(self): + priv, pub = _generate_key() + token = _build_root_token(priv, expires_offset_ms=-1000) + result = verify_chain(token, pub) + assert result.valid is False + + +# --------------------------------------------------------------------------- +# Raw public key bytes accepted +# --------------------------------------------------------------------------- + +class TestRawPublicKeyBytes: + def test_verify_accepts_raw_32_byte_public_key(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + raw_bytes = pub.public_bytes_raw() + result = verify_chain(token, raw_bytes) + assert result.valid + + def test_verify_raw_bytes_catches_wrong_key(self): + priv, _ = _generate_key() + _, other_pub = _generate_key() + token = _build_root_token(priv) + result = verify_chain(token, other_pub.public_bytes_raw()) + assert result.valid is False + + def test_raw_bytes_with_hops_verifies_correctly(self): + priv, pub = _generate_key() + token = _build_root_token(priv) + token = _append_hop(token, priv, "raw-agent") + result = verify_chain(token, pub.public_bytes_raw()) + assert result.valid From a2ef9f46651b2ce0f4074b319deb73b3d2954687 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:52:42 +1200 Subject: [PATCH 08/15] test(hdp-agent-framework): tighten scope enforcement and empty-chain tests --- .gitignore | 1 + packages/hdp-agent-framework/tests/test_middleware.py | 4 ++++ packages/hdp-agent-framework/tests/test_verify.py | 7 ------- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ec38f3b..8e3aff8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +.venv/ *.js.map __pycache__/ *.pyc diff --git a/packages/hdp-agent-framework/tests/test_middleware.py b/packages/hdp-agent-framework/tests/test_middleware.py index 3c471b9..fbda4e4 100644 --- a/packages/hdp-agent-framework/tests/test_middleware.py +++ b/packages/hdp-agent-framework/tests/test_middleware.py @@ -222,6 +222,10 @@ async def test_none_authorized_tools_allows_any_tool(self): ctx = FakeFunctionContext(function=FakeFunctionInfo(name="any_tool")) await mw._function_middleware(ctx, call_next) call_next.assert_awaited_once() + violations = ( + mw.export_token().get("scope", {}).get("extensions", {}).get("scope_violations", []) + ) + assert violations == [] @pytest.mark.asyncio async def test_unauthorized_tool_recorded_as_violation(self): diff --git a/packages/hdp-agent-framework/tests/test_verify.py b/packages/hdp-agent-framework/tests/test_verify.py index 807b8d6..5dc8ad2 100644 --- a/packages/hdp-agent-framework/tests/test_verify.py +++ b/packages/hdp-agent-framework/tests/test_verify.py @@ -182,13 +182,6 @@ def test_wrong_key_with_hops_still_fails(self): # --------------------------------------------------------------------------- class TestEmptyChain: - def test_empty_chain_is_valid(self): - priv, pub = _generate_key() - token = _build_root_token(priv) - result = verify_chain(token, pub) - assert result.valid is True - assert result.hop_count == 0 - def test_empty_chain_depth_is_zero(self): priv, pub = _generate_key() token = _build_root_token(priv) From 23c3b1f9a79610f52aa0fcbdb07ed508906ad747 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:54:17 +1200 Subject: [PATCH 09/15] feat(hdp-agent-framework): implement HdpMiddleware for agent-framework --- .../src/hdp_agent_framework/__init__.py | 12 + .../src/hdp_agent_framework/middleware.py | 315 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py index 49d323e..4773fe2 100644 --- a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py @@ -1,3 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) 2026 Helixar Limited """hdp-agent-framework — HDP delegation provenance middleware for Microsoft agent-framework.""" + +from ._types import HdpPrincipal +from .middleware import HDPScopeViolationError, HdpMiddleware, ScopePolicy +from .verify import verify_chain + +__all__ = [ + "HdpMiddleware", + "HdpPrincipal", + "HDPScopeViolationError", + "ScopePolicy", + "verify_chain", +] diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py b/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py new file mode 100644 index 0000000..9dec550 --- /dev/null +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py @@ -0,0 +1,315 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2026 Helixar Limited +"""HdpMiddleware — non-blocking HDP audit trail for agent-framework agents. + +Design considerations implemented: + #1 Scope enforcement: _function_middleware() inspects tool calls against + authorized_tools. In strict mode raises HDPScopeViolationError; otherwise + logs and records violation. + #2 Delegation depth limits: max_hops is enforced in _extend_chain(). + #3 Token size / performance: non-blocking throughout; Ed25519 = 64 bytes/hop. + #4 Verification: see hdp_agent_framework.verify.verify_chain(). + +Usage: + from hdp_agent_framework import HdpMiddleware, ScopePolicy, HdpPrincipal + + middleware = HdpMiddleware( + signing_key=ed25519_private_key_bytes, + session_id="session-abc123", + principal=HdpPrincipal(id="user@example.com", id_type="email"), + scope=ScopePolicy( + intent="Coordinate research agents", + authorized_tools=["web_search", "file_reader"], + max_hops=10, + ), + ) + + # Attach to an agent + middleware.configure(agent) + + # After the run completes + print(middleware.export_token_json()) +""" + +from __future__ import annotations + +import json +import logging +import time +import uuid +from typing import Any, Optional + +from ._crypto import sign_hop, sign_root +from ._types import DataClassification, HdpPrincipal + +logger = logging.getLogger(__name__) + + +class HDPScopeViolationError(Exception): + """Raised when an agent attempts to use a tool outside the authorized scope.""" + + def __init__(self, tool: str, authorized_tools: list[str]) -> None: + self.tool = tool + self.authorized_tools = authorized_tools + super().__init__( + f"Tool '{tool}' is not in the authorized scope {authorized_tools}" + ) + + +class ScopePolicy: + """Human-readable policy that becomes the HDP scope field.""" + + def __init__( + self, + intent: str, + data_classification: DataClassification = "internal", + network_egress: bool = True, + persistence: bool = False, + authorized_tools: Optional[list[str]] = None, + authorized_resources: Optional[list[str]] = None, + max_hops: Optional[int] = None, + ) -> None: + self.intent = intent + self.data_classification = data_classification + self.network_egress = network_egress + self.persistence = persistence + self.authorized_tools = authorized_tools + self.authorized_resources = authorized_resources + self.max_hops = max_hops + + def to_dict(self) -> dict: + d: dict = { + "intent": self.intent, + "data_classification": self.data_classification, + "network_egress": self.network_egress, + "persistence": self.persistence, + } + if self.authorized_tools is not None: + d["authorized_tools"] = self.authorized_tools + if self.authorized_resources is not None: + d["authorized_resources"] = self.authorized_resources + if self.max_hops is not None: + d["max_hops"] = self.max_hops + return d + + +class HdpMiddleware: + """Non-blocking HDP middleware for agent-framework. + + Hooks into agent-framework's ChatMiddleware protocol to build a + tamper-evident delegation chain. + + All HDP operations are non-blocking by default: failures are logged as + warnings and agent execution continues unaffected. Set ``strict=True`` to + have scope violations raise HDPScopeViolationError and halt the agent. + """ + + def __init__( + self, + signing_key: bytes, + session_id: str, + principal: HdpPrincipal, + scope: ScopePolicy, + key_id: str = "default", + expires_in_ms: int = 86_400_000, + strict: bool = False, + ) -> None: + self._signing_key = signing_key + self._session_id = session_id + self._principal = principal + self._scope = scope + self._key_id = key_id + self._expires_in_ms = expires_in_ms + self._strict = strict + self._token: Optional[dict] = None + self._hop_seq = 0 + + # ------------------------------------------------------------------ + # ChatMiddleware protocol + # ------------------------------------------------------------------ + + async def process(self, context: Any, call_next: Any) -> None: + """Chat middleware entry point. + + 1. Lazily issue root token if not yet done. + 2. Extract agent_id from context.metadata.get("agent_name", "unknown"). + 3. Extend the chain with one hop. + 4. await call_next(). + """ + if self._token is None: + self._issue_root_token() + + try: + agent_id = context.metadata.get("agent_name", "unknown") + action_summary = context.metadata.get("action_summary", "") + self._extend_chain( + agent_id=agent_id, + action_summary=action_summary, + agent_type="sub-agent", + ) + except Exception as exc: + logger.warning("HDP process failed (non-blocking): %s", exc) + + await call_next() + + async def _function_middleware(self, context: Any, call_next: Any) -> None: + """Function/tool middleware entry point. + + 1. Get tool name from context.function.name. + 2. If authorized_tools is None: await call_next() and return. + 3. If tool NOT in authorized_tools: + If strict=True: raise HDPScopeViolationError. + Else: record violation in token, then await call_next(). + 4. Else (authorized): await call_next(). + """ + tool_name = context.function.name + authorized = self._scope.authorized_tools + + if authorized is None: + await call_next() + return + + if tool_name not in authorized: + if self._strict: + raise HDPScopeViolationError(tool_name, authorized) + logger.warning( + "HDP scope violation: tool '%s' not in authorized_tools %s", + tool_name, + authorized, + ) + self._record_scope_violation(tool_name) + + await call_next() + + # ------------------------------------------------------------------ + # Configuration + # ------------------------------------------------------------------ + + def configure(self, target: Any) -> None: + """Attach HDP middleware to an agent-framework agent. + + If the target has a .middleware attribute (a list), appends + self and self._function_middleware if not already present. + Otherwise logs a warning. + """ + if hasattr(target, "middleware") and isinstance(target.middleware, list): + if self not in target.middleware: + target.middleware.append(self) + if self._function_middleware not in target.middleware: + target.middleware.append(self._function_middleware) + else: + logger.warning( + "HDP configure: target %s has no .middleware list — no hooks attached", + type(target).__name__, + ) + + # ------------------------------------------------------------------ + # Inspection / export + # ------------------------------------------------------------------ + + def export_token(self) -> Optional[dict]: + """Return the current token dict, or None if no token has been issued.""" + return self._token + + def export_token_json(self, indent: int = 2) -> Optional[str]: + """Return the token as a JSON string, or None if no token has been issued.""" + if self._token is None: + return None + return json.dumps(self._token, indent=indent) + + # ------------------------------------------------------------------ + # Internal: root token issuance + # ------------------------------------------------------------------ + + def _issue_root_token(self) -> None: + """Issue the HDP root token. Called lazily on first process() call.""" + try: + now = int(time.time() * 1000) + unsigned: dict = { + "hdp": "0.1", + "header": { + "token_id": str(uuid.uuid4()), + "issued_at": now, + "expires_at": now + self._expires_in_ms, + "session_id": self._session_id, + "version": "0.1", + }, + "principal": self._build_principal_dict(), + "scope": self._scope.to_dict(), + "chain": [], + } + signature = sign_root(unsigned, self._signing_key, self._key_id) + self._token = {**unsigned, "signature": signature} + logger.debug("HDP root token issued: %s", self._token["header"]["token_id"]) + except Exception as exc: + logger.warning("HDP _issue_root_token failed (non-blocking): %s", exc) + # Leave self._token as None — non-blocking design + + # ------------------------------------------------------------------ + # Internal: chain extension + # ------------------------------------------------------------------ + + def _extend_chain(self, agent_id: str, action_summary: str = "", agent_type: str = "sub-agent") -> None: + """Append a signed hop to the delegation chain. + + Enforces max_hops — hops beyond the limit are skipped and logged. + """ + if self._token is None: + return + + max_hops = self._scope.max_hops + if max_hops is not None and self._hop_seq >= max_hops: + logger.warning( + "HDP max_hops (%d) reached — skipping hop for agent '%s'", + max_hops, + agent_id, + ) + return + + self._hop_seq += 1 + unsigned_hop: dict = { + "seq": self._hop_seq, + "agent_id": agent_id, + "agent_type": agent_type, + "timestamp": int(time.time() * 1000), + "action_summary": action_summary, + "parent_hop": self._hop_seq - 1, + } + + current_chain: list = self._token.get("chain", []) + cumulative = [*current_chain, unsigned_hop] + hop_sig = sign_hop(cumulative, self._token["signature"]["value"], self._signing_key) + + signed_hop = {**unsigned_hop, "hop_signature": hop_sig} + self._token = {**self._token, "chain": [*current_chain, signed_hop]} + logger.debug("HDP hop %d recorded for agent '%s'", self._hop_seq, agent_id) + + # ------------------------------------------------------------------ + # Internal: scope violation recording + # ------------------------------------------------------------------ + + def _record_scope_violation(self, tool: str) -> None: + """Record a scope violation in the token's scope extensions for audit visibility.""" + if self._token is None: + return + scope = self._token.get("scope", {}) + extensions = scope.get("extensions", {}) + violations: list = extensions.get("scope_violations", []) + violations.append({"tool": tool, "timestamp": int(time.time() * 1000)}) + updated_extensions = {**extensions, "scope_violations": violations} + self._token = { + **self._token, + "scope": {**scope, "extensions": updated_extensions}, + } + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _build_principal_dict(self) -> dict: + d: dict = {"id": self._principal.id, "id_type": self._principal.id_type} + if self._principal.display_name is not None: + d["display_name"] = self._principal.display_name + if self._principal.metadata is not None: + d["metadata"] = self._principal.metadata + return d From c7dcd9b1695d36ab7cfbebaccce4c0343e0e9853 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:55:55 +1200 Subject: [PATCH 10/15] fix(hdp-agent-framework): warn when scope violation cannot be recorded before token issued --- .../src/hdp_agent_framework/middleware.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py b/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py index 9dec550..7f469f2 100644 --- a/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/middleware.py @@ -177,6 +177,11 @@ async def _function_middleware(self, context: Any, call_next: Any) -> None: tool_name, authorized, ) + if self._token is None: + logger.warning( + "HDP scope violation for '%s' could not be recorded — no token issued yet", + tool_name, + ) self._record_scope_violation(tool_name) await call_next() From 5c98c83a46437f12ff1d888beec0b0ea389a6936 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:56:30 +1200 Subject: [PATCH 11/15] feat(hdp-agent-framework): finalize public API in __init__.py --- .../src/hdp_agent_framework/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py index 4773fe2..928a572 100644 --- a/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py +++ b/packages/hdp-agent-framework/src/hdp_agent_framework/__init__.py @@ -2,14 +2,20 @@ # Copyright (c) 2026 Helixar Limited """hdp-agent-framework — HDP delegation provenance middleware for Microsoft agent-framework.""" -from ._types import HdpPrincipal +from ._types import DataClassification, HdpPrincipal, HdpScope, HdpToken, HopRecord from .middleware import HDPScopeViolationError, HdpMiddleware, ScopePolicy -from .verify import verify_chain +from .verify import HopVerification, VerificationResult, verify_chain __all__ = [ "HdpMiddleware", - "HdpPrincipal", - "HDPScopeViolationError", "ScopePolicy", + "HDPScopeViolationError", + "HdpPrincipal", + "HdpScope", + "HdpToken", + "HopRecord", + "DataClassification", "verify_chain", + "VerificationResult", + "HopVerification", ] From e635832b54bcaff896227df24c131c9c2c3413e8 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:57:17 +1200 Subject: [PATCH 12/15] docs(hdp-agent-framework): write full README with arXiv citation and HDP links --- packages/hdp-agent-framework/README.md | 226 ++++++++++++++++++++++++- 1 file changed, 225 insertions(+), 1 deletion(-) diff --git a/packages/hdp-agent-framework/README.md b/packages/hdp-agent-framework/README.md index 5a74642..7eb6815 100644 --- a/packages/hdp-agent-framework/README.md +++ b/packages/hdp-agent-framework/README.md @@ -1,3 +1,227 @@ # hdp-agent-framework -HDP (Human Delegation Provenance) middleware for Microsoft agent-framework. +**HDP (Human Delegation Provenance) middleware for Microsoft agent-framework** — attach a +cryptographic audit trail to any agent or multi-agent workflow with zero changes to +your existing code. + +Every chat call and tool invocation is recorded in a tamper-evident chain of Ed25519 +signatures, verifiable fully **offline** with a single public key. + +``` +pip install hdp-agent-framework +``` + +--- + +## Quick start + +```python +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from agent_framework import Agent +from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import AzureCliCredential +from hdp_agent_framework import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain + +private_key = Ed25519PrivateKey.generate() + +middleware = HdpMiddleware( + signing_key=private_key.private_bytes_raw(), + session_id="analysis-2026", + principal=HdpPrincipal(id="analyst@corp.com", id_type="email"), + scope=ScopePolicy( + intent="Analyse Q1 sales data and generate a summary", + authorized_tools=["fetch_data", "write_report"], + max_hops=5, + ), +) + +agent = Agent( + client=FoundryChatClient(credential=AzureCliCredential()), + name="sales_analyst", + tools=[...], +) + +# Attach HDP — one line, zero agent changes +middleware.configure(agent) +await agent.run("Analyse Q1 EMEA sales and write a summary.") + +# Verify the delegation chain offline — no network call +result = verify_chain(middleware.export_token(), private_key.public_key()) +print(result.valid) # True +print(result.hop_count) # number of agent turns recorded +``` + +--- + +## Five design considerations + +| # | Consideration | How it's handled | +|---|---|---| +| **1** | **Scope enforcement** | Tool calls are inspected against `authorized_tools`. Default: logs + records violation in token. `strict=True`: raises `HDPScopeViolationError`. | +| **2** | **Delegation depth** | `ScopePolicy(max_hops=N)` is enforced; hops beyond the limit are skipped and logged. | +| **3** | **Token size / performance** | Ed25519 signatures are 64 bytes each. All HDP operations are non-blocking — failures log as warnings, the agent always continues. | +| **4** | **Verification** | `verify_chain(token, public_key)` validates root + every hop offline. Returns `VerificationResult` with `valid`, `hop_count`, `violations`, and per-hop outcomes. | +| **5** | **Agent integration** | `configure()` appends `HdpMiddleware` (chat middleware) and `_function_middleware` (tool middleware) to `agent.middleware`. Works with a single Agent or a list. | + +--- + +## API reference + +### `HdpMiddleware` + +```python +HdpMiddleware( + signing_key: bytes, # Ed25519 private key (raw 32 bytes) + session_id: str, # unique ID for this session + principal: HdpPrincipal, # the human delegating authority + scope: ScopePolicy, # what is authorised + key_id: str = "default", # label stored in the token header + expires_in_ms: int = 86400000, + strict: bool = False, # True → raise on scope violations +) +``` + +| Method | Description | +|---|---| +| `configure(target)` | Attach to an `Agent` or list of Agents | +| `export_token()` | Return the token dict (or `None` before first call) | +| `export_token_json()` | Return the token as a JSON string | + +### `verify_chain(token, public_key)` + +```python +result = verify_chain(token_dict, public_key) # Ed25519PublicKey or raw bytes +result.valid # bool +result.hop_count # int +result.violations # list[str] +result.hop_results # list[HopVerification] +``` + +### `ScopePolicy` + +```python +ScopePolicy( + intent: str, + data_classification: str = "internal", # "public" | "internal" | "confidential" | "restricted" + network_egress: bool = True, + persistence: bool = False, + authorized_tools: list[str] | None = None, + authorized_resources: list[str] | None = None, + max_hops: int | None = None, +) +``` + +--- + +## Error handling + +By default, HDP middleware is **non-blocking** — violations are logged as warnings and +recorded in the token for post-hoc audit. The agent always continues. + +```python +# Default (non-blocking): violations recorded, agent keeps running +middleware = HdpMiddleware( + signing_key=key, session_id="s1", + principal=HdpPrincipal(id="alice", id_type="handle"), + scope=ScopePolicy(intent="research", authorized_tools=["web_search"]), +) +middleware.configure(agent) + +# Strict mode: violations raise immediately +middleware_strict = HdpMiddleware( + signing_key=key, session_id="s1", + principal=HdpPrincipal(id="alice", id_type="handle"), + scope=ScopePolicy(intent="research", authorized_tools=["web_search"]), + strict=True, +) +``` + +After a session, inspect violations: + +```python +token = middleware.export_token() +for v in token["scope"].get("extensions", {}).get("scope_violations", []): + print(f"Violation: {v['tool']} at {v['timestamp']}") +``` + +--- + +## Cross-language compatibility + +HDP tokens use the same wire format across all language SDKs (RFC 8785 canonical JSON ++ Ed25519). A token issued by `hdp-agent-framework` (Python) can be verified by +`@helixar_ai/hdp` (TypeScript) and vice versa. + +```python +# Python: export token +token_json = middleware.export_token_json() +# → pass to TypeScript service via API, message queue, etc. +``` + +```typescript +// TypeScript: verify a token issued by Python +import { verifyChain } from "@helixar_ai/hdp"; +const result = verifyChain(JSON.parse(tokenJson), publicKey); +``` + +--- + +## Releasing + +Published to [PyPI](https://pypi.org/project/hdp-agent-framework/) via GitHub Actions: + +```bash +git tag python/hdp-agent-framework/v0.1.0 && git push origin python/hdp-agent-framework/v0.1.0 +``` + +Pipeline: `test-hdp-agent-framework` → `vet-hdp-agent-framework` ([ReleaseGuard](https://github.com/Helixar-AI/ReleaseGuard)) → `publish-hdp-agent-framework` + +| Detail | Value | +|---|---| +| **PyPI project** | [`hdp-agent-framework`](https://pypi.org/project/hdp-agent-framework/) | +| **Tag pattern** | `python/hdp-agent-framework/v*` | +| **Workflow** | `.github/workflows/release.yml` | +| **Auth** | OIDC trusted publisher (no token needed) | +| **Environment** | `pypi-hdp-agent-framework` | + +--- + +## Spec & citation + +HDP is an IETF draft standard: +[draft-helixar-hdp-agentic-delegation](https://datatracker.ietf.org/doc/draft-helixar-hdp-agentic-delegation/) + +Protocol specification and documentation: +[helixar.ai/about/labs/hdp/](https://helixar.ai/about/labs/hdp/) + +If you use HDP in research, please cite: + +```bibtex +@misc{dalugoda2026hdp, + title = {{HDP}: A Lightweight Cryptographic Protocol for Human Delegation + Provenance in Agentic {AI} Systems}, + author = {Dalugoda, Asiri}, + year = {2026}, + month = apr, + eprint = {2604.04522}, + archivePrefix = {arXiv}, + primaryClass = {cs.CR}, + url = {https://arxiv.org/abs/2604.04522}, +} +``` + +--- + +## References + +- [HDP protocol spec and docs](https://helixar.ai/about/labs/hdp/) +- [arXiv paper (2604.04522)](https://arxiv.org/abs/2604.04522) +- [HDP GitHub repository](https://github.com/Helixar-AI/HDP) +- [IETF draft: draft-helixar-hdp-agentic-delegation](https://datatracker.ietf.org/doc/draft-helixar-hdp-agentic-delegation/) +- [hdp-agent-framework on PyPI](https://pypi.org/project/hdp-agent-framework/) + +--- + +## License + +[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) — Helixar Limited From 4ed5fb87cca88d3330bb9ca2133b8e30580296b9 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:57:42 +1200 Subject: [PATCH 13/15] ci(hdp-agent-framework): add test, vet, and publish jobs to release workflow --- .github/workflows/release.yml | 84 +++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e800c77..4173f1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,7 @@ on: - 'python/hdp-grok/v*' - 'python/hdp-langchain/v*' - 'python/hdp-autogen/v*' + - 'python/hdp-agent-framework/v*' - 'python/hdp-physical/v*' - 'python/llama-index-callbacks-hdp/v*' - 'python/hdp-llamaindex/v*' @@ -872,6 +873,89 @@ jobs: with: packages-dir: dist/ + # ── hdp-agent-framework (Python / Microsoft agent-framework) ───────────── + + test-hdp-agent-framework: + name: Test hdp-agent-framework + if: startsWith(github.ref, 'refs/tags/python/hdp-agent-framework/v') + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + working-directory: packages/hdp-agent-framework + run: pip install -e ".[dev]" + + - name: Run tests + working-directory: packages/hdp-agent-framework + run: pytest tests/ -v + + vet-hdp-agent-framework: + name: Build & Vet hdp-agent-framework (ReleaseGuard) + needs: test-hdp-agent-framework + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: pip install build + + - name: Build distribution + working-directory: packages/hdp-agent-framework + run: python -m build + + - name: Vet artifacts with ReleaseGuard + uses: Helixar-AI/ReleaseGuard@94c067008f3ad516d4b61a6e7163d9d5518a4548 + with: + path: packages/hdp-agent-framework/dist + config: packages/hdp-agent-framework/.releaseguard.yml + sbom: 'true' + fix: 'true' + format: sarif + artifact-name: releaseguard-evidence-hdp-agent-framework + + - name: Upload vetted distribution + uses: actions/upload-artifact@v4 + with: + name: hdp-agent-framework-dist + path: packages/hdp-agent-framework/dist/ + retention-days: 1 + + publish-hdp-agent-framework: + name: Publish hdp-agent-framework to PyPI + needs: vet-hdp-agent-framework + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + environment: + name: pypi-hdp-agent-framework + url: https://pypi.org/project/hdp-agent-framework/ + steps: + - name: Download vetted distribution + uses: actions/download-artifact@v4 + with: + name: hdp-agent-framework-dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + # ── hdp-physical (TypeScript) ───────────────────────────────────────────── test-hdp-physical: From df69ee95c4d64aeeff85d433efa7e2eff56e42b5 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:58:37 +1200 Subject: [PATCH 14/15] docs: add hdp-agent-framework badge, package row, integration section, and release tag --- README.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/README.md b/README.md index 8a94481..fc4dab1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ _Every action an AI agent takes, traceable back to the human who authorized it._ [![CrewAI](https://img.shields.io/badge/CrewAI-integration-f43f5e?style=flat-square)](./packages/hdp-crewai) [![Grok / xAI](https://img.shields.io/badge/Grok%20%2F%20xAI-integration-000000?style=flat-square)](./packages/hdp-grok) [![AutoGen](https://img.shields.io/badge/AutoGen-integration-10b981?style=flat-square)](./packages/hdp-autogen) +[![agent-framework](https://img.shields.io/badge/agent--framework-integration-0078d4?style=flat-square)](./packages/hdp-agent-framework) [![LangChain](https://img.shields.io/badge/LangChain-integration-1c7c4c?style=flat-square)](./packages/hdp-langchain) [![LlamaIndex](https://img.shields.io/badge/LlamaIndex-integration-7c3aed?style=flat-square)](./packages/llama-index-callbacks-hdp) [![PyPI llama-index-callbacks-hdp](https://img.shields.io/pypi/v/llama-index-callbacks-hdp?style=flat-square&logo=pypi&logoColor=white&color=7c3aed&label=llama-index-callbacks-hdp)](https://pypi.org/project/llama-index-callbacks-hdp/) @@ -59,6 +60,7 @@ When a person authorizes an AI agent to act — and that agent delegates to anot | [`hdp-crewai`](./packages/hdp-crewai) | [PyPI](https://pypi.org/project/hdp-crewai/) | Python | CrewAI | CrewAI middleware — attaches HDP to any crew | | [`hdp-grok`](./packages/hdp-grok) | [PyPI](https://pypi.org/project/hdp-grok/) | Python | Grok / xAI | Grok middleware — attaches HDP to any xAI conversation | | [`hdp-autogen`](./packages/hdp-autogen) | [PyPI](https://pypi.org/project/hdp-autogen/) | Python | AutoGen | AutoGen middleware — attaches HDP to any AutoGen agent or GroupChat | +| [`hdp-agent-framework`](./packages/hdp-agent-framework) | [PyPI](https://pypi.org/project/hdp-agent-framework/) | Python | Microsoft agent-framework | agent-framework middleware — attaches HDP to any Agent or workflow | | [`@helixar_ai/hdp-autogen`](./packages/hdp-autogen-ts) | [npm](https://www.npmjs.com/package/@helixar_ai/hdp-autogen) | TypeScript | AutoGen | AutoGen middleware — HdpAgentWrapper + hdpMiddleware for AutoGen flows | | [`hdp-langchain`](./packages/hdp-langchain) | [PyPI](https://pypi.org/project/hdp-langchain/) | Python | LangChain / LangGraph | LangChain middleware — attaches HDP to any chain, agent, or LangGraph node | | [`llama-index-callbacks-hdp`](./packages/llama-index-callbacks-hdp) | [PyPI](https://pypi.org/project/llama-index-callbacks-hdp/) | Python | LlamaIndex | LlamaIndex integration — callback handler, instrumentation dispatcher, node postprocessor | @@ -102,6 +104,12 @@ pip install hdp-grok pip install hdp-autogen ``` +**Python / Microsoft agent-framework** + +```bash +pip install hdp-agent-framework +``` + **Python / LangChain** ```bash @@ -418,6 +426,50 @@ print(result.valid, result.hop_count, result.violations) --- +## Microsoft agent-framework Integration + +`hdp-agent-framework` attaches HDP to any Microsoft agent-framework `Agent` via the native `ChatMiddleware` and function middleware protocols. A single `middleware.configure(agent)` call appends both middlewares to `agent.middleware` — no other changes required. + +```python +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from agent_framework import Agent +from agent_framework.foundry import FoundryChatClient +from azure.identity.aio import AzureCliCredential +from hdp_agent_framework import HdpMiddleware, HdpPrincipal, ScopePolicy, verify_chain + +private_key = Ed25519PrivateKey.generate() + +middleware = HdpMiddleware( + signing_key=private_key.private_bytes_raw(), + session_id="analysis-2026", + principal=HdpPrincipal(id="analyst@corp.com", id_type="email"), + scope=ScopePolicy( + intent="Analyse Q1 sales data and generate a summary", + authorized_tools=["fetch_data", "write_report"], + max_hops=5, + ), +) + +agent = Agent(client=FoundryChatClient(credential=AzureCliCredential()), name="sales_analyst", tools=[...]) +middleware.configure(agent) # attaches chat + function middleware — one line +await agent.run("Analyse Q1 EMEA sales and write a summary.") + +result = verify_chain(middleware.export_token(), private_key.public_key()) +print(result.valid, result.hop_count) +``` + +| # | Consideration | Behaviour | +|---|---|---| +| 1 | **Scope enforcement** | Tool calls are inspected against `authorized_tools`. `strict=True` raises `HDPScopeViolationError`; default logs and records in the audit trail. | +| 2 | **Delegation depth** | `max_hops` is enforced; hops beyond the limit are skipped and logged. | +| 3 | **Token size / perf** | Ed25519 = 64 bytes/hop. All operations are non-blocking — failures log, never halt agents. | +| 4 | **Verification** | `verify_chain(token, public_key)` validates root + every hop offline. | +| 5 | **Agent integration** | `configure()` appends `HdpMiddleware` and `_function_middleware` to `agent.middleware` — idempotent, duck-typed, no hard dependency on agent-framework internals. | + +→ [Full agent-framework integration docs](./packages/hdp-agent-framework/README.md) + +--- + ## LlamaIndex Integration `llama-index-callbacks-hdp` covers all three LlamaIndex hook points. Use whichever layer fits your pipeline — they share the same ContextVar-backed session so all three can be active simultaneously. @@ -707,6 +759,14 @@ git tag python/hdp-autogen/v0.1.2 && git push origin python/hdp-autogen/v0.1.2 Pipeline: `test-hdp-autogen` → `vet-hdp-autogen` (ReleaseGuard) → `publish-hdp-autogen` +### hdp-agent-framework → PyPI + +```bash +git tag python/hdp-agent-framework/v0.1.0 && git push origin python/hdp-agent-framework/v0.1.0 +``` + +Pipeline: `test-hdp-agent-framework` → `vet-hdp-agent-framework` (ReleaseGuard) → `publish-hdp-agent-framework` + ### hdp-langchain → PyPI ```bash @@ -740,6 +800,7 @@ Every artifact is scanned by [ReleaseGuard](https://github.com/Helixar-AI/Releas cd packages/hdp-grok && python -m build && releaseguard check ./dist cd packages/hdp-crewai && python -m build && releaseguard check ./dist cd packages/hdp-autogen && python -m build && releaseguard check ./dist +cd packages/hdp-agent-framework && python -m build && releaseguard check ./dist cd packages/hdp-langchain && python -m build && releaseguard check ./dist cd packages/llama-index-callbacks-hdp && python -m build && releaseguard check ./dist cd packages/hdp-autogen-ts && npm run build && releaseguard check ./dist From 990e183a4ced07263afca0032c640c7f3801aa54 Mon Sep 17 00:00:00 2001 From: Siri Dalugoda Date: Sat, 9 May 2026 09:59:35 +1200 Subject: [PATCH 15/15] chore: gitignore agent-framework draft files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8e3aff8..932f6d5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ # Claude / AI planning artifacts — excluded from repo docs/superpowers/ docs/*-design.md +docs/*-draft.md .superpowers/ .DS_Store .worktrees/