sockridge 0.1.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.
- sockridge-0.1.0/.gitignore +55 -0
- sockridge-0.1.0/PKG-INFO +246 -0
- sockridge-0.1.0/README.md +232 -0
- sockridge-0.1.0/auth.py +90 -0
- sockridge-0.1.0/pyproject.toml +21 -0
- sockridge-0.1.0/sockridge/__init__.py +6 -0
- sockridge-0.1.0/sockridge/auth.py +90 -0
- sockridge-0.1.0/sockridge/models.py +82 -0
- sockridge-0.1.0/sockridge/registry.py +260 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ── Go ────────────────────────────────────────────────────────────────────────
|
|
2
|
+
bin/
|
|
3
|
+
*.exe
|
|
4
|
+
*.test
|
|
5
|
+
*.out
|
|
6
|
+
vendor/
|
|
7
|
+
go.sum
|
|
8
|
+
|
|
9
|
+
# generated proto code — regenerate with buf generate
|
|
10
|
+
gen/
|
|
11
|
+
|
|
12
|
+
# ── Python SDK ────────────────────────────────────────────────────────────────
|
|
13
|
+
sdk/python/__pycache__/
|
|
14
|
+
sdk/python/*.egg-info/
|
|
15
|
+
sdk/python/dist/
|
|
16
|
+
sdk/python/build/
|
|
17
|
+
sdk/python/.venv/
|
|
18
|
+
sdk/python/sockridge/__pycache__/
|
|
19
|
+
**/__pycache__/
|
|
20
|
+
**/*.pyc
|
|
21
|
+
**/*.pyo
|
|
22
|
+
|
|
23
|
+
# ── TypeScript SDK ────────────────────────────────────────────────────────────
|
|
24
|
+
sdk/typescript/node_modules/
|
|
25
|
+
sdk/typescript/dist/
|
|
26
|
+
sdk/typescript/*.js
|
|
27
|
+
sdk/typescript/*.d.ts
|
|
28
|
+
sdk/typescript/*.js.map
|
|
29
|
+
|
|
30
|
+
# ── Docker ────────────────────────────────────────────────────────────────────
|
|
31
|
+
.env
|
|
32
|
+
.env.local
|
|
33
|
+
.env.production
|
|
34
|
+
|
|
35
|
+
# ── Credentials — NEVER commit these ─────────────────────────────────────────
|
|
36
|
+
.sockridge/
|
|
37
|
+
*.key
|
|
38
|
+
credentials.json
|
|
39
|
+
**/ed25519.key
|
|
40
|
+
**/credentials.json
|
|
41
|
+
|
|
42
|
+
# ── Test files ────────────────────────────────────────────────────────────────
|
|
43
|
+
test_agents/
|
|
44
|
+
/tmp/
|
|
45
|
+
*.log
|
|
46
|
+
|
|
47
|
+
# ── OS ────────────────────────────────────────────────────────────────────────
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
|
50
|
+
|
|
51
|
+
# ── IDE ───────────────────────────────────────────────────────────────────────
|
|
52
|
+
.vscode/
|
|
53
|
+
.idea/
|
|
54
|
+
*.swp
|
|
55
|
+
*.swo
|
sockridge-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sockridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the SockRidge agent registry
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sockridge/sockridge
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: cryptography>=42.0.0
|
|
9
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# sockridge — Python SDK
|
|
16
|
+
|
|
17
|
+
Python SDK for the [Sockridge](https://sockridge.com) agent registry.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install git+https://github.com/Sockridge/sockridge.git#subdirectory=sdk/python
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or for local development:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/Sockridge/sockridge.git
|
|
29
|
+
pip install -e sockridge/sdk/python
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Prerequisites
|
|
33
|
+
|
|
34
|
+
You need a publisher account before using the SDK. Set one up with the CLI:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# install CLI
|
|
38
|
+
go install github.com/Sockridge/sockridge/cli@latest
|
|
39
|
+
|
|
40
|
+
# register
|
|
41
|
+
sockridge auth keygen
|
|
42
|
+
sockridge auth register --handle yourhandle --server https://sockridge.com:9000
|
|
43
|
+
sockridge auth login --server https://sockridge.com:9000
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This creates `~/.sockridge/credentials.json` and `~/.sockridge/ed25519.key` which the SDK reads automatically.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Connect
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from sockridge import Registry
|
|
56
|
+
|
|
57
|
+
registry = Registry("https://sockridge.com:9000")
|
|
58
|
+
registry.login() # reads ~/.sockridge/credentials.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Custom credentials path:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
registry.login(
|
|
65
|
+
credentials_path="/custom/path/credentials.json",
|
|
66
|
+
key_path="/custom/path/ed25519.key",
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Publish an agent
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from sockridge import Registry, AgentCard, Skill, Capabilities
|
|
76
|
+
|
|
77
|
+
registry = Registry("https://sockridge.com:9000")
|
|
78
|
+
registry.login()
|
|
79
|
+
|
|
80
|
+
card = AgentCard(
|
|
81
|
+
name="My FHIR Agent",
|
|
82
|
+
description="Analyzes lab trends from FHIR data using ML models",
|
|
83
|
+
url="https://my-agent.example.com",
|
|
84
|
+
version="1.0.0",
|
|
85
|
+
skills=[
|
|
86
|
+
Skill(
|
|
87
|
+
id="fhir.lab.analyze",
|
|
88
|
+
name="Lab Analyzer",
|
|
89
|
+
description="Detects anomalies in lab result trends",
|
|
90
|
+
tags=["fhir", "labs", "analysis"],
|
|
91
|
+
)
|
|
92
|
+
],
|
|
93
|
+
capabilities=Capabilities(streaming=True, tool_use=True),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
published = registry.publish(card)
|
|
97
|
+
print(f"id: {published.id}")
|
|
98
|
+
print(f"status: {published.status}") # PENDING → ACTIVE after gatekeeper
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The agent goes through automatic validation after publish:
|
|
102
|
+
|
|
103
|
+
- Fields are checked (name, description, skills required)
|
|
104
|
+
- URL is pinged to verify the agent is running
|
|
105
|
+
- AI scores the card quality (0.0 - 1.0)
|
|
106
|
+
- Score >= 0.4 → `AGENT_STATUS_ACTIVE`
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### Self-register on startup
|
|
111
|
+
|
|
112
|
+
For agents that register themselves when they start:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
registry = Registry("https://sockridge.com:9000")
|
|
116
|
+
published = registry.register_and_publish(card)
|
|
117
|
+
print(f"registered: {published.id}")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Search for agents
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# by tag
|
|
126
|
+
agents = registry.search(tags=["fhir", "labs"])
|
|
127
|
+
for agent in agents:
|
|
128
|
+
print(f"{agent.name} — {agent.id}")
|
|
129
|
+
|
|
130
|
+
# by natural language
|
|
131
|
+
results = registry.semantic_search("find me a lab result analyzer")
|
|
132
|
+
for r in results:
|
|
133
|
+
print(f"{r['score']:.2f} {r['agent'].name}")
|
|
134
|
+
|
|
135
|
+
# by ID
|
|
136
|
+
agent = registry.get_agent("agent-uuid-here")
|
|
137
|
+
print(agent.name, agent.skills)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Access agreements
|
|
143
|
+
|
|
144
|
+
Agents can only get each other's endpoint URLs after a mutual access agreement is approved by both publishers.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# request access from another publisher
|
|
148
|
+
agreement = registry.request_access(
|
|
149
|
+
receiver_id="other-publisher-uuid",
|
|
150
|
+
message="want to connect our agents for a healthcare pipeline",
|
|
151
|
+
)
|
|
152
|
+
print(f"agreement id: {agreement['id']}")
|
|
153
|
+
print(f"status: {agreement['status']}") # AGREEMENT_STATUS_PENDING
|
|
154
|
+
|
|
155
|
+
# once they approve, get the shared key
|
|
156
|
+
# (they share it with you out of band, or you retrieve via get_agreement)
|
|
157
|
+
|
|
158
|
+
# resolve an agent's endpoint URL
|
|
159
|
+
result = registry.resolve_endpoint(
|
|
160
|
+
agent_id="agent-uuid",
|
|
161
|
+
shared_key="sk_abc123...",
|
|
162
|
+
)
|
|
163
|
+
print(f"url: {result['url']}")
|
|
164
|
+
print(f"transport: {result['transport']}")
|
|
165
|
+
print(f"skills: {[s.name for s in result['agent'].skills]}")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Full agreement flow
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# publisher A requests access to publisher B
|
|
174
|
+
agreement = registry_a.request_access(
|
|
175
|
+
receiver_id=publisher_b_id,
|
|
176
|
+
message="building a medical pipeline",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# publisher B approves (on their side)
|
|
180
|
+
shared_key = registry_b.approve_access(agreement["id"])
|
|
181
|
+
# shared_key = "sk_abc123..."
|
|
182
|
+
|
|
183
|
+
# both sides can now resolve each other's agents
|
|
184
|
+
endpoint_a = registry_b.resolve_endpoint(agent_a_id, shared_key)
|
|
185
|
+
endpoint_b = registry_a.resolve_endpoint(agent_b_id, shared_key)
|
|
186
|
+
|
|
187
|
+
# either side can revoke
|
|
188
|
+
registry_a.revoke_access(agreement["id"])
|
|
189
|
+
# key is instantly invalid
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## API reference
|
|
195
|
+
|
|
196
|
+
### `Registry(server_url)`
|
|
197
|
+
|
|
198
|
+
| Method | Description |
|
|
199
|
+
| -------------------------------------------- | ---------------------------------------------------------- |
|
|
200
|
+
| `login(credentials_path?, key_path?)` | Authenticate with Ed25519 challenge-response |
|
|
201
|
+
| `publish(card)` | Publish an AgentCard, returns card with server-assigned id |
|
|
202
|
+
| `register_and_publish(card, ...)` | Login + publish in one call |
|
|
203
|
+
| `search(tags?, limit?)` | List agents by tag. URL not included |
|
|
204
|
+
| `semantic_search(query, top_k?, min_score?)` | Natural language search |
|
|
205
|
+
| `get_agent(agent_id)` | Get a single agent by ID |
|
|
206
|
+
| `request_access(receiver_id, message?)` | Send mutual access request |
|
|
207
|
+
| `approve_access(agreement_id)` | Approve a pending request, returns shared key |
|
|
208
|
+
| `resolve_endpoint(agent_id, shared_key)` | Resolve agent URL using shared key |
|
|
209
|
+
|
|
210
|
+
### `AgentCard`
|
|
211
|
+
|
|
212
|
+
| Field | Type | Required |
|
|
213
|
+
| ------------------ | ------------ | ------------------- |
|
|
214
|
+
| `name` | str | ✓ |
|
|
215
|
+
| `description` | str | ✓ |
|
|
216
|
+
| `url` | str | ✓ |
|
|
217
|
+
| `version` | str | defaults to `0.1.0` |
|
|
218
|
+
| `protocol_version` | str | defaults to `0.3.0` |
|
|
219
|
+
| `skills` | list[Skill] | ✓ at least one |
|
|
220
|
+
| `capabilities` | Capabilities | optional |
|
|
221
|
+
|
|
222
|
+
### `Skill`
|
|
223
|
+
|
|
224
|
+
| Field | Type | Required |
|
|
225
|
+
| ------------- | --------- | --------------------------- |
|
|
226
|
+
| `id` | str | ✓ (e.g. `fhir.lab.analyze`) |
|
|
227
|
+
| `name` | str | ✓ |
|
|
228
|
+
| `description` | str | ✓ |
|
|
229
|
+
| `tags` | list[str] | optional |
|
|
230
|
+
|
|
231
|
+
### `Capabilities`
|
|
232
|
+
|
|
233
|
+
| Field | Type | Default |
|
|
234
|
+
| -------------------- | ---- | ------- |
|
|
235
|
+
| `streaming` | bool | False |
|
|
236
|
+
| `push_notifications` | bool | False |
|
|
237
|
+
| `multi_turn` | bool | False |
|
|
238
|
+
| `tool_use` | bool | False |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Requirements
|
|
243
|
+
|
|
244
|
+
- Python 3.11+
|
|
245
|
+
- `httpx[http2]` — HTTP/2 client (required, server speaks h2c)
|
|
246
|
+
- `cryptography` — Ed25519 signing
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# sockridge — Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Sockridge](https://sockridge.com) agent registry.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install git+https://github.com/Sockridge/sockridge.git#subdirectory=sdk/python
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or for local development:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/Sockridge/sockridge.git
|
|
15
|
+
pip install -e sockridge/sdk/python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
You need a publisher account before using the SDK. Set one up with the CLI:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# install CLI
|
|
24
|
+
go install github.com/Sockridge/sockridge/cli@latest
|
|
25
|
+
|
|
26
|
+
# register
|
|
27
|
+
sockridge auth keygen
|
|
28
|
+
sockridge auth register --handle yourhandle --server https://sockridge.com:9000
|
|
29
|
+
sockridge auth login --server https://sockridge.com:9000
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates `~/.sockridge/credentials.json` and `~/.sockridge/ed25519.key` which the SDK reads automatically.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### Connect
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from sockridge import Registry
|
|
42
|
+
|
|
43
|
+
registry = Registry("https://sockridge.com:9000")
|
|
44
|
+
registry.login() # reads ~/.sockridge/credentials.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Custom credentials path:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
registry.login(
|
|
51
|
+
credentials_path="/custom/path/credentials.json",
|
|
52
|
+
key_path="/custom/path/ed25519.key",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### Publish an agent
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from sockridge import Registry, AgentCard, Skill, Capabilities
|
|
62
|
+
|
|
63
|
+
registry = Registry("https://sockridge.com:9000")
|
|
64
|
+
registry.login()
|
|
65
|
+
|
|
66
|
+
card = AgentCard(
|
|
67
|
+
name="My FHIR Agent",
|
|
68
|
+
description="Analyzes lab trends from FHIR data using ML models",
|
|
69
|
+
url="https://my-agent.example.com",
|
|
70
|
+
version="1.0.0",
|
|
71
|
+
skills=[
|
|
72
|
+
Skill(
|
|
73
|
+
id="fhir.lab.analyze",
|
|
74
|
+
name="Lab Analyzer",
|
|
75
|
+
description="Detects anomalies in lab result trends",
|
|
76
|
+
tags=["fhir", "labs", "analysis"],
|
|
77
|
+
)
|
|
78
|
+
],
|
|
79
|
+
capabilities=Capabilities(streaming=True, tool_use=True),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
published = registry.publish(card)
|
|
83
|
+
print(f"id: {published.id}")
|
|
84
|
+
print(f"status: {published.status}") # PENDING → ACTIVE after gatekeeper
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The agent goes through automatic validation after publish:
|
|
88
|
+
|
|
89
|
+
- Fields are checked (name, description, skills required)
|
|
90
|
+
- URL is pinged to verify the agent is running
|
|
91
|
+
- AI scores the card quality (0.0 - 1.0)
|
|
92
|
+
- Score >= 0.4 → `AGENT_STATUS_ACTIVE`
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Self-register on startup
|
|
97
|
+
|
|
98
|
+
For agents that register themselves when they start:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
registry = Registry("https://sockridge.com:9000")
|
|
102
|
+
published = registry.register_and_publish(card)
|
|
103
|
+
print(f"registered: {published.id}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Search for agents
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# by tag
|
|
112
|
+
agents = registry.search(tags=["fhir", "labs"])
|
|
113
|
+
for agent in agents:
|
|
114
|
+
print(f"{agent.name} — {agent.id}")
|
|
115
|
+
|
|
116
|
+
# by natural language
|
|
117
|
+
results = registry.semantic_search("find me a lab result analyzer")
|
|
118
|
+
for r in results:
|
|
119
|
+
print(f"{r['score']:.2f} {r['agent'].name}")
|
|
120
|
+
|
|
121
|
+
# by ID
|
|
122
|
+
agent = registry.get_agent("agent-uuid-here")
|
|
123
|
+
print(agent.name, agent.skills)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Access agreements
|
|
129
|
+
|
|
130
|
+
Agents can only get each other's endpoint URLs after a mutual access agreement is approved by both publishers.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# request access from another publisher
|
|
134
|
+
agreement = registry.request_access(
|
|
135
|
+
receiver_id="other-publisher-uuid",
|
|
136
|
+
message="want to connect our agents for a healthcare pipeline",
|
|
137
|
+
)
|
|
138
|
+
print(f"agreement id: {agreement['id']}")
|
|
139
|
+
print(f"status: {agreement['status']}") # AGREEMENT_STATUS_PENDING
|
|
140
|
+
|
|
141
|
+
# once they approve, get the shared key
|
|
142
|
+
# (they share it with you out of band, or you retrieve via get_agreement)
|
|
143
|
+
|
|
144
|
+
# resolve an agent's endpoint URL
|
|
145
|
+
result = registry.resolve_endpoint(
|
|
146
|
+
agent_id="agent-uuid",
|
|
147
|
+
shared_key="sk_abc123...",
|
|
148
|
+
)
|
|
149
|
+
print(f"url: {result['url']}")
|
|
150
|
+
print(f"transport: {result['transport']}")
|
|
151
|
+
print(f"skills: {[s.name for s in result['agent'].skills]}")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### Full agreement flow
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# publisher A requests access to publisher B
|
|
160
|
+
agreement = registry_a.request_access(
|
|
161
|
+
receiver_id=publisher_b_id,
|
|
162
|
+
message="building a medical pipeline",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# publisher B approves (on their side)
|
|
166
|
+
shared_key = registry_b.approve_access(agreement["id"])
|
|
167
|
+
# shared_key = "sk_abc123..."
|
|
168
|
+
|
|
169
|
+
# both sides can now resolve each other's agents
|
|
170
|
+
endpoint_a = registry_b.resolve_endpoint(agent_a_id, shared_key)
|
|
171
|
+
endpoint_b = registry_a.resolve_endpoint(agent_b_id, shared_key)
|
|
172
|
+
|
|
173
|
+
# either side can revoke
|
|
174
|
+
registry_a.revoke_access(agreement["id"])
|
|
175
|
+
# key is instantly invalid
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## API reference
|
|
181
|
+
|
|
182
|
+
### `Registry(server_url)`
|
|
183
|
+
|
|
184
|
+
| Method | Description |
|
|
185
|
+
| -------------------------------------------- | ---------------------------------------------------------- |
|
|
186
|
+
| `login(credentials_path?, key_path?)` | Authenticate with Ed25519 challenge-response |
|
|
187
|
+
| `publish(card)` | Publish an AgentCard, returns card with server-assigned id |
|
|
188
|
+
| `register_and_publish(card, ...)` | Login + publish in one call |
|
|
189
|
+
| `search(tags?, limit?)` | List agents by tag. URL not included |
|
|
190
|
+
| `semantic_search(query, top_k?, min_score?)` | Natural language search |
|
|
191
|
+
| `get_agent(agent_id)` | Get a single agent by ID |
|
|
192
|
+
| `request_access(receiver_id, message?)` | Send mutual access request |
|
|
193
|
+
| `approve_access(agreement_id)` | Approve a pending request, returns shared key |
|
|
194
|
+
| `resolve_endpoint(agent_id, shared_key)` | Resolve agent URL using shared key |
|
|
195
|
+
|
|
196
|
+
### `AgentCard`
|
|
197
|
+
|
|
198
|
+
| Field | Type | Required |
|
|
199
|
+
| ------------------ | ------------ | ------------------- |
|
|
200
|
+
| `name` | str | ✓ |
|
|
201
|
+
| `description` | str | ✓ |
|
|
202
|
+
| `url` | str | ✓ |
|
|
203
|
+
| `version` | str | defaults to `0.1.0` |
|
|
204
|
+
| `protocol_version` | str | defaults to `0.3.0` |
|
|
205
|
+
| `skills` | list[Skill] | ✓ at least one |
|
|
206
|
+
| `capabilities` | Capabilities | optional |
|
|
207
|
+
|
|
208
|
+
### `Skill`
|
|
209
|
+
|
|
210
|
+
| Field | Type | Required |
|
|
211
|
+
| ------------- | --------- | --------------------------- |
|
|
212
|
+
| `id` | str | ✓ (e.g. `fhir.lab.analyze`) |
|
|
213
|
+
| `name` | str | ✓ |
|
|
214
|
+
| `description` | str | ✓ |
|
|
215
|
+
| `tags` | list[str] | optional |
|
|
216
|
+
|
|
217
|
+
### `Capabilities`
|
|
218
|
+
|
|
219
|
+
| Field | Type | Default |
|
|
220
|
+
| -------------------- | ---- | ------- |
|
|
221
|
+
| `streaming` | bool | False |
|
|
222
|
+
| `push_notifications` | bool | False |
|
|
223
|
+
| `multi_turn` | bool | False |
|
|
224
|
+
| `tool_use` | bool | False |
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Requirements
|
|
229
|
+
|
|
230
|
+
- Python 3.11+
|
|
231
|
+
- `httpx[http2]` — HTTP/2 client (required, server speaks h2c)
|
|
232
|
+
- `cryptography` — Ed25519 signing
|
sockridge-0.1.0/auth.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
6
|
+
Ed25519PrivateKey,
|
|
7
|
+
Ed25519PublicKey,
|
|
8
|
+
)
|
|
9
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
10
|
+
Encoding,
|
|
11
|
+
PublicFormat,
|
|
12
|
+
PrivateFormat,
|
|
13
|
+
NoEncryption,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KeyPair:
|
|
18
|
+
def __init__(self, private_key: Ed25519PrivateKey):
|
|
19
|
+
self._private_key = private_key
|
|
20
|
+
self._public_key = private_key.public_key()
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def generate(cls) -> "KeyPair":
|
|
24
|
+
"""Generate a new Ed25519 keypair."""
|
|
25
|
+
return cls(Ed25519PrivateKey.generate())
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def load(cls, path: str) -> "KeyPair":
|
|
29
|
+
"""Load a private key from a base64-encoded file."""
|
|
30
|
+
raw = Path(path).read_text().strip()
|
|
31
|
+
priv_bytes = base64.b64decode(raw)
|
|
32
|
+
private_key = Ed25519PrivateKey.from_private_bytes(priv_bytes[:32])
|
|
33
|
+
return cls(private_key)
|
|
34
|
+
|
|
35
|
+
def save(self, path: str) -> None:
|
|
36
|
+
"""Save the private key to a file (chmod 600)."""
|
|
37
|
+
priv_bytes = self._private_key.private_bytes(
|
|
38
|
+
Encoding.Raw, PrivateFormat.Raw, NoEncryption()
|
|
39
|
+
)
|
|
40
|
+
Path(path).write_text(base64.b64encode(priv_bytes).decode())
|
|
41
|
+
os.chmod(path, 0o600)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def public_key_b64(self) -> str:
|
|
45
|
+
"""Base64-encoded public key for registration."""
|
|
46
|
+
pub_bytes = self._public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
47
|
+
return base64.b64encode(pub_bytes).decode()
|
|
48
|
+
|
|
49
|
+
def sign(self, message: bytes) -> bytes:
|
|
50
|
+
"""Sign a message with the private key."""
|
|
51
|
+
return self._private_key.sign(message)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Credentials:
|
|
55
|
+
DEFAULT_PATH = Path.home() / ".sockridge" / "credentials.json"
|
|
56
|
+
DEFAULT_KEY = Path.home() / ".sockridge" / "ed25519.key"
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
publisher_id: str,
|
|
61
|
+
handle: str,
|
|
62
|
+
server_url: str,
|
|
63
|
+
session_token: str = "",
|
|
64
|
+
):
|
|
65
|
+
self.publisher_id = publisher_id
|
|
66
|
+
self.handle = handle
|
|
67
|
+
self.server_url = server_url
|
|
68
|
+
self.session_token = session_token
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def load(cls, path: str | None = None) -> "Credentials":
|
|
72
|
+
p = Path(path) if path else cls.DEFAULT_PATH
|
|
73
|
+
data = json.loads(p.read_text())
|
|
74
|
+
return cls(
|
|
75
|
+
publisher_id = data["publisher_id"],
|
|
76
|
+
handle = data["handle"],
|
|
77
|
+
server_url = data.get("server_url", "https://sockridge.com:9000"),
|
|
78
|
+
session_token = data.get("session_token", ""),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def save(self, path: str | None = None) -> None:
|
|
82
|
+
p = Path(path) if path else cls.DEFAULT_PATH
|
|
83
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
p.write_text(json.dumps({
|
|
85
|
+
"publisher_id": self.publisher_id,
|
|
86
|
+
"handle": self.handle,
|
|
87
|
+
"server_url": self.server_url,
|
|
88
|
+
"session_token": self.session_token,
|
|
89
|
+
}, indent=2))
|
|
90
|
+
os.chmod(p, 0o600)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sockridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the SockRidge agent registry"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx[http2]>=0.27.0",
|
|
14
|
+
"cryptography>=42.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = ["pytest", "pytest-asyncio"]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/Sockridge/sockridge"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
6
|
+
Ed25519PrivateKey,
|
|
7
|
+
Ed25519PublicKey,
|
|
8
|
+
)
|
|
9
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
10
|
+
Encoding,
|
|
11
|
+
PublicFormat,
|
|
12
|
+
PrivateFormat,
|
|
13
|
+
NoEncryption,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KeyPair:
|
|
18
|
+
def __init__(self, private_key: Ed25519PrivateKey):
|
|
19
|
+
self._private_key = private_key
|
|
20
|
+
self._public_key = private_key.public_key()
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def generate(cls) -> "KeyPair":
|
|
24
|
+
"""Generate a new Ed25519 keypair."""
|
|
25
|
+
return cls(Ed25519PrivateKey.generate())
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def load(cls, path: str) -> "KeyPair":
|
|
29
|
+
"""Load a private key from a base64-encoded file."""
|
|
30
|
+
raw = Path(path).read_text().strip()
|
|
31
|
+
priv_bytes = base64.b64decode(raw)
|
|
32
|
+
private_key = Ed25519PrivateKey.from_private_bytes(priv_bytes[:32])
|
|
33
|
+
return cls(private_key)
|
|
34
|
+
|
|
35
|
+
def save(self, path: str) -> None:
|
|
36
|
+
"""Save the private key to a file (chmod 600)."""
|
|
37
|
+
priv_bytes = self._private_key.private_bytes(
|
|
38
|
+
Encoding.Raw, PrivateFormat.Raw, NoEncryption()
|
|
39
|
+
)
|
|
40
|
+
Path(path).write_text(base64.b64encode(priv_bytes).decode())
|
|
41
|
+
os.chmod(path, 0o600)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def public_key_b64(self) -> str:
|
|
45
|
+
"""Base64-encoded public key for registration."""
|
|
46
|
+
pub_bytes = self._public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
|
|
47
|
+
return base64.b64encode(pub_bytes).decode()
|
|
48
|
+
|
|
49
|
+
def sign(self, message: bytes) -> bytes:
|
|
50
|
+
"""Sign a message with the private key."""
|
|
51
|
+
return self._private_key.sign(message)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Credentials:
|
|
55
|
+
DEFAULT_PATH = Path.home() / ".sockridge" / "credentials.json"
|
|
56
|
+
DEFAULT_KEY = Path.home() / ".sockridge" / "ed25519.key"
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
publisher_id: str,
|
|
61
|
+
handle: str,
|
|
62
|
+
server_url: str,
|
|
63
|
+
session_token: str = "",
|
|
64
|
+
):
|
|
65
|
+
self.publisher_id = publisher_id
|
|
66
|
+
self.handle = handle
|
|
67
|
+
self.server_url = server_url
|
|
68
|
+
self.session_token = session_token
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def load(cls, path: str | None = None) -> "Credentials":
|
|
72
|
+
p = Path(path) if path else cls.DEFAULT_PATH
|
|
73
|
+
data = json.loads(p.read_text())
|
|
74
|
+
return cls(
|
|
75
|
+
publisher_id = data["publisher_id"],
|
|
76
|
+
handle = data["handle"],
|
|
77
|
+
server_url = data.get("server_url", "https://sockridge.com:9000"),
|
|
78
|
+
session_token = data.get("session_token", ""),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def save(self, path: str | None = None) -> None:
|
|
82
|
+
p = Path(path) if path else cls.DEFAULT_PATH
|
|
83
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
p.write_text(json.dumps({
|
|
85
|
+
"publisher_id": self.publisher_id,
|
|
86
|
+
"handle": self.handle,
|
|
87
|
+
"server_url": self.server_url,
|
|
88
|
+
"session_token": self.session_token,
|
|
89
|
+
}, indent=2))
|
|
90
|
+
os.chmod(p, 0o600)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentStatus(str, Enum):
|
|
7
|
+
PENDING = "AGENT_STATUS_PENDING"
|
|
8
|
+
ACTIVE = "AGENT_STATUS_ACTIVE"
|
|
9
|
+
REJECTED = "AGENT_STATUS_REJECTED"
|
|
10
|
+
INACTIVE = "AGENT_STATUS_INACTIVE"
|
|
11
|
+
DEPRECATED = "AGENT_STATUS_DEPRECATED"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Skill:
|
|
16
|
+
id: str
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
tags: list[str] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"id": self.id,
|
|
24
|
+
"name": self.name,
|
|
25
|
+
"description": self.description,
|
|
26
|
+
"tags": self.tags,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Capabilities:
|
|
32
|
+
streaming: bool = False
|
|
33
|
+
push_notifications: bool = False
|
|
34
|
+
multi_turn: bool = False
|
|
35
|
+
tool_use: bool = False
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict:
|
|
38
|
+
return {
|
|
39
|
+
"streaming": self.streaming,
|
|
40
|
+
"pushNotifications": self.push_notifications,
|
|
41
|
+
"multiTurn": self.multi_turn,
|
|
42
|
+
"toolUse": self.tool_use,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GatekeeperResult:
|
|
48
|
+
approved: bool
|
|
49
|
+
confidence_score: float
|
|
50
|
+
reason: str
|
|
51
|
+
reachable: bool
|
|
52
|
+
ping_latency_ms: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class AgentCard:
|
|
57
|
+
name: str
|
|
58
|
+
description: str
|
|
59
|
+
url: str
|
|
60
|
+
version: str = "0.1.0"
|
|
61
|
+
protocol_version: str = "0.3.0"
|
|
62
|
+
skills: list[Skill] = field(default_factory=list)
|
|
63
|
+
capabilities: Optional[Capabilities] = None
|
|
64
|
+
|
|
65
|
+
# set by registry after publish — do not set manually
|
|
66
|
+
id: Optional[str] = None
|
|
67
|
+
publisher_id: Optional[str] = None
|
|
68
|
+
status: Optional[AgentStatus] = None
|
|
69
|
+
gatekeeper_result: Optional[GatekeeperResult] = None
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict:
|
|
72
|
+
d = {
|
|
73
|
+
"name": self.name,
|
|
74
|
+
"description": self.description,
|
|
75
|
+
"url": self.url,
|
|
76
|
+
"version": self.version,
|
|
77
|
+
"protocolVersion": self.protocol_version,
|
|
78
|
+
"skills": [s.to_dict() for s in self.skills],
|
|
79
|
+
}
|
|
80
|
+
if self.capabilities:
|
|
81
|
+
d["capabilities"] = self.capabilities.to_dict()
|
|
82
|
+
return d
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import httpx
|
|
5
|
+
import struct
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from .models import AgentCard, AgentStatus, GatekeeperResult, Skill, Capabilities
|
|
9
|
+
from .auth import KeyPair, Credentials
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegistryError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Registry:
|
|
17
|
+
"""
|
|
18
|
+
SockRidge registry client.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from sockridge import Registry, AgentCard, Skill, Capabilities
|
|
22
|
+
|
|
23
|
+
registry = Registry("https://sockridge.com:9000")
|
|
24
|
+
registry.login(credentials_path="~/.sockridge/credentials.json")
|
|
25
|
+
|
|
26
|
+
card = AgentCard(
|
|
27
|
+
name="My Agent",
|
|
28
|
+
description="Does something useful",
|
|
29
|
+
url="https://my-agent.example.com",
|
|
30
|
+
skills=[Skill(id="do.thing", name="Do Thing", description="Does the thing", tags=["thing"])],
|
|
31
|
+
capabilities=Capabilities(streaming=True),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
published = registry.publish(card)
|
|
35
|
+
print(published.id)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, server_url: str = "https://sockridge.com:9000"):
|
|
39
|
+
self.server_url = server_url.rstrip("/")
|
|
40
|
+
self._token = ""
|
|
41
|
+
self._keypair: Optional[KeyPair] = None
|
|
42
|
+
self._publisher_id = ""
|
|
43
|
+
self._client = httpx.Client(http2=True, timeout=15.0)
|
|
44
|
+
|
|
45
|
+
# ── Auth ──────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
def login(
|
|
48
|
+
self,
|
|
49
|
+
credentials_path: str | None = None,
|
|
50
|
+
key_path: str | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Load credentials and perform challenge-response auth."""
|
|
53
|
+
creds = Credentials.load(credentials_path)
|
|
54
|
+
kp = KeyPair.load(key_path or str(Credentials.DEFAULT_KEY))
|
|
55
|
+
|
|
56
|
+
self._keypair = kp
|
|
57
|
+
self._publisher_id = creds.publisher_id
|
|
58
|
+
|
|
59
|
+
# challenge → sign → token
|
|
60
|
+
challenge = self._post("/agentregistry.v1.RegistryService/AuthChallenge", {
|
|
61
|
+
"publisherId": creds.publisher_id,
|
|
62
|
+
})
|
|
63
|
+
nonce = challenge["nonce"]
|
|
64
|
+
sig = kp.sign(nonce.encode())
|
|
65
|
+
|
|
66
|
+
verify = self._post("/agentregistry.v1.RegistryService/AuthVerify", {
|
|
67
|
+
"publisherId": creds.publisher_id,
|
|
68
|
+
"nonce": nonce,
|
|
69
|
+
"signature": base64.b64encode(sig).decode(),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
self._token = verify["sessionToken"]
|
|
73
|
+
|
|
74
|
+
# ── Publish ───────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def publish(self, card: AgentCard) -> AgentCard:
|
|
77
|
+
"""
|
|
78
|
+
Publish an agent to the registry.
|
|
79
|
+
Signs the payload with Ed25519 before sending.
|
|
80
|
+
Returns the AgentCard with server-assigned id and status.
|
|
81
|
+
"""
|
|
82
|
+
if not self._keypair:
|
|
83
|
+
raise RegistryError("not authenticated — call registry.login() first")
|
|
84
|
+
|
|
85
|
+
# use compact JSON with sorted keys for deterministic signing
|
|
86
|
+
payload_json = json.dumps(card.to_dict(), separators=(",", ":"), sort_keys=True).encode()
|
|
87
|
+
signature = self._keypair.sign(payload_json)
|
|
88
|
+
|
|
89
|
+
resp = self._post_auth("/agentregistry.v1.RegistryService/PublishAgent", {
|
|
90
|
+
"payload": {
|
|
91
|
+
"payload": base64.b64encode(payload_json).decode(),
|
|
92
|
+
"signature": base64.b64encode(signature).decode(),
|
|
93
|
+
"keyId": self._publisher_id,
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return self._parse_agent_card(resp.get("agent", {}))
|
|
98
|
+
|
|
99
|
+
# ── Discovery ─────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def search(self, tags: list[str] = [], limit: int = 20) -> list[AgentCard]:
|
|
102
|
+
"""List agents by tags. URL is not included (use resolve for that)."""
|
|
103
|
+
resp = self._post("/agentregistry.v1.DiscoveryService/ListAgents", {
|
|
104
|
+
"tags": tags,
|
|
105
|
+
"limit": limit,
|
|
106
|
+
})
|
|
107
|
+
return [self._parse_agent_card(a) for a in resp if a]
|
|
108
|
+
|
|
109
|
+
def semantic_search(self, query: str, top_k: int = 10, min_score: float = 0.1) -> list[dict]:
|
|
110
|
+
"""Find agents by natural language description."""
|
|
111
|
+
resp = self._post("/agentregistry.v1.DiscoveryService/SemanticSearch", {
|
|
112
|
+
"query": query,
|
|
113
|
+
"topK": top_k,
|
|
114
|
+
"minScore": min_score,
|
|
115
|
+
})
|
|
116
|
+
return [{"agent": self._parse_agent_card(r.get("agent", {})), "score": r.get("score", 0)} for r in resp]
|
|
117
|
+
|
|
118
|
+
def get_agent(self, agent_id: str) -> AgentCard:
|
|
119
|
+
"""Get a single agent by ID."""
|
|
120
|
+
resp = self._post("/agentregistry.v1.DiscoveryService/GetAgent", {
|
|
121
|
+
"agentId": agent_id,
|
|
122
|
+
})
|
|
123
|
+
return self._parse_agent_card(resp.get("agent", {}))
|
|
124
|
+
|
|
125
|
+
# ── Access Agreements ─────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def request_access(self, receiver_id: str, message: str = "") -> dict:
|
|
128
|
+
"""Request mutual access with another publisher."""
|
|
129
|
+
resp = self._post_auth("/agentregistry.v1.AccessAgreementService/RequestAccess", {
|
|
130
|
+
"requesterId": self._publisher_id,
|
|
131
|
+
"receiverId": receiver_id,
|
|
132
|
+
"message": message,
|
|
133
|
+
})
|
|
134
|
+
return resp.get("agreement", {})
|
|
135
|
+
|
|
136
|
+
def approve_access(self, agreement_id: str) -> str:
|
|
137
|
+
"""Approve a pending access request. Returns the shared key."""
|
|
138
|
+
resp = self._post_auth("/agentregistry.v1.AccessAgreementService/ApproveAccess", {
|
|
139
|
+
"publisherId": self._publisher_id,
|
|
140
|
+
"agreementId": agreement_id,
|
|
141
|
+
})
|
|
142
|
+
return resp.get("sharedKey", "")
|
|
143
|
+
|
|
144
|
+
def resolve_endpoint(self, agent_id: str, shared_key: str) -> dict:
|
|
145
|
+
"""
|
|
146
|
+
Resolve an agent's endpoint URL using a shared key.
|
|
147
|
+
Returns { url, transport, agent: AgentCard }
|
|
148
|
+
"""
|
|
149
|
+
resp = self._post("/agentregistry.v1.AccessAgreementService/ResolveEndpoint", {
|
|
150
|
+
"agentId": agent_id,
|
|
151
|
+
"sharedKey": shared_key,
|
|
152
|
+
})
|
|
153
|
+
return {
|
|
154
|
+
"url": resp.get("url", ""),
|
|
155
|
+
"transport": resp.get("transport", "http"),
|
|
156
|
+
"agent": self._parse_agent_card(resp.get("agent", {})),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ── Self-register helper ──────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def register_and_publish(
|
|
162
|
+
self,
|
|
163
|
+
card: AgentCard,
|
|
164
|
+
credentials_path: str | None = None,
|
|
165
|
+
key_path: str | None = None,
|
|
166
|
+
) -> AgentCard:
|
|
167
|
+
"""
|
|
168
|
+
Convenience method: login then publish in one call.
|
|
169
|
+
Ideal for agent startup scripts.
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
registry = Registry("https://sockridge.com:9000")
|
|
173
|
+
published = registry.register_and_publish(my_card)
|
|
174
|
+
"""
|
|
175
|
+
self.login(credentials_path, key_path)
|
|
176
|
+
return self.publish(card)
|
|
177
|
+
|
|
178
|
+
# ── HTTP helpers ──────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
def _post(self, path: str, body: dict) -> dict | list:
|
|
181
|
+
url = self.server_url + path
|
|
182
|
+
resp = self._client.post(
|
|
183
|
+
url,
|
|
184
|
+
json=body,
|
|
185
|
+
headers={"Content-Type": "application/json"},
|
|
186
|
+
)
|
|
187
|
+
self._raise_for_status(resp)
|
|
188
|
+
data = resp.json()
|
|
189
|
+
# connect-rpc returns arrays for server-streaming endpoints
|
|
190
|
+
if isinstance(data, list):
|
|
191
|
+
return data
|
|
192
|
+
return data
|
|
193
|
+
|
|
194
|
+
def _post_auth(self, path: str, body: dict) -> dict:
|
|
195
|
+
if not self._token:
|
|
196
|
+
raise RegistryError("not authenticated — call registry.login() first")
|
|
197
|
+
url = self.server_url + path
|
|
198
|
+
resp = self._client.post(
|
|
199
|
+
url,
|
|
200
|
+
json=body,
|
|
201
|
+
headers={
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
"Authorization": f"Bearer {self._token}",
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
self._raise_for_status(resp)
|
|
207
|
+
return resp.json()
|
|
208
|
+
|
|
209
|
+
def _raise_for_status(self, resp: httpx.Response) -> None:
|
|
210
|
+
if resp.status_code >= 400:
|
|
211
|
+
try:
|
|
212
|
+
detail = resp.json().get("message", resp.text)
|
|
213
|
+
except Exception:
|
|
214
|
+
detail = resp.text
|
|
215
|
+
raise RegistryError(f"registry error {resp.status_code}: {detail}")
|
|
216
|
+
|
|
217
|
+
def _parse_agent_card(self, data: dict) -> AgentCard:
|
|
218
|
+
if not data:
|
|
219
|
+
return AgentCard(name="", description="", url="")
|
|
220
|
+
|
|
221
|
+
skills = [
|
|
222
|
+
Skill(
|
|
223
|
+
id=s.get("id", ""),
|
|
224
|
+
name=s.get("name", ""),
|
|
225
|
+
description=s.get("description", ""),
|
|
226
|
+
tags=s.get("tags", []),
|
|
227
|
+
)
|
|
228
|
+
for s in data.get("skills", [])
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
caps_data = data.get("capabilities", {})
|
|
232
|
+
caps = Capabilities(
|
|
233
|
+
streaming=caps_data.get("streaming", False),
|
|
234
|
+
push_notifications=caps_data.get("pushNotifications", False),
|
|
235
|
+
multi_turn=caps_data.get("multiTurn", False),
|
|
236
|
+
tool_use=caps_data.get("toolUse", False),
|
|
237
|
+
) if caps_data else None
|
|
238
|
+
|
|
239
|
+
gk_data = data.get("gatekeeperResult")
|
|
240
|
+
gk = GatekeeperResult(
|
|
241
|
+
approved=gk_data.get("approved", False),
|
|
242
|
+
confidence_score=gk_data.get("confidenceScore", 0),
|
|
243
|
+
reason=gk_data.get("reason", ""),
|
|
244
|
+
reachable=gk_data.get("reachable", False),
|
|
245
|
+
ping_latency_ms=gk_data.get("pingLatencyMs", 0),
|
|
246
|
+
) if gk_data else None
|
|
247
|
+
|
|
248
|
+
return AgentCard(
|
|
249
|
+
id=data.get("id"),
|
|
250
|
+
name=data.get("name", ""),
|
|
251
|
+
description=data.get("description", ""),
|
|
252
|
+
url=data.get("url", ""),
|
|
253
|
+
version=data.get("version", "0.1.0"),
|
|
254
|
+
protocol_version=data.get("protocolVersion", "0.3.0"),
|
|
255
|
+
skills=skills,
|
|
256
|
+
capabilities=caps,
|
|
257
|
+
publisher_id=data.get("publisherId"),
|
|
258
|
+
status=AgentStatus(data["status"]) if data.get("status") else None,
|
|
259
|
+
gatekeeper_result=gk,
|
|
260
|
+
)
|