sage-agent-sdk 1.0.0__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.
- sage_agent_sdk-1.0.0/PKG-INFO +950 -0
- sage_agent_sdk-1.0.0/README.md +921 -0
- sage_agent_sdk-1.0.0/pyproject.toml +44 -0
- sage_agent_sdk-1.0.0/setup.cfg +4 -0
- sage_agent_sdk-1.0.0/src/sage_agent_sdk.egg-info/PKG-INFO +950 -0
- sage_agent_sdk-1.0.0/src/sage_agent_sdk.egg-info/SOURCES.txt +19 -0
- sage_agent_sdk-1.0.0/src/sage_agent_sdk.egg-info/dependency_links.txt +1 -0
- sage_agent_sdk-1.0.0/src/sage_agent_sdk.egg-info/requires.txt +8 -0
- sage_agent_sdk-1.0.0/src/sage_agent_sdk.egg-info/top_level.txt +1 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/__init__.py +8 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/async_client.py +368 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/auth.py +72 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/client.py +364 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/exceptions.py +65 -0
- sage_agent_sdk-1.0.0/src/sage_sdk/models.py +197 -0
- sage_agent_sdk-1.0.0/tests/test_async_client.py +57 -0
- sage_agent_sdk-1.0.0/tests/test_auth.py +52 -0
- sage_agent_sdk-1.0.0/tests/test_client.py +74 -0
- sage_agent_sdk-1.0.0/tests/test_dept_rbac.py +205 -0
- sage_agent_sdk-1.0.0/tests/test_models.py +60 -0
- sage_agent_sdk-1.0.0/tests/test_org_federation.py +316 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sage-agent-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for SAGE — Sovereign Agent Governed Experience. Persistent, consensus-validated memory for AI agents.
|
|
5
|
+
Author-email: Dhillon Andrew Kannabhiran <dhillon@levelupctf.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/l33tdawg/sage
|
|
8
|
+
Project-URL: Documentation, https://l33tdawg.github.io/sage/
|
|
9
|
+
Project-URL: Repository, https://github.com/l33tdawg/sage
|
|
10
|
+
Project-URL: Issues, https://github.com/l33tdawg/sage/issues
|
|
11
|
+
Keywords: sage,ai,agent,memory,bft,consensus,llm,mcp
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Requires-Dist: PyNaCl>=1.5.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# SAGE Python SDK
|
|
31
|
+
|
|
32
|
+
Python client for the SAGE (Sovereign Agent Governed Experience) protocol -- a governed, verifiable institutional memory layer for multi-agent systems.
|
|
33
|
+
|
|
34
|
+
**Requires Python 3.10+**
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# From source (development)
|
|
40
|
+
git clone <repo-url>
|
|
41
|
+
cd sage/sdk/python
|
|
42
|
+
pip install -e .
|
|
43
|
+
|
|
44
|
+
# With dev/test dependencies
|
|
45
|
+
pip install -e ".[dev]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from sage_sdk import SageClient, AgentIdentity
|
|
52
|
+
|
|
53
|
+
# Generate a new agent identity (Ed25519 keypair)
|
|
54
|
+
identity = AgentIdentity.generate()
|
|
55
|
+
|
|
56
|
+
# Save for reuse across sessions
|
|
57
|
+
identity.to_file("my_agent.key")
|
|
58
|
+
|
|
59
|
+
# Connect to a SAGE node
|
|
60
|
+
client = SageClient(base_url="http://localhost:8080", identity=identity)
|
|
61
|
+
|
|
62
|
+
# Submit a memory
|
|
63
|
+
result = client.propose(
|
|
64
|
+
content="Flask web challenges with SQLi require prepared statements bypass",
|
|
65
|
+
memory_type="fact",
|
|
66
|
+
domain_tag="challenge_generation",
|
|
67
|
+
confidence=0.85,
|
|
68
|
+
)
|
|
69
|
+
print(f"Memory {result.memory_id} submitted (tx: {result.tx_hash})")
|
|
70
|
+
|
|
71
|
+
# Query by vector similarity
|
|
72
|
+
matches = client.query(
|
|
73
|
+
embedding=[0.1] * 768, # 768-dim (nomic-embed-text)
|
|
74
|
+
domain_tag="challenge_generation",
|
|
75
|
+
min_confidence=0.7,
|
|
76
|
+
top_k=5,
|
|
77
|
+
)
|
|
78
|
+
for mem in matches.results:
|
|
79
|
+
print(f" [{mem.status.value}] {mem.content[:80]}")
|
|
80
|
+
|
|
81
|
+
# Vote on a proposed memory
|
|
82
|
+
client.vote(result.memory_id, decision="accept", rationale="Verified correct")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Authentication
|
|
86
|
+
|
|
87
|
+
SAGE uses Ed25519 keypairs for agent identity. Every API request is signed with the agent's private key.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from sage_sdk import AgentIdentity
|
|
91
|
+
|
|
92
|
+
# Generate a new identity
|
|
93
|
+
identity = AgentIdentity.generate()
|
|
94
|
+
|
|
95
|
+
# The agent_id is the hex-encoded public key
|
|
96
|
+
print(identity.agent_id) # e.g. "a1b2c3d4..."
|
|
97
|
+
|
|
98
|
+
# Persist to disk
|
|
99
|
+
identity.to_file("agent.key")
|
|
100
|
+
|
|
101
|
+
# Load from disk
|
|
102
|
+
identity = AgentIdentity.from_file("agent.key")
|
|
103
|
+
|
|
104
|
+
# Create from a known 32-byte seed (deterministic)
|
|
105
|
+
identity = AgentIdentity.from_seed(b"\x00" * 32)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Request signing is handled automatically by the client. Each request includes three headers:
|
|
109
|
+
|
|
110
|
+
| Header | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `X-Agent-ID` | Hex-encoded public verify key |
|
|
113
|
+
| `X-Signature` | Ed25519 signature of `SHA256(body) || timestamp` |
|
|
114
|
+
| `X-Timestamp` | Unix timestamp (seconds) |
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `propose()`
|
|
119
|
+
|
|
120
|
+
Submit a new memory to the network for validation.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
result = client.propose(
|
|
124
|
+
content="The observation text",
|
|
125
|
+
memory_type="fact", # "fact", "observation", or "inference"
|
|
126
|
+
domain_tag="security",
|
|
127
|
+
confidence=0.9, # 0.0 - 1.0
|
|
128
|
+
embedding=[0.1, 0.2, ...], # Optional: precomputed vector
|
|
129
|
+
knowledge_triples=[ # Optional: structured knowledge
|
|
130
|
+
KnowledgeTriple(subject="SQLi", predicate="bypasses", object_="prepared_statements")
|
|
131
|
+
],
|
|
132
|
+
parent_hash="abc123", # Optional: link to parent memory
|
|
133
|
+
)
|
|
134
|
+
# Returns: MemorySubmitResponse(memory_id, tx_hash, status)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Server endpoint:** `POST /v1/memory/submit`
|
|
138
|
+
|
|
139
|
+
### `query()`
|
|
140
|
+
|
|
141
|
+
Search memories by vector similarity. All parameters are sent in the POST body.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
results = client.query(
|
|
145
|
+
embedding=[0.1] * 768, # 768-dim (nomic-embed-text) # Required: query vector
|
|
146
|
+
domain_tag="security", # Optional: filter by domain
|
|
147
|
+
min_confidence=0.7, # Optional: minimum confidence threshold
|
|
148
|
+
top_k=10, # Number of results (default: 10)
|
|
149
|
+
status_filter="committed", # Optional: filter by status
|
|
150
|
+
cursor="abc123", # Optional: pagination cursor
|
|
151
|
+
)
|
|
152
|
+
# Returns: MemoryQueryResponse(results, next_cursor, total_count)
|
|
153
|
+
for memory in results.results:
|
|
154
|
+
print(f"{memory.memory_id}: {memory.content}")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Server endpoint:** `POST /v1/memory/query`
|
|
158
|
+
|
|
159
|
+
### `get_memory()`
|
|
160
|
+
|
|
161
|
+
Retrieve a single memory by ID.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
memory = client.get_memory("550e8400-e29b-41d4-a716-446655440000")
|
|
165
|
+
# Returns: MemoryRecord
|
|
166
|
+
print(memory.content, memory.status, memory.confidence_score)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Server endpoint:** `GET /v1/memory/{id}`
|
|
170
|
+
|
|
171
|
+
### `vote()`
|
|
172
|
+
|
|
173
|
+
Cast a vote on a proposed memory.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
result = client.vote(
|
|
177
|
+
memory_id="550e8400-...",
|
|
178
|
+
decision="accept", # "accept", "reject", or "abstain"
|
|
179
|
+
rationale="Verified correct", # Optional
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Server endpoint:** `POST /v1/memory/{id}/vote`
|
|
184
|
+
|
|
185
|
+
### `challenge()`
|
|
186
|
+
|
|
187
|
+
Challenge a committed memory with evidence.
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
result = client.challenge(
|
|
191
|
+
memory_id="550e8400-...",
|
|
192
|
+
reason="Outdated information",
|
|
193
|
+
evidence="See CVE-2024-XXXX", # Optional
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Server endpoint:** `POST /v1/memory/{id}/challenge`
|
|
198
|
+
|
|
199
|
+
### `corroborate()`
|
|
200
|
+
|
|
201
|
+
Corroborate an existing memory to strengthen its confidence.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
result = client.corroborate(
|
|
205
|
+
memory_id="550e8400-...",
|
|
206
|
+
evidence="Independently verified via testing", # Optional
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Server endpoint:** `POST /v1/memory/{id}/corroborate`
|
|
211
|
+
|
|
212
|
+
### `get_profile()`
|
|
213
|
+
|
|
214
|
+
Get the current agent's profile and Proof of Experience weight.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
profile = client.get_profile()
|
|
218
|
+
print(f"Agent: {profile.agent_id}")
|
|
219
|
+
print(f"PoE Weight: {profile.poe_weight}")
|
|
220
|
+
print(f"Votes Cast: {profile.vote_count}")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Server endpoint:** `GET /v1/agent/me`
|
|
224
|
+
|
|
225
|
+
### `register_domain()`
|
|
226
|
+
|
|
227
|
+
Register a new domain. The registering agent becomes the domain owner and can control access.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
result = client.register_domain(
|
|
231
|
+
name="security.crypto", # Domain name (hierarchical with dots)
|
|
232
|
+
description="Cryptographic security knowledge", # Optional
|
|
233
|
+
parent="security", # Optional: parent domain
|
|
234
|
+
)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Server endpoint:** `POST /v1/domain/register`
|
|
238
|
+
|
|
239
|
+
### `get_domain()`
|
|
240
|
+
|
|
241
|
+
Look up domain info including owner.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
info = client.get_domain("security.crypto")
|
|
245
|
+
print(f"Owner: {info['owner_agent_id']}")
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Server endpoint:** `GET /v1/domain/{name}`
|
|
249
|
+
|
|
250
|
+
### `request_access()`
|
|
251
|
+
|
|
252
|
+
Request access to a domain owned by another agent.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
result = client.request_access(
|
|
256
|
+
domain="security.crypto",
|
|
257
|
+
justification="Need to submit cryptographic observations",
|
|
258
|
+
level=1, # Clearance level (1-4)
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Server endpoint:** `POST /v1/access/request`
|
|
263
|
+
|
|
264
|
+
### `grant_access()`
|
|
265
|
+
|
|
266
|
+
Grant access to a domain you own.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
result = client.grant_access(
|
|
270
|
+
grantee_id="a1b2c3...", # Agent to grant access to
|
|
271
|
+
domain="security.crypto",
|
|
272
|
+
level=1, # Clearance level (1-4)
|
|
273
|
+
expires_at=0, # Unix timestamp, 0 = never expires
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Server endpoint:** `POST /v1/access/grant`
|
|
278
|
+
|
|
279
|
+
### `revoke_access()`
|
|
280
|
+
|
|
281
|
+
Revoke a previously granted access.
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
result = client.revoke_access(
|
|
285
|
+
grantee_id="a1b2c3...",
|
|
286
|
+
domain="security.crypto",
|
|
287
|
+
reason="Agent decommissioned",
|
|
288
|
+
)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Server endpoint:** `POST /v1/access/revoke`
|
|
292
|
+
|
|
293
|
+
### `list_grants()`
|
|
294
|
+
|
|
295
|
+
List active access grants for an agent.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
grants = client.list_grants() # Current agent's grants
|
|
299
|
+
grants = client.list_grants("a1b2c3") # Specific agent's grants
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Server endpoint:** `GET /v1/access/grants/{agent_id}`
|
|
303
|
+
|
|
304
|
+
## Access Control
|
|
305
|
+
|
|
306
|
+
SAGE uses a hierarchical access control model. See the **Deployment Guide** section below for the complete setup walkthrough.
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
Organization → Department → Domain → Agent (with clearance 0-4)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Key points:
|
|
313
|
+
- **Register domains before submitting memories** — unregistered domains have no access control
|
|
314
|
+
- **Department boundaries are enforced** — agents in one dept cannot see another dept's memories
|
|
315
|
+
- **Federation enables cross-org access** — scoped by department and clearance cap
|
|
316
|
+
- **All access control operations are on-chain BFT transactions** — immutable once committed
|
|
317
|
+
|
|
318
|
+
## Deployment Guide: Access Control Setup
|
|
319
|
+
|
|
320
|
+
> **CRITICAL: Access controls are on-chain and immutable. You MUST define your organization structure, departments, domains, and agent memberships BEFORE agents start submitting memories. Memories submitted before access controls exist cannot be retroactively restricted. Plan your access hierarchy NOW, not later.**
|
|
321
|
+
|
|
322
|
+
### Architecture Overview
|
|
323
|
+
|
|
324
|
+
SAGE has two layers of identity:
|
|
325
|
+
|
|
326
|
+
1. **Validator nodes** — the 4+ CometBFT nodes running consensus. These are infrastructure.
|
|
327
|
+
2. **Application agents** — the AI agents that submit, query, and vote on memories. Each needs its own Ed25519 keypair, org membership, and department assignment.
|
|
328
|
+
|
|
329
|
+
The access control hierarchy:
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
Organization (on-chain entity — controls all access within)
|
|
333
|
+
├── Department A (subdivision — scopes agent access)
|
|
334
|
+
│ ├── Domain "security.crypto" (knowledge category — access-controlled)
|
|
335
|
+
│ │ ├── Agent 1 (clearance 2: can read+write)
|
|
336
|
+
│ │ └── Agent 2 (clearance 1: read-only)
|
|
337
|
+
│ └── Domain "security.web"
|
|
338
|
+
│ └── Agent 3 (clearance 2)
|
|
339
|
+
└── Department B
|
|
340
|
+
└── Domain "research.ml"
|
|
341
|
+
└── Agent 4 (clearance 3)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Access rules:**
|
|
345
|
+
- An agent in Dept A can access Dept A's domains — but NOT Dept B's domains (same org, different dept)
|
|
346
|
+
- An agent in Org X cannot access ANY memories in Org Y — unless an explicit federation agreement exists
|
|
347
|
+
- Federation agreements are scoped: "Org X allows Org Y's Engineering dept to access our data, max clearance 2"
|
|
348
|
+
- An agent always has access to memories it submitted, regardless of RBAC
|
|
349
|
+
|
|
350
|
+
### Step 0: Deploy the Chain
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Generate 4-node validator configs
|
|
354
|
+
make init
|
|
355
|
+
|
|
356
|
+
# Start the BFT network (4 CometBFT + 4 ABCI + PostgreSQL + Ollama)
|
|
357
|
+
make up
|
|
358
|
+
|
|
359
|
+
# Verify all nodes are healthy
|
|
360
|
+
make status
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The chain is now running with 4 validator nodes. No application agents exist yet — the validators handle consensus only.
|
|
364
|
+
|
|
365
|
+
### Step 1: Create Org Admin Identity
|
|
366
|
+
|
|
367
|
+
The org admin is the first agent registered. This keypair has permanent admin authority over the organization. **Store it securely — it cannot be changed.**
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
from sage_sdk import SageClient, AgentIdentity
|
|
371
|
+
|
|
372
|
+
# Generate the org admin keypair
|
|
373
|
+
admin = AgentIdentity.generate()
|
|
374
|
+
admin.to_file("org_admin.key") # BACK THIS UP — it's your org's root authority
|
|
375
|
+
|
|
376
|
+
admin_client = SageClient(base_url="http://localhost:8080", identity=admin)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Step 2: Register Your Organization
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
org = admin_client.register_org("Acme Corp", description="AI security research")
|
|
383
|
+
org_id = org["org_id"]
|
|
384
|
+
print(f"Organization registered: {org_id}")
|
|
385
|
+
# Save org_id — you'll need it for every subsequent operation
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This is an on-chain BFT transaction. Once committed, the registering agent becomes the permanent admin.
|
|
389
|
+
|
|
390
|
+
### Step 3: Create ALL Departments
|
|
391
|
+
|
|
392
|
+
Define every department your organization needs. Each department is an access boundary — agents in one department cannot see memories in another.
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
# Create departments for each team/function
|
|
396
|
+
eng = admin_client.register_dept(org_id, name="Engineering", description="Core engineering team")
|
|
397
|
+
eng_dept = eng["dept_id"]
|
|
398
|
+
|
|
399
|
+
security = admin_client.register_dept(org_id, name="Security", description="Security research")
|
|
400
|
+
sec_dept = security["dept_id"]
|
|
401
|
+
|
|
402
|
+
research = admin_client.register_dept(org_id, name="Research", description="ML research")
|
|
403
|
+
res_dept = research["dept_id"]
|
|
404
|
+
|
|
405
|
+
# Sub-departments are supported (optional)
|
|
406
|
+
crypto = admin_client.register_dept(
|
|
407
|
+
org_id, name="Cryptography", description="Crypto team", parent_dept=sec_dept
|
|
408
|
+
)
|
|
409
|
+
crypto_dept = crypto["dept_id"]
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Step 4: Register ALL Domains
|
|
413
|
+
|
|
414
|
+
**This is the most critical step.** Unregistered domains have NO access control — any agent can read and write. You must register every domain your agents will use.
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
# Register domains — the admin agent becomes the owner
|
|
418
|
+
admin_client.register_domain(name="security.crypto", description="Cryptographic security knowledge")
|
|
419
|
+
admin_client.register_domain(name="security.web", description="Web security knowledge")
|
|
420
|
+
admin_client.register_domain(name="research.ml", description="ML research findings")
|
|
421
|
+
admin_client.register_domain(name="engineering.infra", description="Infrastructure knowledge")
|
|
422
|
+
|
|
423
|
+
# Hierarchical domains: register parent first, then children
|
|
424
|
+
admin_client.register_domain(name="security", description="All security knowledge")
|
|
425
|
+
admin_client.register_domain(name="security.vuln_intel", description="Vulnerability intelligence", parent="security")
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
After this step, these domains are access-controlled. Only agents with explicit clearance can read or write to them.
|
|
429
|
+
|
|
430
|
+
### Step 5: Generate Agent Identities
|
|
431
|
+
|
|
432
|
+
Create a keypair for each AI agent that will interact with the chain. Each agent is a separate identity.
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
# Generate keypairs for all your agents
|
|
436
|
+
designer_agent = AgentIdentity.generate()
|
|
437
|
+
designer_agent.to_file("agents/designer.key")
|
|
438
|
+
|
|
439
|
+
evaluator_agent = AgentIdentity.generate()
|
|
440
|
+
evaluator_agent.to_file("agents/evaluator.key")
|
|
441
|
+
|
|
442
|
+
validator_agent = AgentIdentity.generate()
|
|
443
|
+
validator_agent.to_file("agents/validator.key")
|
|
444
|
+
|
|
445
|
+
orchestrator_agent = AgentIdentity.generate()
|
|
446
|
+
orchestrator_agent.to_file("agents/orchestrator.key")
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Step 6: Add Agents to Organization + Departments
|
|
450
|
+
|
|
451
|
+
Each agent must be added to the org first, then to their specific department(s).
|
|
452
|
+
|
|
453
|
+
```python
|
|
454
|
+
# Add agents to org with clearance level
|
|
455
|
+
admin_client.add_org_member(org_id, designer_agent.agent_id, clearance=2, role="member")
|
|
456
|
+
admin_client.add_org_member(org_id, evaluator_agent.agent_id, clearance=2, role="member")
|
|
457
|
+
admin_client.add_org_member(org_id, validator_agent.agent_id, clearance=3, role="member")
|
|
458
|
+
admin_client.add_org_member(org_id, orchestrator_agent.agent_id, clearance=2, role="member")
|
|
459
|
+
|
|
460
|
+
# Assign agents to departments — this determines what domains they can access
|
|
461
|
+
admin_client.add_dept_member(org_id, sec_dept, designer_agent.agent_id, clearance=2)
|
|
462
|
+
admin_client.add_dept_member(org_id, sec_dept, evaluator_agent.agent_id, clearance=2)
|
|
463
|
+
admin_client.add_dept_member(org_id, sec_dept, validator_agent.agent_id, clearance=3)
|
|
464
|
+
admin_client.add_dept_member(org_id, eng_dept, orchestrator_agent.agent_id, clearance=2)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Step 7: Agents Can Now Operate
|
|
468
|
+
|
|
469
|
+
Only NOW should agents start submitting and querying memories.
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
# Designer agent submits to its department's domain
|
|
473
|
+
designer_client = SageClient(base_url="http://localhost:8080", identity=designer_agent)
|
|
474
|
+
designer_client.propose(
|
|
475
|
+
content="AES-GCM nonce reuse leads to key recovery",
|
|
476
|
+
memory_type="fact",
|
|
477
|
+
domain_tag="security.crypto",
|
|
478
|
+
confidence=0.95,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Evaluator in the SAME department can query it
|
|
482
|
+
evaluator_client = SageClient(base_url="http://localhost:8080", identity=evaluator_agent)
|
|
483
|
+
results = evaluator_client.query(
|
|
484
|
+
embedding=evaluator_client.embed("AES vulnerabilities"),
|
|
485
|
+
domain_tag="security.crypto",
|
|
486
|
+
status_filter="committed",
|
|
487
|
+
)
|
|
488
|
+
# Returns results — evaluator has clearance in the Security department
|
|
489
|
+
|
|
490
|
+
# Orchestrator in ENGINEERING department CANNOT query Security domain
|
|
491
|
+
orchestrator_client = SageClient(base_url="http://localhost:8080", identity=orchestrator_agent)
|
|
492
|
+
results = orchestrator_client.query(
|
|
493
|
+
embedding=orchestrator_client.embed("AES vulnerabilities"),
|
|
494
|
+
domain_tag="security.crypto",
|
|
495
|
+
status_filter="committed",
|
|
496
|
+
)
|
|
497
|
+
# Returns EMPTY — orchestrator is in Engineering, not Security
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Write-Side Domain Enforcement
|
|
501
|
+
|
|
502
|
+
```
|
|
503
|
+
┌─────────────────────────────────────────────┐
|
|
504
|
+
│ (S)AGE ABCI State Machine │
|
|
505
|
+
│ │
|
|
506
|
+
Agent A ──propose()──►│ processMemorySubmit() │
|
|
507
|
+
(dept: security) │ ├─ Ed25519 signature ✓ (on-chain) │
|
|
508
|
+
│ ├─ Domain tag check ? (YOUR app) ◄────┼─── You implement this
|
|
509
|
+
│ └─ Store to BadgerDB + PostgreSQL │
|
|
510
|
+
│ │
|
|
511
|
+
Agent B ──query()────►│ processMemoryQuery() │
|
|
512
|
+
(dept: engineering) │ ├─ Ed25519 signature ✓ (on-chain) │
|
|
513
|
+
│ ├─ Domain access gate ✓ (on-chain) ◄────┼─── Already enforced
|
|
514
|
+
│ └─ Return results (filtered by RBAC) │
|
|
515
|
+
└─────────────────────────────────────────────┘
|
|
516
|
+
|
|
517
|
+
Read-side: ON-CHAIN — consensus-enforced, cannot be bypassed
|
|
518
|
+
Write-side: YOUR APP — implement in ABCI handler or application layer
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Read-side access control is enforced on-chain by the ABCI state machine — agents can only query domains they have clearance for. **Write-side enforcement is your responsibility** when building your ABCI application.
|
|
522
|
+
|
|
523
|
+
The base (S)AGE ABCI accepts any `domain_tag` on memory submissions. This is by design — your application defines what domain taxonomy rules to enforce. Without write-side checks, any agent can submit memories tagged to any domain, which **pollutes retrieval** for all downstream consumers.
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
WITHOUT write-side enforcement:
|
|
527
|
+
|
|
528
|
+
Telemetry Agent ──► domain: "security.analysis" ──► "Status OK, 289s"
|
|
529
|
+
Telemetry Agent ──► domain: "security.analysis" ──► "Status OK, 311s"
|
|
530
|
+
Telemetry Agent ──► domain: "security.analysis" ──► "Status OK, 254s"
|
|
531
|
+
Analyst Agent ──► domain: "security.analysis" ──► "CVE-2026-1234 requires..."
|
|
532
|
+
Telemetry Agent ──► domain: "security.analysis" ──► "Status OK, 304s"
|
|
533
|
+
|
|
534
|
+
Designer queries "security.analysis" (top_k=5) → gets 4 status lines + 1 analysis
|
|
535
|
+
Result: Designer has no useful knowledge. Performance regresses.
|
|
536
|
+
|
|
537
|
+
WITH write-side enforcement:
|
|
538
|
+
|
|
539
|
+
Telemetry Agent ──► domain: "ops.telemetry" ──► "Status OK, 289s" (correct domain)
|
|
540
|
+
Analyst Agent ──► domain: "security.analysis" ──► "CVE-2026-1234 requires..."
|
|
541
|
+
|
|
542
|
+
Designer queries "security.analysis" (top_k=5) → gets 5 curated analyses
|
|
543
|
+
Result: Designer has full institutional knowledge. Performance improves.
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**Why this matters:** In a production deployment with 10+ agents, a telemetry agent submitting one-line status updates to the same domain as curated analysis reports will drown out the signal. Semantic search returns the telemetry noise instead of the analysis. The knowledge base degrades silently — no errors, just wrong results.
|
|
547
|
+
|
|
548
|
+
**Pattern 1: ABCI-level enforcement (recommended)**
|
|
549
|
+
|
|
550
|
+
Add a domain-tag check in your `processMemorySubmit` handler:
|
|
551
|
+
|
|
552
|
+
```go
|
|
553
|
+
func (app *MyApp) processMemorySubmit(parsedTx *tx.ParsedTx, height int64, blockTime time.Time) *abcitypes.ExecTxResult {
|
|
554
|
+
submit := parsedTx.MemorySubmit
|
|
555
|
+
agentID := auth.PublicKeyToAgentID(parsedTx.PublicKey)
|
|
556
|
+
|
|
557
|
+
// Enforce write-side domain access
|
|
558
|
+
hasWriteAccess, err := app.badgerStore.HasAccessMultiOrg(
|
|
559
|
+
submit.DomainTag, agentID, 2, blockTime, // level 2 = write
|
|
560
|
+
)
|
|
561
|
+
if err != nil || !hasWriteAccess {
|
|
562
|
+
return &abcitypes.ExecTxResult{
|
|
563
|
+
Code: 13,
|
|
564
|
+
Log: fmt.Sprintf("agent %s has no write access to domain %s", agentID[:16], submit.DomainTag),
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ... proceed with memory creation
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**Pattern 2: Application-layer gatekeeper**
|
|
573
|
+
|
|
574
|
+
If you prefer flexibility over strictness, enforce domain tagging in your orchestrator/CEO agent before submission reaches the chain:
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
# CEO validates domain tag before forwarding to SAGE
|
|
578
|
+
AGENT_DOMAIN_MAP = {
|
|
579
|
+
"designer": ["design.generation", "design.patterns"],
|
|
580
|
+
"evaluator": ["evaluation.calibration", "evaluation.hardening"],
|
|
581
|
+
"red_team_auditor": ["red_team.verification"],
|
|
582
|
+
"solution_verifier": ["red_team.solver"], # NOT red_team.verification!
|
|
583
|
+
"quality": ["quality.scoring", "quality.testing"],
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
def validate_submission(agent_name: str, domain_tag: str) -> bool:
|
|
587
|
+
"""Check if agent is allowed to write to this domain."""
|
|
588
|
+
allowed = AGENT_DOMAIN_MAP.get(agent_name, [])
|
|
589
|
+
return any(domain_tag.startswith(prefix) for prefix in allowed)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Pattern 3: Domain prefix convention**
|
|
593
|
+
|
|
594
|
+
Use a naming convention that maps departments to domain prefixes: `{dept}.{subdomain}.{category}`. Then validate that the submitting agent's department matches the domain prefix:
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
# Agent in "red_team" dept can only write to "red_team.*" domains
|
|
598
|
+
agent_dept = get_agent_department(agent_id) # from on-chain RBAC
|
|
599
|
+
domain_prefix = domain_tag.split(".")[0]
|
|
600
|
+
if agent_dept != domain_prefix:
|
|
601
|
+
raise ValueError(f"Agent in {agent_dept} cannot write to {domain_tag}")
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Choose based on your threat model:**
|
|
605
|
+
- Pattern 1 (ABCI): Strongest — consensus-enforced, cannot be bypassed
|
|
606
|
+
- Pattern 2 (Gatekeeper): Flexible — easy to update rules without chain changes
|
|
607
|
+
- Pattern 3 (Convention): Lightweight — works without modifying the ABCI app
|
|
608
|
+
|
|
609
|
+
### Setup Checklist
|
|
610
|
+
|
|
611
|
+
Run through this before any agent submits its first memory:
|
|
612
|
+
|
|
613
|
+
- [ ] Chain deployed and healthy (`make init && make up && make status`)
|
|
614
|
+
- [ ] Org admin keypair generated and backed up securely
|
|
615
|
+
- [ ] Organization registered on-chain
|
|
616
|
+
- [ ] ALL departments created (you can add more later, but plan ahead)
|
|
617
|
+
- [ ] ALL domains registered (unregistered domains have NO access control)
|
|
618
|
+
- [ ] Every agent has a unique Ed25519 keypair
|
|
619
|
+
- [ ] Every agent added to the organization with correct clearance level
|
|
620
|
+
- [ ] Every agent assigned to their department(s)
|
|
621
|
+
- [ ] Write-side domain enforcement implemented (ABCI, gatekeeper, or convention)
|
|
622
|
+
- [ ] Domain taxonomy reviewed — each agent type writes to a distinct domain prefix
|
|
623
|
+
- [ ] Federation agreements established (if cross-org access needed)
|
|
624
|
+
- [ ] Test: submit a memory from Agent A, verify Agent B in same dept can query it
|
|
625
|
+
- [ ] Test: verify Agent C in a different dept CANNOT query it
|
|
626
|
+
- [ ] Test: verify Agent A CANNOT write to Agent C's domain (write-side enforcement)
|
|
627
|
+
|
|
628
|
+
### Development Mode (No RBAC)
|
|
629
|
+
|
|
630
|
+
For local development and testing only, agents can skip the full hierarchy. Any agent with an Ed25519 keypair can immediately submit and query unregistered domains:
|
|
631
|
+
|
|
632
|
+
```python
|
|
633
|
+
from sage_sdk import SageClient, AgentIdentity
|
|
634
|
+
|
|
635
|
+
# Generate keypair — that's it, you're onboarded
|
|
636
|
+
identity = AgentIdentity.generate()
|
|
637
|
+
client = SageClient(base_url="http://localhost:8080", identity=identity)
|
|
638
|
+
|
|
639
|
+
# Submit to any unregistered domain immediately — no access control
|
|
640
|
+
client.propose(
|
|
641
|
+
content="Dev mode observation",
|
|
642
|
+
memory_type="observation",
|
|
643
|
+
domain_tag="testing", # unregistered domain = open access
|
|
644
|
+
confidence=0.8,
|
|
645
|
+
)
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
**WARNING:** Dev mode is for local testing only. In production, unregistered domains are a security gap — any agent can read and write to them.
|
|
649
|
+
|
|
650
|
+
### Cross-Organization Federation
|
|
651
|
+
|
|
652
|
+
Federation allows controlled data sharing between separate organizations. Access is scoped by department and clearance level.
|
|
653
|
+
|
|
654
|
+
```python
|
|
655
|
+
# --- Org A admin proposes federation ---
|
|
656
|
+
fed = admin_client_a.propose_federation(
|
|
657
|
+
target_org_id=org_b_id,
|
|
658
|
+
allowed_depts=["Engineering"], # Only Org B's Engineering dept gets access
|
|
659
|
+
max_clearance=2, # Cap at Confidential (won't see Secret/Top Secret)
|
|
660
|
+
requires_approval=True, # Org B must explicitly approve
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# --- Org B admin approves ---
|
|
664
|
+
# Look up the federation ID (generated on-chain)
|
|
665
|
+
feds = admin_client_b.list_federations(org_b_id)
|
|
666
|
+
fed_id = feds[0]["federation_id"]
|
|
667
|
+
admin_client_b.approve_federation(fed_id)
|
|
668
|
+
|
|
669
|
+
# Now: Org B agents in "Engineering" dept can query Org A's data
|
|
670
|
+
# up to Confidential clearance level
|
|
671
|
+
# Org B agents in "Research" dept still CANNOT see Org A's data
|
|
672
|
+
|
|
673
|
+
# --- Revoke when partnership ends ---
|
|
674
|
+
admin_client_a.revoke_federation(fed_id, reason="Partnership ended")
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
**Federation rules:**
|
|
678
|
+
- Both org admins must agree (propose + approve)
|
|
679
|
+
- `allowed_depts` restricts which departments in the TARGET org can access your data
|
|
680
|
+
- `max_clearance` caps the clearance level — even if an agent has clearance 4, federation cap applies
|
|
681
|
+
- Revocation is immediate and on-chain
|
|
682
|
+
- Omit `allowed_depts` to allow all departments (use with caution)
|
|
683
|
+
|
|
684
|
+
### Organization API Reference
|
|
685
|
+
|
|
686
|
+
| Method | Endpoint | SDK Method |
|
|
687
|
+
|--------|----------|------------|
|
|
688
|
+
| `POST` | `/v1/org/register` | `register_org(name, description)` |
|
|
689
|
+
| `GET` | `/v1/org/{org_id}` | `get_org(org_id)` |
|
|
690
|
+
| `POST` | `/v1/org/{org_id}/member` | `add_org_member(org_id, agent_id, clearance, role)` |
|
|
691
|
+
| `DELETE` | `/v1/org/{org_id}/member/{agent_id}` | `remove_org_member(org_id, agent_id)` |
|
|
692
|
+
| `POST` | `/v1/org/{org_id}/clearance` | `set_org_clearance(org_id, agent_id, clearance)` |
|
|
693
|
+
| `GET` | `/v1/org/{org_id}/members` | `list_org_members(org_id)` |
|
|
694
|
+
|
|
695
|
+
### Department API Reference
|
|
696
|
+
|
|
697
|
+
| Method | Endpoint | SDK Method |
|
|
698
|
+
|--------|----------|------------|
|
|
699
|
+
| `POST` | `/v1/org/{org_id}/dept` | `register_dept(org_id, name, description, parent_dept)` |
|
|
700
|
+
| `GET` | `/v1/org/{org_id}/dept/{dept_id}` | `get_dept(org_id, dept_id)` |
|
|
701
|
+
| `GET` | `/v1/org/{org_id}/depts` | `list_depts(org_id)` |
|
|
702
|
+
| `POST` | `/v1/org/{org_id}/dept/{dept_id}/member` | `add_dept_member(org_id, dept_id, agent_id, clearance, role)` |
|
|
703
|
+
| `DELETE` | `/v1/org/{org_id}/dept/{dept_id}/member/{agent_id}` | `remove_dept_member(org_id, dept_id, agent_id)` |
|
|
704
|
+
| `GET` | `/v1/org/{org_id}/dept/{dept_id}/members` | `list_dept_members(org_id, dept_id)` |
|
|
705
|
+
|
|
706
|
+
### Federation API Reference
|
|
707
|
+
|
|
708
|
+
| Method | Endpoint | SDK Method |
|
|
709
|
+
|--------|----------|------------|
|
|
710
|
+
| `POST` | `/v1/federation/propose` | `propose_federation(target_org_id, allowed_depts, max_clearance)` |
|
|
711
|
+
| `POST` | `/v1/federation/{fed_id}/approve` | `approve_federation(fed_id)` |
|
|
712
|
+
| `POST` | `/v1/federation/{fed_id}/revoke` | `revoke_federation(fed_id, reason)` |
|
|
713
|
+
| `GET` | `/v1/federation/{fed_id}` | `get_federation(fed_id)` |
|
|
714
|
+
| `GET` | `/v1/federation/active/{org_id}` | `list_federations(org_id)` |
|
|
715
|
+
|
|
716
|
+
### Domain & Access API Reference
|
|
717
|
+
|
|
718
|
+
| Method | Endpoint | SDK Method |
|
|
719
|
+
|--------|----------|------------|
|
|
720
|
+
| `POST` | `/v1/domain/register` | `register_domain(name, description, parent)` |
|
|
721
|
+
| `GET` | `/v1/domain/{name}` | `get_domain(name)` |
|
|
722
|
+
| `POST` | `/v1/access/request` | `request_access(domain, justification, level)` |
|
|
723
|
+
| `POST` | `/v1/access/grant` | `grant_access(grantee_id, domain, level, expires_at)` |
|
|
724
|
+
| `POST` | `/v1/access/revoke` | `revoke_access(grantee_id, domain, reason)` |
|
|
725
|
+
| `GET` | `/v1/access/grants/{agent_id}` | `list_grants(agent_id)` |
|
|
726
|
+
|
|
727
|
+
### Clearance Levels
|
|
728
|
+
|
|
729
|
+
| Level | Name | Description |
|
|
730
|
+
|-------|------|-------------|
|
|
731
|
+
| 0 | Public | No registration needed |
|
|
732
|
+
| 1 | Internal | Default for registered domains |
|
|
733
|
+
| 2 | Confidential | Restricted access |
|
|
734
|
+
| 3 | Secret | High-security data |
|
|
735
|
+
| 4 | Top Secret | Maximum restriction |
|
|
736
|
+
|
|
737
|
+
### Key Rules
|
|
738
|
+
|
|
739
|
+
- **Setup order matters.** Register org → departments → domains → agents BEFORE submitting memories.
|
|
740
|
+
- **The org admin is permanent.** The keypair that registered the org has irrevocable admin authority.
|
|
741
|
+
- **Unregistered domains are open.** Any agent can read/write — register all production domains.
|
|
742
|
+
- **Read-side RBAC is on-chain. Write-side RBAC is your responsibility.** The base ABCI enforces query access but accepts any domain tag on submissions. Implement write-side checks in your ABCI app or application layer (see "Write-Side Domain Enforcement" above).
|
|
743
|
+
- **Domain taxonomy determines retrieval quality.** Agents writing to the wrong domain silently degrades search results for all consumers. This is the most common source of knowledge base pollution.
|
|
744
|
+
- **Memories are permanent.** On-chain data cannot be retroactively access-controlled.
|
|
745
|
+
- **Department boundaries are enforced.** Agents in Dept A cannot see Dept B's memories.
|
|
746
|
+
- **Federation is opt-in.** Both orgs must agree. Scoped by department and clearance cap.
|
|
747
|
+
- **An agent always sees its own memories.** Regardless of RBAC, the submitter can always read back.
|
|
748
|
+
- **All operations are on-chain.** BFT consensus ensures no single node can bypass access controls.
|
|
749
|
+
|
|
750
|
+
## Async Client
|
|
751
|
+
|
|
752
|
+
For async/concurrent workloads, use `AsyncSageClient`:
|
|
753
|
+
|
|
754
|
+
```python
|
|
755
|
+
import asyncio
|
|
756
|
+
from sage_sdk import AsyncSageClient, AgentIdentity
|
|
757
|
+
|
|
758
|
+
async def main():
|
|
759
|
+
identity = AgentIdentity.generate()
|
|
760
|
+
async with AsyncSageClient(base_url="http://localhost:8080", identity=identity) as client:
|
|
761
|
+
# Submit a memory
|
|
762
|
+
result = await client.propose(
|
|
763
|
+
content="Async observation",
|
|
764
|
+
memory_type="observation",
|
|
765
|
+
domain_tag="testing",
|
|
766
|
+
confidence=0.75,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Run concurrent queries
|
|
770
|
+
results = await asyncio.gather(
|
|
771
|
+
client.query(embedding=[0.1] * 768, domain_tag="security"),
|
|
772
|
+
client.query(embedding=[0.2] * 768, domain_tag="testing"),
|
|
773
|
+
client.query(embedding=[0.3] * 768, domain_tag="crypto"),
|
|
774
|
+
)
|
|
775
|
+
for r in results:
|
|
776
|
+
print(f"Found {r.total_count} memories")
|
|
777
|
+
|
|
778
|
+
asyncio.run(main())
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
The async client has the same methods as `SageClient`, all returning awaitables.
|
|
782
|
+
|
|
783
|
+
## Models
|
|
784
|
+
|
|
785
|
+
### MemoryType
|
|
786
|
+
|
|
787
|
+
```python
|
|
788
|
+
from sage_sdk.models import MemoryType
|
|
789
|
+
|
|
790
|
+
MemoryType.fact # Verified factual knowledge
|
|
791
|
+
MemoryType.observation # Agent-observed data
|
|
792
|
+
MemoryType.inference # Derived conclusion
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### MemoryStatus
|
|
796
|
+
|
|
797
|
+
```python
|
|
798
|
+
from sage_sdk.models import MemoryStatus
|
|
799
|
+
|
|
800
|
+
MemoryStatus.proposed # Awaiting validation
|
|
801
|
+
MemoryStatus.validated # Passed quorum vote
|
|
802
|
+
MemoryStatus.committed # Finalized on-chain
|
|
803
|
+
MemoryStatus.challenged # Under dispute
|
|
804
|
+
MemoryStatus.deprecated # Superseded or invalidated
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
### MemoryRecord
|
|
808
|
+
|
|
809
|
+
Returned by `get_memory()` and in query results:
|
|
810
|
+
|
|
811
|
+
| Field | Type | Description |
|
|
812
|
+
|-------|------|-------------|
|
|
813
|
+
| `memory_id` | `str` | Unique identifier |
|
|
814
|
+
| `submitting_agent` | `str` | Agent public key (hex) |
|
|
815
|
+
| `content` | `str` | Natural language content |
|
|
816
|
+
| `content_hash` | `str` | SHA-256 of content |
|
|
817
|
+
| `memory_type` | `MemoryType` | fact, observation, inference |
|
|
818
|
+
| `domain_tag` | `str` | Domain classification |
|
|
819
|
+
| `confidence_score` | `float` | 0.0 - 1.0 |
|
|
820
|
+
| `status` | `MemoryStatus` | Lifecycle state |
|
|
821
|
+
| `created_at` | `datetime` | Submission timestamp |
|
|
822
|
+
| `similarity_score` | `float \| None` | Populated in query results |
|
|
823
|
+
|
|
824
|
+
### KnowledgeTriple
|
|
825
|
+
|
|
826
|
+
Structured knowledge for the knowledge graph:
|
|
827
|
+
|
|
828
|
+
```python
|
|
829
|
+
from sage_sdk.models import KnowledgeTriple
|
|
830
|
+
|
|
831
|
+
triple = KnowledgeTriple(
|
|
832
|
+
subject="SQL injection",
|
|
833
|
+
predicate="mitigated_by",
|
|
834
|
+
object_="parameterized queries", # Note: object_ (Python keyword)
|
|
835
|
+
)
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
Serializes to `{"subject": "...", "predicate": "...", "object": "..."}` via the `object` alias.
|
|
839
|
+
|
|
840
|
+
### AgentProfile
|
|
841
|
+
|
|
842
|
+
Returned by `get_profile()`:
|
|
843
|
+
|
|
844
|
+
| Field | Type | Description |
|
|
845
|
+
|-------|------|-------------|
|
|
846
|
+
| `agent_id` | `str` | Agent public key (hex) |
|
|
847
|
+
| `poe_weight` | `float` | Current Proof of Experience weight |
|
|
848
|
+
| `vote_count` | `int` | Total votes cast |
|
|
849
|
+
|
|
850
|
+
## Error Handling
|
|
851
|
+
|
|
852
|
+
The SDK raises typed exceptions for API errors following RFC 7807 Problem Details:
|
|
853
|
+
|
|
854
|
+
```python
|
|
855
|
+
from sage_sdk.exceptions import (
|
|
856
|
+
SageError, # Base exception
|
|
857
|
+
SageAPIError, # Any API error (has status_code, detail)
|
|
858
|
+
SageAuthError, # 401/403 authentication failure
|
|
859
|
+
SageNotFoundError, # 404 resource not found
|
|
860
|
+
SageValidationError, # 422 validation error
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
memory = client.get_memory("nonexistent-id")
|
|
865
|
+
except SageNotFoundError as e:
|
|
866
|
+
print(f"Not found: {e.detail}")
|
|
867
|
+
except SageAuthError as e:
|
|
868
|
+
print(f"Auth failed: {e}")
|
|
869
|
+
except SageAPIError as e:
|
|
870
|
+
print(f"API error {e.status_code}: {e.detail}")
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
## Configuration
|
|
874
|
+
|
|
875
|
+
```python
|
|
876
|
+
from sage_sdk import SageClient, AgentIdentity
|
|
877
|
+
|
|
878
|
+
identity = AgentIdentity.from_file("agent.key")
|
|
879
|
+
|
|
880
|
+
client = SageClient(
|
|
881
|
+
base_url="http://localhost:8080", # SAGE node URL
|
|
882
|
+
identity=identity,
|
|
883
|
+
timeout=30.0, # Request timeout in seconds (default: 30)
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
# Use as context manager for automatic cleanup
|
|
887
|
+
with SageClient(base_url="http://localhost:8080", identity=identity) as client:
|
|
888
|
+
profile = client.get_profile()
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
## Embeddings
|
|
892
|
+
|
|
893
|
+
SAGE uses 768-dimensional vectors (Ollama `nomic-embed-text` model). You can generate embeddings in three ways:
|
|
894
|
+
|
|
895
|
+
### 1. Direct Ollama (recommended for local agents)
|
|
896
|
+
|
|
897
|
+
```python
|
|
898
|
+
import httpx
|
|
899
|
+
|
|
900
|
+
resp = httpx.post(
|
|
901
|
+
"http://localhost:11434/api/embed",
|
|
902
|
+
json={"model": "nomic-embed-text", "input": "your text here"},
|
|
903
|
+
timeout=30.0,
|
|
904
|
+
)
|
|
905
|
+
embedding = resp.json()["embeddings"][0] # 768-dim float list
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### 2. SAGE Embed Endpoint (for remote agents without local Ollama)
|
|
909
|
+
|
|
910
|
+
```python
|
|
911
|
+
# Uses the SAGE network's Ollama instance via authenticated REST endpoint
|
|
912
|
+
result = client.embed("your text here")
|
|
913
|
+
embedding = result["embedding"] # 768-dim float list
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
**Server endpoint:** `POST /v1/embed`
|
|
917
|
+
|
|
918
|
+
### 3. Hash Embedding (testing/fallback only)
|
|
919
|
+
|
|
920
|
+
Deterministic SHA-256 pseudo-embedding. Not semantic — only matches near-identical text. Useful for testing without Ollama.
|
|
921
|
+
|
|
922
|
+
```python
|
|
923
|
+
import hashlib, struct
|
|
924
|
+
|
|
925
|
+
def hash_embed(text: str, dim: int = 768) -> list[float]:
|
|
926
|
+
rounds = (dim * 4 + 31) // 32
|
|
927
|
+
raw = b""
|
|
928
|
+
current = text.encode("utf-8")
|
|
929
|
+
for i in range(rounds):
|
|
930
|
+
current = hashlib.sha256(current + struct.pack(">I", i)).digest()
|
|
931
|
+
raw += current
|
|
932
|
+
return [(struct.unpack(">I", raw[j*4:j*4+4])[0] / 2147483647.5) - 1.0 for j in range(dim)]
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
## Development
|
|
936
|
+
|
|
937
|
+
```bash
|
|
938
|
+
# Install with dev dependencies
|
|
939
|
+
pip install -e ".[dev]"
|
|
940
|
+
|
|
941
|
+
# Run tests
|
|
942
|
+
python -m pytest tests/ -v
|
|
943
|
+
|
|
944
|
+
# Run async tests
|
|
945
|
+
python -m pytest tests/test_async_client.py -v
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
## License
|
|
949
|
+
|
|
950
|
+
See the project root LICENSE file.
|