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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/aid-discovery.svg?color=blue)](https://pypi.org/project/aid-discovery/)
21
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/aid-discovery.svg?color=blue)](https://pypi.org/project/aid-discovery/)
6
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/aid-discovery.svg?color=blue)](https://pypi.org/project/aid-discovery/)
21
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](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,5 @@
1
+ dnspython>=2.6.0
2
+ idna>=3.6
3
+
4
+ [dev]
5
+ pytest>=8
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)