spendguard-sdk 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spendguard_sdk-0.1.0a1/.gitignore +25 -0
- spendguard_sdk-0.1.0a1/CHANGELOG.md +44 -0
- spendguard_sdk-0.1.0a1/Makefile +61 -0
- spendguard_sdk-0.1.0a1/PKG-INFO +152 -0
- spendguard_sdk-0.1.0a1/README.md +107 -0
- spendguard_sdk-0.1.0a1/examples_pydantic_ai/basic_agent.py +109 -0
- spendguard_sdk-0.1.0a1/pyproject.toml +87 -0
- spendguard_sdk-0.1.0a1/src/spendguard/__init__.py +70 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/__init__.py +8 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/common/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/common/v1/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/common/v1/common_pb2.py +77 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/common/v1/common_pb2.pyi +286 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/common/v1/common_pb2_grpc.py +24 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/ledger/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/ledger/v1/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/ledger/v1/ledger_pb2.py +120 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/ledger/v1/ledger_pb2.pyi +645 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/ledger/v1/ledger_pb2_grpc.py +656 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/sidecar_adapter/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/sidecar_adapter/v1/__init__.py +0 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/sidecar_adapter/v1/adapter_pb2.py +101 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/sidecar_adapter/v1/adapter_pb2.pyi +446 -0
- spendguard_sdk-0.1.0a1/src/spendguard/_proto/spendguard/sidecar_adapter/v1/adapter_pb2_grpc.py +436 -0
- spendguard_sdk-0.1.0a1/src/spendguard/client.py +721 -0
- spendguard_sdk-0.1.0a1/src/spendguard/errors.py +95 -0
- spendguard_sdk-0.1.0a1/src/spendguard/ids.py +184 -0
- spendguard_sdk-0.1.0a1/src/spendguard/integrations/__init__.py +12 -0
- spendguard_sdk-0.1.0a1/src/spendguard/integrations/agt.py +278 -0
- spendguard_sdk-0.1.0a1/src/spendguard/integrations/langchain.py +342 -0
- spendguard_sdk-0.1.0a1/src/spendguard/integrations/openai_agents.py +302 -0
- spendguard_sdk-0.1.0a1/src/spendguard/integrations/pydantic_ai.py +657 -0
- spendguard_sdk-0.1.0a1/src/spendguard/pricing.py +97 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.ait/
|
|
2
|
+
|
|
3
|
+
# AIT framework local hook config (absolute paths to user pipx venv).
|
|
4
|
+
.claude/
|
|
5
|
+
.codex/
|
|
6
|
+
|
|
7
|
+
# direnv local PATH setup (regenerated per checkout).
|
|
8
|
+
.envrc
|
|
9
|
+
|
|
10
|
+
# AIT per-session context file (regenerated each session).
|
|
11
|
+
.ait-context.md
|
|
12
|
+
|
|
13
|
+
# Python build / cache artifacts (Phase 4 O1 SDK + generated proto stubs).
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.pyc
|
|
16
|
+
sdk/python/src/spendguard/_proto/spendguard/
|
|
17
|
+
|
|
18
|
+
# Terraform local state (Phase 4 O9).
|
|
19
|
+
terraform/**/.terraform/
|
|
20
|
+
terraform/**/.terraform.lock.hcl
|
|
21
|
+
terraform/**/terraform.tfstate
|
|
22
|
+
terraform/**/terraform.tfstate.backup
|
|
23
|
+
|
|
24
|
+
# mkdocs build output (Phase 4 O10).
|
|
25
|
+
docs/site/site/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0a1 (Phase 4 O1) — 2026-05-09
|
|
4
|
+
|
|
5
|
+
Initial SDK release. Restructured from `spendguard-pydantic-ai` to the
|
|
6
|
+
multi-framework `spendguard-sdk` with optional extras.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Top-level package `spendguard` with framework-agnostic core
|
|
11
|
+
(`SpendGuardClient`, `DecisionStopped`, etc.)
|
|
12
|
+
- `spendguard.integrations.pydantic_ai` (was `spendguard_pydantic_ai`
|
|
13
|
+
top-level) — gated behind `pip install 'spendguard-sdk[pydantic-ai]'`
|
|
14
|
+
- Slots reserved for `spendguard.integrations.langchain`,
|
|
15
|
+
`spendguard.integrations.langgraph`,
|
|
16
|
+
`spendguard.integrations.openai_agents` (Phase 4 O5).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Package name: `spendguard-pydantic-ai` → `spendguard-sdk`
|
|
21
|
+
- Python module: `spendguard_pydantic_ai` → `spendguard`
|
|
22
|
+
- Pydantic-AI wrapper moved from top-level to
|
|
23
|
+
`spendguard.integrations.pydantic_ai`
|
|
24
|
+
- Internal contextvar renamed `spendguard_pydantic_ai_run_context` →
|
|
25
|
+
`spendguard_run_context`
|
|
26
|
+
|
|
27
|
+
### Migration from `spendguard-pydantic-ai`
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# Before
|
|
31
|
+
from spendguard_pydantic_ai import SpendGuardClient, SpendGuardModel, RunContext
|
|
32
|
+
|
|
33
|
+
# After
|
|
34
|
+
from spendguard import SpendGuardClient
|
|
35
|
+
from spendguard.integrations.pydantic_ai import SpendGuardModel, RunContext
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Before
|
|
40
|
+
pip install spendguard-pydantic-ai
|
|
41
|
+
|
|
42
|
+
# After
|
|
43
|
+
pip install 'spendguard-sdk[pydantic-ai]'
|
|
44
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
PROTO_ROOT := $(realpath $(CURDIR)/../../proto)
|
|
2
|
+
OUT_DIR := $(CURDIR)/src/spendguard/_proto
|
|
3
|
+
# ledger.proto is included for the demo's webhook simulator path
|
|
4
|
+
# (talks directly to ledger gRPC for Step 8 dev convenience).
|
|
5
|
+
# Production adapters do NOT use ledger.proto; sidecar is the only
|
|
6
|
+
# ledger client per Stage 2 §3.1.
|
|
7
|
+
PROTOS := \
|
|
8
|
+
$(PROTO_ROOT)/spendguard/common/v1/common.proto \
|
|
9
|
+
$(PROTO_ROOT)/spendguard/sidecar_adapter/v1/adapter.proto \
|
|
10
|
+
$(PROTO_ROOT)/spendguard/ledger/v1/ledger.proto
|
|
11
|
+
|
|
12
|
+
.PHONY: proto clean lint typecheck test build publish-test publish
|
|
13
|
+
|
|
14
|
+
# Regenerate Python protobuf + gRPC stubs from the wire spec.
|
|
15
|
+
# Output lands under src/spendguard/_proto/spendguard/...
|
|
16
|
+
# Generated code uses absolute imports rooted at `spendguard.*`; we
|
|
17
|
+
# rewrite them to `spendguard._proto.spendguard.*` so the wheel ships
|
|
18
|
+
# a self-contained import tree.
|
|
19
|
+
proto:
|
|
20
|
+
@mkdir -p $(OUT_DIR)
|
|
21
|
+
python -m grpc_tools.protoc \
|
|
22
|
+
--proto_path=$(PROTO_ROOT) \
|
|
23
|
+
--python_out=$(OUT_DIR) \
|
|
24
|
+
--grpc_python_out=$(OUT_DIR) \
|
|
25
|
+
--pyi_out=$(OUT_DIR) \
|
|
26
|
+
$(PROTOS)
|
|
27
|
+
@find $(OUT_DIR) -type d -exec sh -c 'touch "$$1/__init__.py"' _ {} \;
|
|
28
|
+
@# Rewrite both `from spendguard.X import` and `import spendguard.X`
|
|
29
|
+
@# patterns to the namespaced location.
|
|
30
|
+
@find $(OUT_DIR) \( -name '*.py' -o -name '*.pyi' \) \
|
|
31
|
+
-exec sed -i.bak \
|
|
32
|
+
-e 's|^from spendguard\.|from spendguard._proto.spendguard.|g' \
|
|
33
|
+
-e 's|^import spendguard\.|import spendguard._proto.spendguard.|g' \
|
|
34
|
+
{} \;
|
|
35
|
+
@find $(OUT_DIR) -name '*.bak' -delete
|
|
36
|
+
|
|
37
|
+
clean:
|
|
38
|
+
rm -rf $(OUT_DIR) dist build *.egg-info
|
|
39
|
+
|
|
40
|
+
lint:
|
|
41
|
+
ruff check src
|
|
42
|
+
|
|
43
|
+
typecheck:
|
|
44
|
+
mypy src
|
|
45
|
+
|
|
46
|
+
test:
|
|
47
|
+
pytest -q
|
|
48
|
+
|
|
49
|
+
# Build sdist + wheel for publishing.
|
|
50
|
+
build: clean proto
|
|
51
|
+
python -m build
|
|
52
|
+
|
|
53
|
+
# Publish to test.pypi.org for verification.
|
|
54
|
+
publish-test: build
|
|
55
|
+
python -m twine check dist/*
|
|
56
|
+
python -m twine upload --repository testpypi dist/*
|
|
57
|
+
|
|
58
|
+
# Publish to PyPI (real).
|
|
59
|
+
publish: build
|
|
60
|
+
python -m twine check dist/*
|
|
61
|
+
python -m twine upload dist/*
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spendguard-sdk
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: SpendGuard SDK — runtime safety layer client for AI agent frameworks (Pydantic-AI, LangChain, LangGraph, OpenAI Agents SDK).
|
|
5
|
+
Project-URL: Homepage, https://github.com/m24927605/agentic-spendguard
|
|
6
|
+
Project-URL: Repository, https://github.com/m24927605/agentic-spendguard
|
|
7
|
+
Project-URL: Issues, https://github.com/m24927605/agentic-spendguard/issues
|
|
8
|
+
Author-email: Michael Chen <m24927605@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: grpcio>=1.62
|
|
21
|
+
Requires-Dist: protobuf<6,>=4.25
|
|
22
|
+
Requires-Dist: pydantic>=2.6
|
|
23
|
+
Provides-Extra: agt
|
|
24
|
+
Requires-Dist: agent-governance-toolkit>=3.4; extra == 'agt'
|
|
25
|
+
Requires-Dist: agent-os-kernel>=3.0; extra == 'agt'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: grpcio-tools>=1.62; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
34
|
+
Provides-Extra: langchain
|
|
35
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
36
|
+
Requires-Dist: langchain>=0.3; extra == 'langchain'
|
|
37
|
+
Provides-Extra: langgraph
|
|
38
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langgraph'
|
|
39
|
+
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
40
|
+
Provides-Extra: openai-agents
|
|
41
|
+
Requires-Dist: openai-agents>=0.17; extra == 'openai-agents'
|
|
42
|
+
Provides-Extra: pydantic-ai
|
|
43
|
+
Requires-Dist: pydantic-ai<0.1.0,>=0.0.20; extra == 'pydantic-ai'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# spendguard-sdk
|
|
47
|
+
|
|
48
|
+
Runtime safety layer client for AI agent frameworks. Talks to the
|
|
49
|
+
SpendGuard sidecar over Unix-domain-socket gRPC; gates each LLM /
|
|
50
|
+
tool-call boundary through a Contract DSL evaluator and an atomic
|
|
51
|
+
budget ledger with immutable audit chain.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Core only (raw client, no framework integration)
|
|
57
|
+
pip install spendguard-sdk
|
|
58
|
+
|
|
59
|
+
# With the integration you need
|
|
60
|
+
pip install 'spendguard-sdk[pydantic-ai]'
|
|
61
|
+
pip install 'spendguard-sdk[langchain]'
|
|
62
|
+
pip install 'spendguard-sdk[langgraph]'
|
|
63
|
+
pip install 'spendguard-sdk[openai-agents]'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quickstart (Pydantic-AI)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from pydantic_ai import Agent
|
|
71
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
72
|
+
|
|
73
|
+
from spendguard import SpendGuardClient, new_uuid7
|
|
74
|
+
from spendguard.integrations.pydantic_ai import (
|
|
75
|
+
RunContext,
|
|
76
|
+
SpendGuardModel,
|
|
77
|
+
run_context,
|
|
78
|
+
)
|
|
79
|
+
from spendguard._proto.spendguard.common.v1 import common_pb2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
async def main():
|
|
83
|
+
client = SpendGuardClient(
|
|
84
|
+
socket_path="/var/run/spendguard/adapter.sock",
|
|
85
|
+
tenant_id="00000000-0000-4000-8000-000000000001",
|
|
86
|
+
)
|
|
87
|
+
await client.connect()
|
|
88
|
+
await client.handshake()
|
|
89
|
+
|
|
90
|
+
guarded = SpendGuardModel(
|
|
91
|
+
inner=OpenAIModel("gpt-4o-mini"),
|
|
92
|
+
client=client,
|
|
93
|
+
budget_id="44444444-4444-4444-8444-444444444444",
|
|
94
|
+
window_instance_id="55555555-5555-4555-8555-555555555555",
|
|
95
|
+
unit=common_pb2.UnitRef(
|
|
96
|
+
unit_id="66666666-6666-4666-8666-666666666666",
|
|
97
|
+
token_kind="output_token",
|
|
98
|
+
model_family="gpt-4",
|
|
99
|
+
),
|
|
100
|
+
pricing=common_pb2.PricingFreeze(
|
|
101
|
+
pricing_version="demo-pricing-v1",
|
|
102
|
+
price_snapshot_hash=b"<32 bytes>",
|
|
103
|
+
fx_rate_version="demo-fx-v1",
|
|
104
|
+
unit_conversion_version="demo-units-v1",
|
|
105
|
+
),
|
|
106
|
+
claim_estimator=lambda messages, settings: [
|
|
107
|
+
common_pb2.BudgetClaim(
|
|
108
|
+
budget_id="44444444-4444-4444-8444-444444444444",
|
|
109
|
+
unit=common_pb2.UnitRef(
|
|
110
|
+
unit_id="66666666-6666-4666-8666-666666666666",
|
|
111
|
+
token_kind="output_token",
|
|
112
|
+
model_family="gpt-4",
|
|
113
|
+
),
|
|
114
|
+
amount_atomic="500",
|
|
115
|
+
direction=common_pb2.BudgetClaim.DEBIT,
|
|
116
|
+
window_instance_id="55555555-5555-4555-8555-555555555555",
|
|
117
|
+
)
|
|
118
|
+
],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
agent = Agent(model=guarded)
|
|
122
|
+
async with run_context(RunContext(run_id=str(new_uuid7()))):
|
|
123
|
+
result = await agent.run("Say hello in three words.")
|
|
124
|
+
print(result.output)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
asyncio.run(main())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If a contract rule denies the call, `agent.run(...)` raises
|
|
131
|
+
`spendguard.DecisionStopped` carrying `reason_codes` and
|
|
132
|
+
`matched_rule_ids`.
|
|
133
|
+
|
|
134
|
+
## API surface (core)
|
|
135
|
+
|
|
136
|
+
| Symbol | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `SpendGuardClient` | UDS gRPC client to the sidecar |
|
|
139
|
+
| `DecisionStopped`, `DecisionSkipped`, `ApprovalRequired` | per-decision exceptions |
|
|
140
|
+
| `derive_idempotency_key(...)` | deterministic key from (tenant, run, step, llm_call, trigger) |
|
|
141
|
+
| `new_uuid7()` | UUID v7 helper |
|
|
142
|
+
|
|
143
|
+
## Wire-protocol compatibility
|
|
144
|
+
|
|
145
|
+
This SDK pins to a specific protobuf wire version. Check the
|
|
146
|
+
sidecar's published version against `spendguard.__version__`; minor
|
|
147
|
+
versions are wire-compatible, major bumps are breaking. (Not yet
|
|
148
|
+
enforced at handshake; planned for v0.2.)
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
Apache-2.0.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# spendguard-sdk
|
|
2
|
+
|
|
3
|
+
Runtime safety layer client for AI agent frameworks. Talks to the
|
|
4
|
+
SpendGuard sidecar over Unix-domain-socket gRPC; gates each LLM /
|
|
5
|
+
tool-call boundary through a Contract DSL evaluator and an atomic
|
|
6
|
+
budget ledger with immutable audit chain.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Core only (raw client, no framework integration)
|
|
12
|
+
pip install spendguard-sdk
|
|
13
|
+
|
|
14
|
+
# With the integration you need
|
|
15
|
+
pip install 'spendguard-sdk[pydantic-ai]'
|
|
16
|
+
pip install 'spendguard-sdk[langchain]'
|
|
17
|
+
pip install 'spendguard-sdk[langgraph]'
|
|
18
|
+
pip install 'spendguard-sdk[openai-agents]'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quickstart (Pydantic-AI)
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from pydantic_ai import Agent
|
|
26
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
27
|
+
|
|
28
|
+
from spendguard import SpendGuardClient, new_uuid7
|
|
29
|
+
from spendguard.integrations.pydantic_ai import (
|
|
30
|
+
RunContext,
|
|
31
|
+
SpendGuardModel,
|
|
32
|
+
run_context,
|
|
33
|
+
)
|
|
34
|
+
from spendguard._proto.spendguard.common.v1 import common_pb2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def main():
|
|
38
|
+
client = SpendGuardClient(
|
|
39
|
+
socket_path="/var/run/spendguard/adapter.sock",
|
|
40
|
+
tenant_id="00000000-0000-4000-8000-000000000001",
|
|
41
|
+
)
|
|
42
|
+
await client.connect()
|
|
43
|
+
await client.handshake()
|
|
44
|
+
|
|
45
|
+
guarded = SpendGuardModel(
|
|
46
|
+
inner=OpenAIModel("gpt-4o-mini"),
|
|
47
|
+
client=client,
|
|
48
|
+
budget_id="44444444-4444-4444-8444-444444444444",
|
|
49
|
+
window_instance_id="55555555-5555-4555-8555-555555555555",
|
|
50
|
+
unit=common_pb2.UnitRef(
|
|
51
|
+
unit_id="66666666-6666-4666-8666-666666666666",
|
|
52
|
+
token_kind="output_token",
|
|
53
|
+
model_family="gpt-4",
|
|
54
|
+
),
|
|
55
|
+
pricing=common_pb2.PricingFreeze(
|
|
56
|
+
pricing_version="demo-pricing-v1",
|
|
57
|
+
price_snapshot_hash=b"<32 bytes>",
|
|
58
|
+
fx_rate_version="demo-fx-v1",
|
|
59
|
+
unit_conversion_version="demo-units-v1",
|
|
60
|
+
),
|
|
61
|
+
claim_estimator=lambda messages, settings: [
|
|
62
|
+
common_pb2.BudgetClaim(
|
|
63
|
+
budget_id="44444444-4444-4444-8444-444444444444",
|
|
64
|
+
unit=common_pb2.UnitRef(
|
|
65
|
+
unit_id="66666666-6666-4666-8666-666666666666",
|
|
66
|
+
token_kind="output_token",
|
|
67
|
+
model_family="gpt-4",
|
|
68
|
+
),
|
|
69
|
+
amount_atomic="500",
|
|
70
|
+
direction=common_pb2.BudgetClaim.DEBIT,
|
|
71
|
+
window_instance_id="55555555-5555-4555-8555-555555555555",
|
|
72
|
+
)
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
agent = Agent(model=guarded)
|
|
77
|
+
async with run_context(RunContext(run_id=str(new_uuid7()))):
|
|
78
|
+
result = await agent.run("Say hello in three words.")
|
|
79
|
+
print(result.output)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If a contract rule denies the call, `agent.run(...)` raises
|
|
86
|
+
`spendguard.DecisionStopped` carrying `reason_codes` and
|
|
87
|
+
`matched_rule_ids`.
|
|
88
|
+
|
|
89
|
+
## API surface (core)
|
|
90
|
+
|
|
91
|
+
| Symbol | Purpose |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `SpendGuardClient` | UDS gRPC client to the sidecar |
|
|
94
|
+
| `DecisionStopped`, `DecisionSkipped`, `ApprovalRequired` | per-decision exceptions |
|
|
95
|
+
| `derive_idempotency_key(...)` | deterministic key from (tenant, run, step, llm_call, trigger) |
|
|
96
|
+
| `new_uuid7()` | UUID v7 helper |
|
|
97
|
+
|
|
98
|
+
## Wire-protocol compatibility
|
|
99
|
+
|
|
100
|
+
This SDK pins to a specific protobuf wire version. Check the
|
|
101
|
+
sidecar's published version against `spendguard.__version__`; minor
|
|
102
|
+
versions are wire-compatible, major bumps are breaking. (Not yet
|
|
103
|
+
enforced at handshake; planned for v0.2.)
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
Apache-2.0.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Minimal end-to-end example: pydantic-ai Agent with SpendGuard gating.
|
|
2
|
+
|
|
3
|
+
Run with a sidecar listening on the configured UDS:
|
|
4
|
+
|
|
5
|
+
SPENDGUARD_SIDECAR_UDS=/run/spendguard.sock \
|
|
6
|
+
SPENDGUARD_TENANT_ID=11111111-1111-1111-1111-111111111111 \
|
|
7
|
+
SPENDGUARD_BUDGET_ID=22222222-2222-2222-2222-222222222222 \
|
|
8
|
+
SPENDGUARD_WINDOW_INSTANCE_ID=33333333-3333-3333-3333-333333333333 \
|
|
9
|
+
OPENAI_API_KEY=sk-... \
|
|
10
|
+
python examples/basic_agent.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import os
|
|
17
|
+
import uuid
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
|
|
20
|
+
from pydantic_ai import Agent
|
|
21
|
+
from pydantic_ai.models.openai import OpenAIModel
|
|
22
|
+
|
|
23
|
+
from spendguard_pydantic_ai import (
|
|
24
|
+
RunContext,
|
|
25
|
+
SpendGuardClient,
|
|
26
|
+
SpendGuardModel,
|
|
27
|
+
new_uuid7,
|
|
28
|
+
run_context,
|
|
29
|
+
)
|
|
30
|
+
from spendguard_pydantic_ai._proto.spendguard.common.v1 import common_pb2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env(name: str) -> str:
|
|
34
|
+
val = os.environ.get(name)
|
|
35
|
+
if not val:
|
|
36
|
+
raise RuntimeError(f"env var {name} is required")
|
|
37
|
+
return val
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def estimate_claims(
|
|
41
|
+
messages: Sequence[object],
|
|
42
|
+
model_settings: object | None,
|
|
43
|
+
) -> list[common_pb2.BudgetClaim]:
|
|
44
|
+
"""Naive token estimator: 1 claim per call, 500 tokens projected.
|
|
45
|
+
|
|
46
|
+
Production code should use a real tokenizer (tiktoken /
|
|
47
|
+
anthropic-tokenizer) to count input tokens, then add the
|
|
48
|
+
`model_settings.max_tokens` ceiling for output.
|
|
49
|
+
"""
|
|
50
|
+
return [
|
|
51
|
+
common_pb2.BudgetClaim(
|
|
52
|
+
budget_id=_env("SPENDGUARD_BUDGET_ID"),
|
|
53
|
+
unit=common_pb2.UnitRef(
|
|
54
|
+
unit_id=_env("SPENDGUARD_UNIT_ID"),
|
|
55
|
+
token_kind="output_token",
|
|
56
|
+
model_family="gpt-4",
|
|
57
|
+
),
|
|
58
|
+
amount_atomic="500",
|
|
59
|
+
direction=common_pb2.BudgetClaim.DEBIT,
|
|
60
|
+
window_instance_id=_env("SPENDGUARD_WINDOW_INSTANCE_ID"),
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def main() -> None:
|
|
66
|
+
socket_path = _env("SPENDGUARD_SIDECAR_UDS")
|
|
67
|
+
tenant_id = _env("SPENDGUARD_TENANT_ID")
|
|
68
|
+
budget_id = _env("SPENDGUARD_BUDGET_ID")
|
|
69
|
+
window_id = _env("SPENDGUARD_WINDOW_INSTANCE_ID")
|
|
70
|
+
|
|
71
|
+
inner = OpenAIModel("gpt-4o-mini")
|
|
72
|
+
|
|
73
|
+
async with SpendGuardClient(
|
|
74
|
+
socket_path=socket_path,
|
|
75
|
+
tenant_id=tenant_id,
|
|
76
|
+
) as client:
|
|
77
|
+
await client.handshake()
|
|
78
|
+
|
|
79
|
+
guarded = SpendGuardModel(
|
|
80
|
+
inner=inner,
|
|
81
|
+
client=client,
|
|
82
|
+
budget_id=budget_id,
|
|
83
|
+
window_instance_id=window_id,
|
|
84
|
+
unit=common_pb2.UnitRef(
|
|
85
|
+
unit_id=_env("SPENDGUARD_UNIT_ID"),
|
|
86
|
+
token_kind="output_token",
|
|
87
|
+
model_family="gpt-4",
|
|
88
|
+
),
|
|
89
|
+
pricing=common_pb2.PricingFreeze(
|
|
90
|
+
pricing_version=_env("SPENDGUARD_PRICING_VERSION"),
|
|
91
|
+
price_snapshot_hash=bytes.fromhex(
|
|
92
|
+
_env("SPENDGUARD_PRICE_SNAPSHOT_HASH_HEX")
|
|
93
|
+
),
|
|
94
|
+
fx_rate_version=_env("SPENDGUARD_FX_RATE_VERSION"),
|
|
95
|
+
unit_conversion_version=_env("SPENDGUARD_UNIT_CONVERSION_VERSION"),
|
|
96
|
+
),
|
|
97
|
+
claim_estimator=estimate_claims,
|
|
98
|
+
)
|
|
99
|
+
agent = Agent(model=guarded)
|
|
100
|
+
|
|
101
|
+
run_id = str(new_uuid7())
|
|
102
|
+
async with run_context(RunContext(run_id=run_id)):
|
|
103
|
+
result = await agent.run("Say hello in three words.")
|
|
104
|
+
print(f"agent output: {result.output}")
|
|
105
|
+
print(f"run_id: {run_id}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spendguard-sdk"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "SpendGuard SDK — runtime safety layer client for AI agent frameworks (Pydantic-AI, LangChain, LangGraph, OpenAI Agents SDK)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Michael Chen", email = "m24927605@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Operating System :: POSIX :: Linux",
|
|
24
|
+
"Operating System :: MacOS",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Core deps: the SpendGuardClient + proto stubs only. Framework deps
|
|
28
|
+
# are gated behind extras so a user who only writes raw OpenAI SDK code
|
|
29
|
+
# doesn't pull in pydantic-ai / langchain / etc.
|
|
30
|
+
dependencies = [
|
|
31
|
+
"grpcio>=1.62",
|
|
32
|
+
"protobuf>=4.25,<6",
|
|
33
|
+
"pydantic>=2.6",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
pydantic-ai = [
|
|
38
|
+
"pydantic-ai>=0.0.20,<0.1.0",
|
|
39
|
+
]
|
|
40
|
+
langchain = [
|
|
41
|
+
"langchain-core>=0.3",
|
|
42
|
+
"langchain>=0.3",
|
|
43
|
+
]
|
|
44
|
+
langgraph = [
|
|
45
|
+
"langchain-core>=0.3",
|
|
46
|
+
"langgraph>=0.2",
|
|
47
|
+
]
|
|
48
|
+
openai-agents = [
|
|
49
|
+
"openai-agents>=0.17",
|
|
50
|
+
]
|
|
51
|
+
agt = [
|
|
52
|
+
"agent-governance-toolkit>=3.4",
|
|
53
|
+
"agent-os-kernel>=3.0",
|
|
54
|
+
]
|
|
55
|
+
dev = [
|
|
56
|
+
"grpcio-tools>=1.62",
|
|
57
|
+
"pytest>=8",
|
|
58
|
+
"pytest-asyncio>=0.23",
|
|
59
|
+
"ruff>=0.5",
|
|
60
|
+
"mypy>=1.10",
|
|
61
|
+
"build>=1.2",
|
|
62
|
+
"twine>=5",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[project.urls]
|
|
66
|
+
Homepage = "https://github.com/m24927605/agentic-spendguard"
|
|
67
|
+
Repository = "https://github.com/m24927605/agentic-spendguard"
|
|
68
|
+
Issues = "https://github.com/m24927605/agentic-spendguard/issues"
|
|
69
|
+
|
|
70
|
+
[tool.hatch.build.targets.wheel]
|
|
71
|
+
packages = ["src/spendguard"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py310"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = ["E", "F", "W", "I", "B", "UP", "ANN", "ASYNC", "S"]
|
|
79
|
+
ignore = ["ANN101", "ANN102", "S101"]
|
|
80
|
+
|
|
81
|
+
[tool.mypy]
|
|
82
|
+
python_version = "3.10"
|
|
83
|
+
strict = true
|
|
84
|
+
warn_unused_configs = true
|
|
85
|
+
|
|
86
|
+
[tool.pytest.ini_options]
|
|
87
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""SpendGuard SDK — runtime safety layer client for AI agent frameworks.
|
|
2
|
+
|
|
3
|
+
Core surface (always available):
|
|
4
|
+
|
|
5
|
+
from spendguard import SpendGuardClient, DecisionStopped, derive_idempotency_key
|
|
6
|
+
|
|
7
|
+
Framework integrations are optional (install via extras):
|
|
8
|
+
|
|
9
|
+
pip install spendguard-sdk[pydantic-ai]
|
|
10
|
+
pip install spendguard-sdk[langchain]
|
|
11
|
+
pip install spendguard-sdk[langgraph]
|
|
12
|
+
pip install spendguard-sdk[openai-agents]
|
|
13
|
+
|
|
14
|
+
After installing the relevant extras::
|
|
15
|
+
|
|
16
|
+
from spendguard.integrations.pydantic_ai import SpendGuardModel
|
|
17
|
+
from spendguard.integrations.langchain import SpendGuardChatModel
|
|
18
|
+
# ...
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import (
|
|
22
|
+
DEFAULT_DECISION_TIMEOUT_S,
|
|
23
|
+
DEFAULT_HANDSHAKE_TIMEOUT_S,
|
|
24
|
+
DecisionOutcome,
|
|
25
|
+
HandshakeOutcome,
|
|
26
|
+
SpendGuardClient,
|
|
27
|
+
)
|
|
28
|
+
from .errors import (
|
|
29
|
+
ApprovalRequired,
|
|
30
|
+
DecisionDenied,
|
|
31
|
+
DecisionSkipped,
|
|
32
|
+
DecisionStopped,
|
|
33
|
+
HandshakeError,
|
|
34
|
+
MutationApplyFailed,
|
|
35
|
+
SidecarUnavailable,
|
|
36
|
+
SpendGuardError,
|
|
37
|
+
)
|
|
38
|
+
from .ids import (
|
|
39
|
+
default_call_signature,
|
|
40
|
+
derive_idempotency_key,
|
|
41
|
+
derive_uuid_from_signature,
|
|
42
|
+
new_uuid7,
|
|
43
|
+
workload_instance_id,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# client
|
|
48
|
+
"DEFAULT_DECISION_TIMEOUT_S",
|
|
49
|
+
"DEFAULT_HANDSHAKE_TIMEOUT_S",
|
|
50
|
+
"DecisionOutcome",
|
|
51
|
+
"HandshakeOutcome",
|
|
52
|
+
"SpendGuardClient",
|
|
53
|
+
# errors
|
|
54
|
+
"ApprovalRequired",
|
|
55
|
+
"DecisionDenied",
|
|
56
|
+
"DecisionSkipped",
|
|
57
|
+
"DecisionStopped",
|
|
58
|
+
"HandshakeError",
|
|
59
|
+
"MutationApplyFailed",
|
|
60
|
+
"SidecarUnavailable",
|
|
61
|
+
"SpendGuardError",
|
|
62
|
+
# ids
|
|
63
|
+
"default_call_signature",
|
|
64
|
+
"derive_idempotency_key",
|
|
65
|
+
"derive_uuid_from_signature",
|
|
66
|
+
"new_uuid7",
|
|
67
|
+
"workload_instance_id",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
__version__ = "0.1.0a1"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Generated protobuf + gRPC stubs.
|
|
2
|
+
|
|
3
|
+
Populated by `make proto` from the wire spec under `proto/spendguard/`.
|
|
4
|
+
The rest of the adapter imports specific generated modules from this
|
|
5
|
+
namespace (e.g. `spendguard._proto.spendguard.common.v1`);
|
|
6
|
+
attempting those imports before `make proto` raises a helpful
|
|
7
|
+
ImportError pointing the developer at the build step.
|
|
8
|
+
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|