Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5cc2450
feat(types): add labels to capabilities, functions, and events
soupat May 10, 2026
aedf561
feat(selector): add selector DSL parser and matcher
soupat May 10, 2026
48b4386
feat(discovery): selector-driven discover and discover_labels
soupat May 10, 2026
8cbf77c
feat(discovery): structure discover/discover_labels error envelope
soupat May 10, 2026
85369ee
docs: move discovery guide to docs/discovery.md and trim to shipped t…
soupat May 10, 2026
a046f46
docs: trim discovery guide to shipped tools
soupat May 10, 2026
bf137d8
feat(invoke): selector-based invoke and invoke_many with sync fan-out
soupat May 10, 2026
b64edac
feat(predicate): add CEL where evaluator with optional [predicate] extra
soupat May 10, 2026
4e2208a
feat(broadcast): async fan-out with correlation, fire_at, and subscribe
soupat May 10, 2026
072ef01
feat(cli): selector-driven verbs in devctl and statectl
soupat May 10, 2026
02a9420
docs: extend discovery guide for operations, where, and CLI
soupat May 10, 2026
7c760ab
fix(broadcast): robustness pass on edge handler, subscribe, and CLI
soupat May 10, 2026
8660801
feat(adapters): expose broadcast and await_replies via all three adap…
soupat May 10, 2026
08c9aa7
fix(broadcast): read identity from driver, not from DeviceCapabilities
soupat May 11, 2026
641b7bd
feat: add device mandate enforcement
soupat May 11, 2026
1d0bf51
feat: expose mandates in agent adapters
soupat May 11, 2026
fc46357
docs: add device mandate examples
soupat May 11, 2026
9c40bd9
feat: add server mandate receipts
soupat May 11, 2026
7b0e749
feat: add mandate receipt query log
soupat May 11, 2026
93e5591
fix: use tenant scoped registry lookup for mandates
soupat May 11, 2026
38a08f8
fix: expose mandate metadata from loaded capabilities
soupat May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
Developer reference material for Device Connect.

- **class-map.html** — Interactive class/module relationship diagram. Open in a browser to explore the architecture.
- **device-mandates.md** — Concise guide and runnable examples for mandate-protected device functions.
- **device-mandates-spec.md** — Implementation notes and acceptance criteria for the Device Mandates feature.
51 changes: 51 additions & 0 deletions docs/device-mandates-spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Spec: Device Mandates

## Objective

Add an optional verifiable authorization layer for Device Connect RPC execution. A device function can declare that it requires a Device Mandate, and the runtime refuses to execute protected RPCs unless the caller presents a signed mandate that authorizes the target device, method, parameters, and validity window.

The first implementation slice proves the contract end to end with a lightweight HMAC-backed mandate format suitable for local tests and demos. The verifier is intentionally small and pluggable so a later slice can replace or augment the credential format with UCAN, Biscuit, or a standards-track profile without changing the decorator or RPC metadata contract.

## Commands

- Edge tests: `pytest packages/device-connect-edge/tests -q`
- Agent tools tests: `pytest packages/device-connect-agent-tools/tests -q`
- Focused mandate tests: `pytest packages/device-connect-edge/tests/test_mandate_verifier.py packages/device-connect-edge/tests/test_device_mandates.py packages/device-connect-agent-tools/tests/test_agent_mandates.py -q`

## Project Structure

- `packages/device-connect-edge/device_connect_edge/mandates.py`: mandate data helpers, signing, and verification.
- `packages/device-connect-edge/device_connect_edge/drivers/decorators.py`: `@requires_mandate` decorator metadata.
- `packages/device-connect-edge/device_connect_edge/device.py`: runtime enforcement before driver invocation.
- `packages/device-connect-edge/device_connect_edge/types.py`: function capability metadata for mandate requirements.
- `packages/device-connect-agent-tools/device_connect_agent_tools/tools.py`: pass mandate metadata through `_dc_meta`.

## Testing Strategy

Use test-driven slices:

- Pure unit tests for signing, verification, time windows, device/method binding, numeric constraints, tamper detection, and replay denial.
- Runtime tests for protected RPC denial before driver execution and successful execution with a valid mandate.
- Agent-tools tests that verify `invoke`, `invoke_many`, `broadcast`, and legacy `invoke_device` attach mandate data inside `_dc_meta`.

## Boundaries

- Always: fail closed for protected methods; keep mandate support optional for unprotected methods; preserve existing unprotected RPC behavior.
- Ask first: adding non-stdlib crypto/credential dependencies; changing transport protocols; adding persistent receipt storage; modifying CI.
- Never: treat unsigned client-provided mandate dictionaries as valid; pass `_dc_meta` into user driver methods; weaken existing ACL/TLS/JWT checks.

## Success Criteria

- A driver can mark an RPC with `@requires_mandate(scope="actuation")`.
- Discovery/capability metadata shows mandate requirements for protected functions.
- Direct JSON-RPC and broadcast execution reject protected functions with no mandate, invalid signature, wrong device, wrong method, expired mandate, or out-of-range parameters.
- Direct JSON-RPC and broadcast execution allow a protected function with a valid closed mandate.
- Agent tools can attach mandate data to invoke paths through `_dc_meta`.
- Existing unprotected RPC tests continue to pass.

## Open Questions

- Which production credential format should be the default: UCAN, Biscuit, or a future AP2-compatible non-payment profile?
- Where should production principal keys live: OS keystore, HSM/KMS, commissioning bundle, or registry-backed trust store?
- Should execution receipts be persisted first in the server state store or emitted as signed events before storage is added?
- Should replay protection be in-memory per device for v0, or backed by the server state layer for distributed deployments?
108 changes: 108 additions & 0 deletions docs/device-mandates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Device Mandates

Device Mandates add a signed authorization envelope to sensitive RPCs. A driver marks a function with `@requires_mandate`, and the runtime denies that function unless the call includes a valid closed mandate in `_dc_meta.mandate`.

Use mandates for actuation that can affect safety, access, cost, or physical state. Read-only functions usually should not require mandates.

## Protect an RPC

Decorate the RPC with `@requires_mandate`. The decorator may be placed above or below `@rpc()`.

```python
from device_connect_edge.drivers import DeviceDriver, requires_mandate, rpc


class SmartLockDriver(DeviceDriver):
device_type = "smart_lock"

@requires_mandate(scope="actuation")
@rpc()
async def unlock(self, duration_s: int = 10) -> dict:
return {"state": "unlocked", "duration_s": duration_s}
```

Discovery metadata for `unlock` includes:

```json
{"mandate": {"required": true, "scope": "actuation"}}
```

## Create Mandates

An open mandate is signed by the principal and delegates bounded authority to an agent. A closed mandate is signed by the agent for one concrete invocation.

```python
from datetime import datetime, timedelta, timezone

from device_connect_edge import create_closed_mandate, create_open_mandate

now = datetime.now(timezone.utc)
principal_key = b"principal-demo-key"
agent_key = b"agent-demo-key"

open_mandate = create_open_mandate(
principal="operator",
agent="agent-1",
device_id="lock-front-door",
methods=["unlock"],
constraints={"duration_s": {"lte": 30}},
not_before=now - timedelta(seconds=5),
not_after=now + timedelta(minutes=5),
key=principal_key,
)

closed_mandate = create_closed_mandate(
open_mandate=open_mandate,
agent="agent-1",
device_id="lock-front-door",
method="unlock",
params={"duration_s": 20},
key=agent_key,
issued_at=now,
)
```

Pass the closed mandate through agent tools with the `mandate` argument:

```python
from device_connect_agent_tools import invoke

result = invoke(
"device(lock-front-door).function(unlock)",
params={"duration_s": 20},
mandate=closed_mandate,
)
```

## Valid and Invalid Use Cases

Valid smart-lock use: unlock the front door for 20 seconds when the open mandate allows `unlock` on `lock-front-door` and constrains `duration_s <= 30`.

Invalid smart-lock use: reuse that same mandate for `duration_s=60`, another device, another method, or changed parameters. The signature and constraint checks fail closed before the driver method runs.

Valid heater use: set a room heater to 21.5 C when the open mandate allows `set_temperature` on `heater-living-room` and constrains `target_c` between 18 and 23.

Invalid heater use: request `target_c=28` or replay a previously used closed mandate nonce. The verifier denies the call.

See `packages/device-connect-edge/examples/device_mandates/mandate_examples.py` for runnable local examples of these cases.

## Testing Commands

Run the focused mandate tests:

```bash
pytest packages/device-connect-edge/tests/test_mandate_verifier.py packages/device-connect-edge/tests/test_device_mandates.py packages/device-connect-agent-tools/tests/test_agent_mandates.py -q
```

Run package test suites:

```bash
pytest packages/device-connect-edge/tests -q
pytest packages/device-connect-agent-tools/tests -q
```

Run the examples:

```bash
PYTHONPATH=packages/device-connect-edge python packages/device-connect-edge/examples/device_mandates/mandate_examples.py
```
Loading