aid-discovery 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.
- aid_discovery-1.0.0/PKG-INFO +167 -0
- aid_discovery-1.0.0/README.md +152 -0
- aid_discovery-1.0.0/aid_discovery.egg-info/PKG-INFO +167 -0
- aid_discovery-1.0.0/aid_discovery.egg-info/SOURCES.txt +14 -0
- aid_discovery-1.0.0/aid_discovery.egg-info/dependency_links.txt +1 -0
- aid_discovery-1.0.0/aid_discovery.egg-info/requires.txt +5 -0
- aid_discovery-1.0.0/aid_discovery.egg-info/top_level.txt +1 -0
- aid_discovery-1.0.0/aid_py/__init__.py +29 -0
- aid_discovery-1.0.0/aid_py/constants.py +93 -0
- aid_discovery-1.0.0/aid_py/discover.py +93 -0
- aid_discovery-1.0.0/aid_py/parser.py +187 -0
- aid_discovery-1.0.0/pyproject.toml +20 -0
- aid_discovery-1.0.0/setup.cfg +4 -0
- aid_discovery-1.0.0/tests/test_discover.py +87 -0
- aid_discovery-1.0.0/tests/test_parity.py +23 -0
- aid_discovery-1.0.0/tests/test_parser.py +63 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aid-discovery
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for Agent Interface Discovery (AID) - v1.0.0 release
|
|
5
|
+
Author-email: Agent Community <maintainers@agentcommunity.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/agent-community/agent-interface-discovery
|
|
8
|
+
Project-URL: Repository, https://github.com/agent-community/agent-interface-discovery
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: dnspython>=2.6.0
|
|
12
|
+
Requires-Dist: idna>=3.6
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# aid-discovery (Python)
|
|
17
|
+
|
|
18
|
+
> Official Python implementation of the [Agent Interface Discovery (AID)](https://github.com/agentcommunity/agent-interface-discovery) specification.
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/aid-discovery/)
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
|
|
23
|
+
AID enables you to discover AI agents by domain name using DNS TXT records. Type a domain, get the agent's endpoint and protocol - that's it.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install aid-discovery
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from aid_py import discover, AidError
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# Discover an agent by domain
|
|
38
|
+
result = discover("supabase.agentcommunity.org")
|
|
39
|
+
|
|
40
|
+
print(f"Protocol: {result.record.proto}") # "mcp"
|
|
41
|
+
print(f"URI: {result.record.uri}") # "https://api.supabase.com/mcp"
|
|
42
|
+
print(f"Description: {result.record.desc}") # "Supabase MCP"
|
|
43
|
+
print(f"TTL: {result.ttl} seconds")
|
|
44
|
+
|
|
45
|
+
except AidError as e:
|
|
46
|
+
print(f"Discovery failed: {e}")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API Reference
|
|
50
|
+
|
|
51
|
+
### `discover(domain: str) -> DiscoveryResult`
|
|
52
|
+
|
|
53
|
+
Discovers an agent by looking up the `_agent` TXT record for the given domain.
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
|
|
57
|
+
- `domain` (str): The domain name to discover
|
|
58
|
+
|
|
59
|
+
**Returns:**
|
|
60
|
+
|
|
61
|
+
- `DiscoveryResult`: Object containing the parsed record and TTL
|
|
62
|
+
|
|
63
|
+
**Raises:**
|
|
64
|
+
|
|
65
|
+
- `AidError`: If discovery fails for any reason
|
|
66
|
+
|
|
67
|
+
### `parse(txt: str) -> AidRecord`
|
|
68
|
+
|
|
69
|
+
Parses and validates a raw TXT record string.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
|
|
73
|
+
- `txt` (str): Raw TXT record content (e.g., "v=aid1;uri=https://...")
|
|
74
|
+
|
|
75
|
+
**Returns:**
|
|
76
|
+
|
|
77
|
+
- `AidRecord`: Parsed and validated record
|
|
78
|
+
|
|
79
|
+
**Raises:**
|
|
80
|
+
|
|
81
|
+
- `AidError`: If parsing or validation fails
|
|
82
|
+
|
|
83
|
+
## Data Types
|
|
84
|
+
|
|
85
|
+
### `AidRecord`
|
|
86
|
+
|
|
87
|
+
Represents a parsed AID record with the following attributes:
|
|
88
|
+
|
|
89
|
+
- `v` (str): Protocol version (always "aid1")
|
|
90
|
+
- `uri` (str): Agent endpoint URI
|
|
91
|
+
- `proto` (str): Protocol identifier (e.g., "mcp", "openapi")
|
|
92
|
+
- `auth` (str, optional): Authentication method
|
|
93
|
+
- `desc` (str, optional): Human-readable description
|
|
94
|
+
|
|
95
|
+
### `DiscoveryResult`
|
|
96
|
+
|
|
97
|
+
Contains discovery results:
|
|
98
|
+
|
|
99
|
+
- `record` (AidRecord): The parsed AID record
|
|
100
|
+
- `ttl` (int): DNS TTL in seconds
|
|
101
|
+
|
|
102
|
+
### `AidError`
|
|
103
|
+
|
|
104
|
+
Exception raised when discovery or parsing fails:
|
|
105
|
+
|
|
106
|
+
- `code` (int): Numeric error code
|
|
107
|
+
- `message` (str): Human-readable error message
|
|
108
|
+
|
|
109
|
+
## Error Codes
|
|
110
|
+
|
|
111
|
+
| Code | Symbol | Description |
|
|
112
|
+
| ---- | ----------------------- | ---------------------------- |
|
|
113
|
+
| 1000 | `ERR_NO_RECORD` | No `_agent` TXT record found |
|
|
114
|
+
| 1001 | `ERR_INVALID_TXT` | Record found but malformed |
|
|
115
|
+
| 1002 | `ERR_UNSUPPORTED_PROTO` | Protocol not supported |
|
|
116
|
+
| 1003 | `ERR_SECURITY` | Security policy violation |
|
|
117
|
+
| 1004 | `ERR_DNS_LOOKUP_FAILED` | DNS query failed |
|
|
118
|
+
|
|
119
|
+
## Advanced Usage
|
|
120
|
+
|
|
121
|
+
### Custom Error Handling
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from aid_py import discover, AidError
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
result = discover("example.com")
|
|
128
|
+
# Use result.record...
|
|
129
|
+
except AidError as e:
|
|
130
|
+
if e.code == 1000: # ERR_NO_RECORD
|
|
131
|
+
print("No agent found for this domain")
|
|
132
|
+
elif e.code == 1001: # ERR_INVALID_TXT
|
|
133
|
+
print("Found a record but it's malformed")
|
|
134
|
+
else:
|
|
135
|
+
print(f"Other error: {e}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Parsing Raw Records
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from aid_py import parse, AidError
|
|
142
|
+
|
|
143
|
+
txt_record = "v=aid1;uri=https://api.example.com/agent;proto=mcp;desc=Example Agent"
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
record = parse(txt_record)
|
|
147
|
+
print(f"Parsed: {record.proto} agent at {record.uri}")
|
|
148
|
+
except AidError as e:
|
|
149
|
+
print(f"Invalid record: {e}")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
This package is part of the [AID monorepo](https://github.com/agentcommunity/agent-interface-discovery). To run tests:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# From the monorepo root
|
|
158
|
+
pnpm test
|
|
159
|
+
|
|
160
|
+
# Or run Python tests directly
|
|
161
|
+
cd packages/aid-py
|
|
162
|
+
python -m pytest tests/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT - see [LICENSE](https://github.com/agentcommunity/agent-interface-discovery/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# aid-discovery (Python)
|
|
2
|
+
|
|
3
|
+
> Official Python implementation of the [Agent Interface Discovery (AID)](https://github.com/agentcommunity/agent-interface-discovery) specification.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/aid-discovery/)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
|
|
8
|
+
AID enables you to discover AI agents by domain name using DNS TXT records. Type a domain, get the agent's endpoint and protocol - that's it.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install aid-discovery
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from aid_py import discover, AidError
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Discover an agent by domain
|
|
23
|
+
result = discover("supabase.agentcommunity.org")
|
|
24
|
+
|
|
25
|
+
print(f"Protocol: {result.record.proto}") # "mcp"
|
|
26
|
+
print(f"URI: {result.record.uri}") # "https://api.supabase.com/mcp"
|
|
27
|
+
print(f"Description: {result.record.desc}") # "Supabase MCP"
|
|
28
|
+
print(f"TTL: {result.ttl} seconds")
|
|
29
|
+
|
|
30
|
+
except AidError as e:
|
|
31
|
+
print(f"Discovery failed: {e}")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API Reference
|
|
35
|
+
|
|
36
|
+
### `discover(domain: str) -> DiscoveryResult`
|
|
37
|
+
|
|
38
|
+
Discovers an agent by looking up the `_agent` TXT record for the given domain.
|
|
39
|
+
|
|
40
|
+
**Parameters:**
|
|
41
|
+
|
|
42
|
+
- `domain` (str): The domain name to discover
|
|
43
|
+
|
|
44
|
+
**Returns:**
|
|
45
|
+
|
|
46
|
+
- `DiscoveryResult`: Object containing the parsed record and TTL
|
|
47
|
+
|
|
48
|
+
**Raises:**
|
|
49
|
+
|
|
50
|
+
- `AidError`: If discovery fails for any reason
|
|
51
|
+
|
|
52
|
+
### `parse(txt: str) -> AidRecord`
|
|
53
|
+
|
|
54
|
+
Parses and validates a raw TXT record string.
|
|
55
|
+
|
|
56
|
+
**Parameters:**
|
|
57
|
+
|
|
58
|
+
- `txt` (str): Raw TXT record content (e.g., "v=aid1;uri=https://...")
|
|
59
|
+
|
|
60
|
+
**Returns:**
|
|
61
|
+
|
|
62
|
+
- `AidRecord`: Parsed and validated record
|
|
63
|
+
|
|
64
|
+
**Raises:**
|
|
65
|
+
|
|
66
|
+
- `AidError`: If parsing or validation fails
|
|
67
|
+
|
|
68
|
+
## Data Types
|
|
69
|
+
|
|
70
|
+
### `AidRecord`
|
|
71
|
+
|
|
72
|
+
Represents a parsed AID record with the following attributes:
|
|
73
|
+
|
|
74
|
+
- `v` (str): Protocol version (always "aid1")
|
|
75
|
+
- `uri` (str): Agent endpoint URI
|
|
76
|
+
- `proto` (str): Protocol identifier (e.g., "mcp", "openapi")
|
|
77
|
+
- `auth` (str, optional): Authentication method
|
|
78
|
+
- `desc` (str, optional): Human-readable description
|
|
79
|
+
|
|
80
|
+
### `DiscoveryResult`
|
|
81
|
+
|
|
82
|
+
Contains discovery results:
|
|
83
|
+
|
|
84
|
+
- `record` (AidRecord): The parsed AID record
|
|
85
|
+
- `ttl` (int): DNS TTL in seconds
|
|
86
|
+
|
|
87
|
+
### `AidError`
|
|
88
|
+
|
|
89
|
+
Exception raised when discovery or parsing fails:
|
|
90
|
+
|
|
91
|
+
- `code` (int): Numeric error code
|
|
92
|
+
- `message` (str): Human-readable error message
|
|
93
|
+
|
|
94
|
+
## Error Codes
|
|
95
|
+
|
|
96
|
+
| Code | Symbol | Description |
|
|
97
|
+
| ---- | ----------------------- | ---------------------------- |
|
|
98
|
+
| 1000 | `ERR_NO_RECORD` | No `_agent` TXT record found |
|
|
99
|
+
| 1001 | `ERR_INVALID_TXT` | Record found but malformed |
|
|
100
|
+
| 1002 | `ERR_UNSUPPORTED_PROTO` | Protocol not supported |
|
|
101
|
+
| 1003 | `ERR_SECURITY` | Security policy violation |
|
|
102
|
+
| 1004 | `ERR_DNS_LOOKUP_FAILED` | DNS query failed |
|
|
103
|
+
|
|
104
|
+
## Advanced Usage
|
|
105
|
+
|
|
106
|
+
### Custom Error Handling
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from aid_py import discover, AidError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
result = discover("example.com")
|
|
113
|
+
# Use result.record...
|
|
114
|
+
except AidError as e:
|
|
115
|
+
if e.code == 1000: # ERR_NO_RECORD
|
|
116
|
+
print("No agent found for this domain")
|
|
117
|
+
elif e.code == 1001: # ERR_INVALID_TXT
|
|
118
|
+
print("Found a record but it's malformed")
|
|
119
|
+
else:
|
|
120
|
+
print(f"Other error: {e}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Parsing Raw Records
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from aid_py import parse, AidError
|
|
127
|
+
|
|
128
|
+
txt_record = "v=aid1;uri=https://api.example.com/agent;proto=mcp;desc=Example Agent"
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
record = parse(txt_record)
|
|
132
|
+
print(f"Parsed: {record.proto} agent at {record.uri}")
|
|
133
|
+
except AidError as e:
|
|
134
|
+
print(f"Invalid record: {e}")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
This package is part of the [AID monorepo](https://github.com/agentcommunity/agent-interface-discovery). To run tests:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# From the monorepo root
|
|
143
|
+
pnpm test
|
|
144
|
+
|
|
145
|
+
# Or run Python tests directly
|
|
146
|
+
cd packages/aid-py
|
|
147
|
+
python -m pytest tests/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT - see [LICENSE](https://github.com/agentcommunity/agent-interface-discovery/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aid-discovery
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for Agent Interface Discovery (AID) - v1.0.0 release
|
|
5
|
+
Author-email: Agent Community <maintainers@agentcommunity.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/agent-community/agent-interface-discovery
|
|
8
|
+
Project-URL: Repository, https://github.com/agent-community/agent-interface-discovery
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: dnspython>=2.6.0
|
|
12
|
+
Requires-Dist: idna>=3.6
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# aid-discovery (Python)
|
|
17
|
+
|
|
18
|
+
> Official Python implementation of the [Agent Interface Discovery (AID)](https://github.com/agentcommunity/agent-interface-discovery) specification.
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/aid-discovery/)
|
|
21
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
|
|
23
|
+
AID enables you to discover AI agents by domain name using DNS TXT records. Type a domain, get the agent's endpoint and protocol - that's it.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install aid-discovery
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from aid_py import discover, AidError
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
# Discover an agent by domain
|
|
38
|
+
result = discover("supabase.agentcommunity.org")
|
|
39
|
+
|
|
40
|
+
print(f"Protocol: {result.record.proto}") # "mcp"
|
|
41
|
+
print(f"URI: {result.record.uri}") # "https://api.supabase.com/mcp"
|
|
42
|
+
print(f"Description: {result.record.desc}") # "Supabase MCP"
|
|
43
|
+
print(f"TTL: {result.ttl} seconds")
|
|
44
|
+
|
|
45
|
+
except AidError as e:
|
|
46
|
+
print(f"Discovery failed: {e}")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API Reference
|
|
50
|
+
|
|
51
|
+
### `discover(domain: str) -> DiscoveryResult`
|
|
52
|
+
|
|
53
|
+
Discovers an agent by looking up the `_agent` TXT record for the given domain.
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
|
|
57
|
+
- `domain` (str): The domain name to discover
|
|
58
|
+
|
|
59
|
+
**Returns:**
|
|
60
|
+
|
|
61
|
+
- `DiscoveryResult`: Object containing the parsed record and TTL
|
|
62
|
+
|
|
63
|
+
**Raises:**
|
|
64
|
+
|
|
65
|
+
- `AidError`: If discovery fails for any reason
|
|
66
|
+
|
|
67
|
+
### `parse(txt: str) -> AidRecord`
|
|
68
|
+
|
|
69
|
+
Parses and validates a raw TXT record string.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
|
|
73
|
+
- `txt` (str): Raw TXT record content (e.g., "v=aid1;uri=https://...")
|
|
74
|
+
|
|
75
|
+
**Returns:**
|
|
76
|
+
|
|
77
|
+
- `AidRecord`: Parsed and validated record
|
|
78
|
+
|
|
79
|
+
**Raises:**
|
|
80
|
+
|
|
81
|
+
- `AidError`: If parsing or validation fails
|
|
82
|
+
|
|
83
|
+
## Data Types
|
|
84
|
+
|
|
85
|
+
### `AidRecord`
|
|
86
|
+
|
|
87
|
+
Represents a parsed AID record with the following attributes:
|
|
88
|
+
|
|
89
|
+
- `v` (str): Protocol version (always "aid1")
|
|
90
|
+
- `uri` (str): Agent endpoint URI
|
|
91
|
+
- `proto` (str): Protocol identifier (e.g., "mcp", "openapi")
|
|
92
|
+
- `auth` (str, optional): Authentication method
|
|
93
|
+
- `desc` (str, optional): Human-readable description
|
|
94
|
+
|
|
95
|
+
### `DiscoveryResult`
|
|
96
|
+
|
|
97
|
+
Contains discovery results:
|
|
98
|
+
|
|
99
|
+
- `record` (AidRecord): The parsed AID record
|
|
100
|
+
- `ttl` (int): DNS TTL in seconds
|
|
101
|
+
|
|
102
|
+
### `AidError`
|
|
103
|
+
|
|
104
|
+
Exception raised when discovery or parsing fails:
|
|
105
|
+
|
|
106
|
+
- `code` (int): Numeric error code
|
|
107
|
+
- `message` (str): Human-readable error message
|
|
108
|
+
|
|
109
|
+
## Error Codes
|
|
110
|
+
|
|
111
|
+
| Code | Symbol | Description |
|
|
112
|
+
| ---- | ----------------------- | ---------------------------- |
|
|
113
|
+
| 1000 | `ERR_NO_RECORD` | No `_agent` TXT record found |
|
|
114
|
+
| 1001 | `ERR_INVALID_TXT` | Record found but malformed |
|
|
115
|
+
| 1002 | `ERR_UNSUPPORTED_PROTO` | Protocol not supported |
|
|
116
|
+
| 1003 | `ERR_SECURITY` | Security policy violation |
|
|
117
|
+
| 1004 | `ERR_DNS_LOOKUP_FAILED` | DNS query failed |
|
|
118
|
+
|
|
119
|
+
## Advanced Usage
|
|
120
|
+
|
|
121
|
+
### Custom Error Handling
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from aid_py import discover, AidError
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
result = discover("example.com")
|
|
128
|
+
# Use result.record...
|
|
129
|
+
except AidError as e:
|
|
130
|
+
if e.code == 1000: # ERR_NO_RECORD
|
|
131
|
+
print("No agent found for this domain")
|
|
132
|
+
elif e.code == 1001: # ERR_INVALID_TXT
|
|
133
|
+
print("Found a record but it's malformed")
|
|
134
|
+
else:
|
|
135
|
+
print(f"Other error: {e}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Parsing Raw Records
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from aid_py import parse, AidError
|
|
142
|
+
|
|
143
|
+
txt_record = "v=aid1;uri=https://api.example.com/agent;proto=mcp;desc=Example Agent"
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
record = parse(txt_record)
|
|
147
|
+
print(f"Parsed: {record.proto} agent at {record.uri}")
|
|
148
|
+
except AidError as e:
|
|
149
|
+
print(f"Invalid record: {e}")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
This package is part of the [AID monorepo](https://github.com/agentcommunity/agent-interface-discovery). To run tests:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# From the monorepo root
|
|
158
|
+
pnpm test
|
|
159
|
+
|
|
160
|
+
# Or run Python tests directly
|
|
161
|
+
cd packages/aid-py
|
|
162
|
+
python -m pytest tests/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT - see [LICENSE](https://github.com/agentcommunity/agent-interface-discovery/blob/main/LICENSE) for details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
aid_discovery.egg-info/PKG-INFO
|
|
4
|
+
aid_discovery.egg-info/SOURCES.txt
|
|
5
|
+
aid_discovery.egg-info/dependency_links.txt
|
|
6
|
+
aid_discovery.egg-info/requires.txt
|
|
7
|
+
aid_discovery.egg-info/top_level.txt
|
|
8
|
+
aid_py/__init__.py
|
|
9
|
+
aid_py/constants.py
|
|
10
|
+
aid_py/discover.py
|
|
11
|
+
aid_py/parser.py
|
|
12
|
+
tests/test_discover.py
|
|
13
|
+
tests/test_parity.py
|
|
14
|
+
tests/test_parser.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aid_py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
"""Agent Interface Discovery (AID) – Python library.
|
|
6
|
+
|
|
7
|
+
This is a **work-in-progress** implementation providing the same high-level API as the
|
|
8
|
+
TypeScript reference:
|
|
9
|
+
|
|
10
|
+
from aid_py import discover, parse, AidError
|
|
11
|
+
|
|
12
|
+
record = discover("example.com")
|
|
13
|
+
# ...
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Dict, Tuple
|
|
19
|
+
|
|
20
|
+
# Re-export key API pieces from submodules
|
|
21
|
+
from .parser import AidError, parse, is_valid_proto # noqa: E402
|
|
22
|
+
from .discover import discover # noqa: E402
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"discover",
|
|
26
|
+
"parse",
|
|
27
|
+
"is_valid_proto",
|
|
28
|
+
"AidError",
|
|
29
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GENERATED FILE - DO NOT EDIT
|
|
3
|
+
|
|
4
|
+
This file is auto-generated from protocol/constants.yml by scripts/generate-constants.ts
|
|
5
|
+
To make changes, edit the YAML file and run: pnpm gen
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Final, Dict, List
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Version
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
SPEC_VERSION: Final[str] = "aid1"
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Protocol tokens
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
PROTO_A2A: Final[str] = "a2a"
|
|
21
|
+
PROTO_LOCAL: Final[str] = "local"
|
|
22
|
+
PROTO_MCP: Final[str] = "mcp"
|
|
23
|
+
PROTO_OPENAPI: Final[str] = "openapi"
|
|
24
|
+
|
|
25
|
+
PROTOCOL_TOKENS: Final[Dict[str, str]] = {
|
|
26
|
+
"a2a": "a2a",
|
|
27
|
+
"local": "local",
|
|
28
|
+
"mcp": "mcp",
|
|
29
|
+
"openapi": "openapi",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Auth tokens
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
AUTH_APIKEY: Final[str] = "apikey"
|
|
36
|
+
AUTH_BASIC: Final[str] = "basic"
|
|
37
|
+
AUTH_CUSTOM: Final[str] = "custom"
|
|
38
|
+
AUTH_MTLS: Final[str] = "mtls"
|
|
39
|
+
AUTH_NONE: Final[str] = "none"
|
|
40
|
+
AUTH_OAUTH2_CODE: Final[str] = "oauth2_code"
|
|
41
|
+
AUTH_OAUTH2_DEVICE: Final[str] = "oauth2_device"
|
|
42
|
+
AUTH_PAT: Final[str] = "pat"
|
|
43
|
+
|
|
44
|
+
AUTH_TOKENS: Final[Dict[str, str]] = {
|
|
45
|
+
"apikey": "apikey",
|
|
46
|
+
"basic": "basic",
|
|
47
|
+
"custom": "custom",
|
|
48
|
+
"mtls": "mtls",
|
|
49
|
+
"none": "none",
|
|
50
|
+
"oauth2_code": "oauth2_code",
|
|
51
|
+
"oauth2_device": "oauth2_device",
|
|
52
|
+
"pat": "pat",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Error codes & messages
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
ERR_DNS_LOOKUP_FAILED: Final[int] = 1004
|
|
60
|
+
ERR_INVALID_TXT: Final[int] = 1001
|
|
61
|
+
ERR_NO_RECORD: Final[int] = 1000
|
|
62
|
+
ERR_SECURITY: Final[int] = 1003
|
|
63
|
+
ERR_UNSUPPORTED_PROTO: Final[int] = 1002
|
|
64
|
+
|
|
65
|
+
ERROR_CODES: Final[Dict[str, int]] = {
|
|
66
|
+
"ERR_DNS_LOOKUP_FAILED": ERR_DNS_LOOKUP_FAILED,
|
|
67
|
+
"ERR_INVALID_TXT": ERR_INVALID_TXT,
|
|
68
|
+
"ERR_NO_RECORD": ERR_NO_RECORD,
|
|
69
|
+
"ERR_SECURITY": ERR_SECURITY,
|
|
70
|
+
"ERR_UNSUPPORTED_PROTO": ERR_UNSUPPORTED_PROTO,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ERROR_MESSAGES: Final[Dict[str, str]] = {
|
|
74
|
+
"ERR_DNS_LOOKUP_FAILED": "The DNS query failed for a network-related reason",
|
|
75
|
+
"ERR_INVALID_TXT": "A record was found but is malformed or missing required keys",
|
|
76
|
+
"ERR_NO_RECORD": "No _agent TXT record was found for the domain",
|
|
77
|
+
"ERR_SECURITY": "Discovery failed due to a security policy (e.g., DNSSEC failure, local execution denied)",
|
|
78
|
+
"ERR_UNSUPPORTED_PROTO": "The record is valid, but the client does not support the specified protocol",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Other spec constants
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
DNS_SUBDOMAIN: Final[str] = "_agent"
|
|
86
|
+
DNS_TTL_MIN: Final[int] = 300
|
|
87
|
+
DNS_TTL_MAX: Final[int] = 900
|
|
88
|
+
|
|
89
|
+
LOCAL_URI_SCHEMES: Final[List[str]] = [
|
|
90
|
+
"docker",
|
|
91
|
+
"npx",
|
|
92
|
+
"pip",
|
|
93
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
"""DNS discovery client for Agent Interface Discovery (AID).
|
|
6
|
+
|
|
7
|
+
Uses `dnspython` to query the `_agent.<domain>` TXT record, validates it
|
|
8
|
+
with `aid_py.parse`, and returns the parsed record together with the DNS TTL.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Tuple
|
|
13
|
+
|
|
14
|
+
import dns.exception
|
|
15
|
+
import dns.resolver
|
|
16
|
+
|
|
17
|
+
from .constants import DNS_SUBDOMAIN
|
|
18
|
+
from .parser import AidError, parse
|
|
19
|
+
|
|
20
|
+
__all__ = ["discover"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _query_txt_record(fqdn: str, timeout: float) -> Tuple[list[str], int]:
|
|
24
|
+
"""Return list of TXT strings and TTL or raise AidError on DNS failure."""
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
answers = dns.resolver.resolve(fqdn, "TXT", lifetime=timeout)
|
|
28
|
+
except dns.resolver.NXDOMAIN as exc:
|
|
29
|
+
raise AidError("ERR_NO_RECORD", str(exc)) from None
|
|
30
|
+
except (dns.resolver.Timeout, dns.exception.DNSException) as exc:
|
|
31
|
+
raise AidError("ERR_DNS_LOOKUP_FAILED", str(exc)) from None
|
|
32
|
+
|
|
33
|
+
# dnspython joins multi-string automatically? Actually each answer.rdata.strings
|
|
34
|
+
ttl = answers.rrset.ttl if answers.rrset else DNS_TTL_DEFAULT
|
|
35
|
+
txt_strings: list[str] = []
|
|
36
|
+
for rdata in answers:
|
|
37
|
+
# each rdata.strings is a tuple of bytes segments
|
|
38
|
+
txt_strings.append("".join(seg.decode() for seg in rdata.strings))
|
|
39
|
+
return txt_strings, ttl
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
DNS_TTL_DEFAULT = 300 # fallback
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover(domain: str, *, protocol: str | None = None, timeout: float = 5.0) -> Tuple[dict, int]:
|
|
46
|
+
"""Discover and validate the AID record for *domain*.
|
|
47
|
+
|
|
48
|
+
Can optionally try a protocol-specific subdomain first.
|
|
49
|
+
|
|
50
|
+
Returns a tuple `(record_dict, ttl_seconds)`.
|
|
51
|
+
Raises `AidError` on any failure as per the specification.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# IDN → A-label conversion per RFC5890
|
|
55
|
+
try:
|
|
56
|
+
import idna
|
|
57
|
+
|
|
58
|
+
domain_alabel = idna.encode(domain).decode()
|
|
59
|
+
except Exception:
|
|
60
|
+
domain_alabel = domain # Fallback – let DNS resolver handle errors
|
|
61
|
+
|
|
62
|
+
def _query_and_parse(query_name: str) -> Tuple[dict, int]:
|
|
63
|
+
"""Query a specific FQDN and parse the result."""
|
|
64
|
+
txt_records, ttl = _query_txt_record(query_name, timeout)
|
|
65
|
+
|
|
66
|
+
last_error: AidError | None = None
|
|
67
|
+
for txt in txt_records:
|
|
68
|
+
try:
|
|
69
|
+
record = parse(txt)
|
|
70
|
+
return record, ttl
|
|
71
|
+
except AidError as exc:
|
|
72
|
+
# Save and try the next TXT string (if multiple records exist)
|
|
73
|
+
last_error = exc
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# If we got here, either no records or all invalid
|
|
77
|
+
if last_error is not None:
|
|
78
|
+
raise last_error
|
|
79
|
+
raise AidError("ERR_NO_RECORD", f"No valid _agent TXT record found for {query_name}")
|
|
80
|
+
|
|
81
|
+
# Try protocol-specific subdomain first if specified
|
|
82
|
+
if protocol:
|
|
83
|
+
protocol_fqdn = f"{DNS_SUBDOMAIN}.{protocol}.{domain_alabel}".rstrip(".")
|
|
84
|
+
try:
|
|
85
|
+
return _query_and_parse(protocol_fqdn)
|
|
86
|
+
except AidError as exc:
|
|
87
|
+
if exc.error_code != "ERR_NO_RECORD":
|
|
88
|
+
raise exc
|
|
89
|
+
# else: fall through to base domain query
|
|
90
|
+
|
|
91
|
+
# Fallback or default: query the base _agent subdomain
|
|
92
|
+
base_fqdn = f"{DNS_SUBDOMAIN}.{domain_alabel}".rstrip(".")
|
|
93
|
+
return _query_and_parse(base_fqdn)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
"""AID record parser and validator (Python).
|
|
6
|
+
|
|
7
|
+
Mirrors the TypeScript reference implementation in `packages/aid/src/parser.ts`.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Dict, Tuple, TypedDict
|
|
13
|
+
|
|
14
|
+
from .constants import (
|
|
15
|
+
SPEC_VERSION,
|
|
16
|
+
PROTOCOL_TOKENS,
|
|
17
|
+
AUTH_TOKENS,
|
|
18
|
+
ERROR_MESSAGES,
|
|
19
|
+
ERROR_CODES,
|
|
20
|
+
LOCAL_URI_SCHEMES,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Error class
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AidError(ValueError):
|
|
29
|
+
"""Raised when parsing/validation fails with spec-specific error codes."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, error_code: str, message: str | None = None):
|
|
32
|
+
if error_code not in ERROR_CODES:
|
|
33
|
+
raise ValueError(f"Unknown error code: {error_code}")
|
|
34
|
+
super().__init__(message or ERROR_MESSAGES[error_code])
|
|
35
|
+
self.name = "AidError"
|
|
36
|
+
self.error_code: str = error_code # symbolic (e.g. "ERR_INVALID_TXT")
|
|
37
|
+
self.code: int = ERROR_CODES[error_code] # numeric (e.g. 1001)
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
40
|
+
return f"AidError(error_code={self.error_code}, message={self.args[0]!r})"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Types
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AidRecord(TypedDict, total=False):
|
|
49
|
+
v: str
|
|
50
|
+
uri: str
|
|
51
|
+
proto: str
|
|
52
|
+
auth: str
|
|
53
|
+
desc: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
RawAidRecord = Dict[str, str]
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# Helpers
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_raw_record(txt: str) -> RawAidRecord:
|
|
64
|
+
"""Split semicolon-delimited key=value pairs into a dict."""
|
|
65
|
+
|
|
66
|
+
record: RawAidRecord = {}
|
|
67
|
+
|
|
68
|
+
# Drop surrounding whitespace and split by semicolon
|
|
69
|
+
for pair in [p.strip() for p in txt.split(";") if p.strip()]:
|
|
70
|
+
if "=" not in pair:
|
|
71
|
+
raise AidError("ERR_INVALID_TXT", f"Invalid key-value pair: {pair}")
|
|
72
|
+
key, value = pair.split("=", 1)
|
|
73
|
+
key = key.strip().lower()
|
|
74
|
+
value = value.strip()
|
|
75
|
+
if not key or not value:
|
|
76
|
+
raise AidError("ERR_INVALID_TXT", f"Empty key or value in pair: {pair}")
|
|
77
|
+
if key in record:
|
|
78
|
+
raise AidError("ERR_INVALID_TXT", f"Duplicate key: {key}")
|
|
79
|
+
record[key] = value
|
|
80
|
+
return record
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_valid_local_uri(uri: str) -> bool:
|
|
84
|
+
match = re.match(r"^(?P<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):", uri)
|
|
85
|
+
if not match:
|
|
86
|
+
return False
|
|
87
|
+
scheme = match.group("scheme")
|
|
88
|
+
return scheme in LOCAL_URI_SCHEMES
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Public API
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_record(raw: RawAidRecord) -> AidRecord:
|
|
97
|
+
"""Validate a raw record dict and return a typed record."""
|
|
98
|
+
|
|
99
|
+
# Required fields
|
|
100
|
+
if "v" not in raw:
|
|
101
|
+
raise AidError("ERR_INVALID_TXT", "Missing required field: v")
|
|
102
|
+
if "uri" not in raw:
|
|
103
|
+
raise AidError("ERR_INVALID_TXT", "Missing required field: uri")
|
|
104
|
+
|
|
105
|
+
# proto or p but not both
|
|
106
|
+
has_proto = "proto" in raw
|
|
107
|
+
has_p = "p" in raw
|
|
108
|
+
if has_proto and has_p:
|
|
109
|
+
raise AidError("ERR_INVALID_TXT", 'Cannot specify both "proto" and "p" fields')
|
|
110
|
+
if not has_proto and not has_p:
|
|
111
|
+
raise AidError("ERR_INVALID_TXT", "Missing required field: proto (or p)")
|
|
112
|
+
|
|
113
|
+
# Version check
|
|
114
|
+
if raw["v"] != SPEC_VERSION:
|
|
115
|
+
raise AidError(
|
|
116
|
+
"ERR_INVALID_TXT",
|
|
117
|
+
f"Unsupported version: {raw['v']}. Expected: {SPEC_VERSION}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
proto_value = raw.get("proto") or raw.get("p") # already ensured exists
|
|
121
|
+
if proto_value not in PROTOCOL_TOKENS:
|
|
122
|
+
raise AidError("ERR_UNSUPPORTED_PROTO", f"Unsupported protocol: {proto_value}")
|
|
123
|
+
|
|
124
|
+
# Auth token validation
|
|
125
|
+
if "auth" in raw and raw["auth"] not in AUTH_TOKENS:
|
|
126
|
+
raise AidError("ERR_INVALID_TXT", f"Invalid auth token: {raw['auth']}")
|
|
127
|
+
|
|
128
|
+
# Description length ≤ 60 UTF-8 bytes
|
|
129
|
+
if "desc" in raw and len(raw["desc"].encode("utf-8")) > 60:
|
|
130
|
+
raise AidError("ERR_INVALID_TXT", "Description field must be ≤ 60 UTF-8 bytes")
|
|
131
|
+
|
|
132
|
+
# URI validation
|
|
133
|
+
uri = raw["uri"]
|
|
134
|
+
if proto_value == "local":
|
|
135
|
+
# Must use approved local scheme
|
|
136
|
+
if not _is_valid_local_uri(uri):
|
|
137
|
+
raise AidError(
|
|
138
|
+
"ERR_INVALID_TXT",
|
|
139
|
+
f"Invalid URI scheme for local protocol. Must be one of: {', '.join(LOCAL_URI_SCHEMES)}",
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
if not uri.startswith("https://"):
|
|
143
|
+
raise AidError(
|
|
144
|
+
"ERR_INVALID_TXT",
|
|
145
|
+
f"Invalid URI scheme for remote protocol '{proto_value}'. MUST be 'https:'",
|
|
146
|
+
)
|
|
147
|
+
# Basic URL validation
|
|
148
|
+
try:
|
|
149
|
+
from urllib.parse import urlparse
|
|
150
|
+
|
|
151
|
+
parsed = urlparse(uri)
|
|
152
|
+
if not parsed.scheme or not parsed.netloc:
|
|
153
|
+
raise ValueError
|
|
154
|
+
except Exception:
|
|
155
|
+
raise AidError("ERR_INVALID_TXT", f"Invalid URI format: {uri}") from None
|
|
156
|
+
|
|
157
|
+
# Build typed record
|
|
158
|
+
record: AidRecord = {
|
|
159
|
+
"v": "aid1",
|
|
160
|
+
"uri": uri,
|
|
161
|
+
"proto": proto_value, # type: ignore[assignment]
|
|
162
|
+
}
|
|
163
|
+
if "auth" in raw:
|
|
164
|
+
record["auth"] = raw["auth"]
|
|
165
|
+
if "desc" in raw:
|
|
166
|
+
record["desc"] = raw["desc"]
|
|
167
|
+
return record
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse(txt_record: str) -> AidRecord:
|
|
171
|
+
"""Parse and validate a TXT record string."""
|
|
172
|
+
|
|
173
|
+
raw = _parse_raw_record(txt_record)
|
|
174
|
+
return validate_record(raw)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def is_valid_proto(token: str) -> bool:
|
|
178
|
+
return token in PROTOCOL_TOKENS
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Expose main helpers for import convenience
|
|
182
|
+
__all__ = [
|
|
183
|
+
"AidError",
|
|
184
|
+
"parse",
|
|
185
|
+
"validate_record",
|
|
186
|
+
"is_valid_proto",
|
|
187
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aid-discovery"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python library for Agent Interface Discovery (AID) - v1.0.0 release"
|
|
9
|
+
authors = [{ name = "Agent Community", email = "maintainers@agentcommunity.org" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = ["dnspython>=2.6.0", "idna>=3.6"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/agent-community/agent-interface-discovery"
|
|
17
|
+
Repository = "https://github.com/agent-community/agent-interface-discovery"
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
import types, sys, pathlib
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
sys.path.append(str(pathlib.Path(__file__).resolve().parents[1]))
|
|
9
|
+
|
|
10
|
+
from aid_py import discover, AidError # noqa: E402
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _FakeRdata: # minimal stub
|
|
14
|
+
def __init__(self, strings):
|
|
15
|
+
self.strings = tuple(s.encode() for s in strings)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _FakeAnswer(list):
|
|
19
|
+
def __init__(self, strings_list, ttl):
|
|
20
|
+
super().__init__(_FakeRdata([s]) for s in strings_list)
|
|
21
|
+
rrset = types.SimpleNamespace()
|
|
22
|
+
rrset.ttl = ttl
|
|
23
|
+
self.rrset = rrset
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture()
|
|
27
|
+
def monkey_resolver(monkeypatch):
|
|
28
|
+
import dns.resolver
|
|
29
|
+
|
|
30
|
+
def _fake_resolve(name, rdtype, lifetime=5.0):
|
|
31
|
+
assert rdtype == "TXT"
|
|
32
|
+
if name == "_agent.example.com":
|
|
33
|
+
return _FakeAnswer(["v=aid1;uri=https://api.example.com/mcp;proto=mcp"], 300)
|
|
34
|
+
raise dns.resolver.NXDOMAIN()
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(dns.resolver, "resolve", _fake_resolve)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_discover_success(monkey_resolver): # pylint: disable=unused-argument
|
|
40
|
+
record, ttl = discover("example.com")
|
|
41
|
+
assert record["proto"] == "mcp"
|
|
42
|
+
assert ttl == 300
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_discover_protocol_specific_success(monkeypatch):
|
|
46
|
+
import dns.resolver
|
|
47
|
+
|
|
48
|
+
def _fake_resolve(name, rdtype, lifetime=5.0):
|
|
49
|
+
if name == "_agent.mcp.example.com":
|
|
50
|
+
return _FakeAnswer(["v=aid1;uri=https://api.example.com/mcp;proto=mcp"], 333)
|
|
51
|
+
if name == "_agent.example.com":
|
|
52
|
+
return _FakeAnswer(["v=aid1;uri=https://api.example.com/fallback;p=a2a"], 444)
|
|
53
|
+
raise dns.resolver.NXDOMAIN()
|
|
54
|
+
|
|
55
|
+
monkeypatch.setattr(dns.resolver, "resolve", _fake_resolve)
|
|
56
|
+
record, ttl = discover("example.com", protocol="mcp")
|
|
57
|
+
assert record["proto"] == "mcp"
|
|
58
|
+
assert record["uri"] == "https://api.example.com/mcp"
|
|
59
|
+
assert ttl == 333
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_discover_fallback_to_base(monkeypatch):
|
|
63
|
+
import dns.resolver
|
|
64
|
+
|
|
65
|
+
def _fake_resolve(name, rdtype, lifetime=5.0):
|
|
66
|
+
if name == "_agent.mcp.example.com":
|
|
67
|
+
raise dns.resolver.NXDOMAIN()
|
|
68
|
+
if name == "_agent.example.com":
|
|
69
|
+
return _FakeAnswer(["v=aid1;uri=https://fallback.com;p=a2a"], 555)
|
|
70
|
+
raise dns.resolver.NXDOMAIN()
|
|
71
|
+
|
|
72
|
+
monkeypatch.setattr(dns.resolver, "resolve", _fake_resolve)
|
|
73
|
+
record, ttl = discover("example.com", protocol="mcp")
|
|
74
|
+
assert record["proto"] == "a2a"
|
|
75
|
+
assert record["uri"] == "https://fallback.com"
|
|
76
|
+
assert ttl == 555
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_discover_no_record(monkeypatch):
|
|
80
|
+
import dns.resolver
|
|
81
|
+
|
|
82
|
+
def _no_record(name, rdtype, lifetime=5.0):
|
|
83
|
+
raise dns.resolver.NXDOMAIN()
|
|
84
|
+
|
|
85
|
+
monkeypatch.setattr(dns.resolver, "resolve", _no_record)
|
|
86
|
+
with pytest.raises(AidError):
|
|
87
|
+
discover("missing.com")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
"""Cross-language parity test (Python).
|
|
6
|
+
Parses the shared golden fixtures and ensures output matches expected.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from aid_py.parser import parse
|
|
14
|
+
|
|
15
|
+
FIXTURE_PATH = Path(__file__).parents[3] / "test-fixtures" / "golden.json"
|
|
16
|
+
_fixture = json.loads(FIXTURE_PATH.read_text())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parity():
|
|
20
|
+
for rec in _fixture["records"]:
|
|
21
|
+
parsed = parse(rec["raw"])
|
|
22
|
+
# Convert TypedDict to plain dict for comparison
|
|
23
|
+
assert dict(parsed) == rec["expected"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
# Copyright (c) 2025 Agent Community
|
|
3
|
+
# Author: Agent Community
|
|
4
|
+
# Repository: https://github.com/agentcommunity/agent-interface-discovery
|
|
5
|
+
|
|
6
|
+
import sys, pathlib
|
|
7
|
+
sys.path.append(str(pathlib.Path(__file__).resolve().parents[1]))
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from aid_py import parse, AidError, is_valid_proto
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parse_valid_record():
|
|
15
|
+
txt = "v=aid1;uri=https://api.example.com/mcp;proto=mcp;auth=pat;desc=Test Agent"
|
|
16
|
+
record = parse(txt)
|
|
17
|
+
assert record == {
|
|
18
|
+
"v": "aid1",
|
|
19
|
+
"uri": "https://api.example.com/mcp",
|
|
20
|
+
"proto": "mcp",
|
|
21
|
+
"auth": "pat",
|
|
22
|
+
"desc": "Test Agent",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_parse_alias_p():
|
|
27
|
+
txt = "v=aid1;uri=https://api.example.com/mcp;p=mcp"
|
|
28
|
+
record = parse(txt)
|
|
29
|
+
assert record == {
|
|
30
|
+
"v": "aid1",
|
|
31
|
+
"uri": "https://api.example.com/mcp",
|
|
32
|
+
"proto": "mcp",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_missing_version():
|
|
37
|
+
txt = "uri=https://api.example.com/mcp;proto=mcp"
|
|
38
|
+
with pytest.raises(AidError):
|
|
39
|
+
parse(txt)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_invalid_proto():
|
|
43
|
+
txt = "v=aid1;uri=https://api.example.com/mcp;proto=unknown"
|
|
44
|
+
with pytest.raises(AidError):
|
|
45
|
+
parse(txt)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_description_length():
|
|
49
|
+
long_desc = "This is a very long description that exceeds the 60 UTF-8 byte limit for AID records"
|
|
50
|
+
txt = f"v=aid1;uri=https://api.example.com/mcp;proto=mcp;desc={long_desc}"
|
|
51
|
+
with pytest.raises(AidError):
|
|
52
|
+
parse(txt)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_is_valid_proto():
|
|
56
|
+
assert is_valid_proto("mcp") is True
|
|
57
|
+
assert is_valid_proto("unknown") is False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_duplicate_keys():
|
|
61
|
+
txt = "v=aid1;v=aid1;uri=https://api.example.com/mcp;proto=mcp"
|
|
62
|
+
with pytest.raises(AidError, match="Duplicate key: v"):
|
|
63
|
+
parse(txt)
|