nest-plugins-reference 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.
- nest_plugins_reference-0.1.0/.gitignore +10 -0
- nest_plugins_reference-0.1.0/PKG-INFO +32 -0
- nest_plugins_reference-0.1.0/README.md +13 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/__init__.py +4 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/auth/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/auth/jwt_auth.py +102 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/comms/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/comms/nest_native.py +114 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/coordination/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/coordination/contract_net.py +95 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/datafacts/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/datafacts/datafacts_v1.py +78 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/identity/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/identity/did_key.py +101 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/memory/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/memory/blackboard.py +80 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/negotiation/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/negotiation/alternating_offers.py +98 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/payments/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/payments/prepaid_credits.py +99 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/privacy/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/privacy/noop.py +59 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/py.typed +0 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/registry/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/registry/in_memory.py +87 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/transport/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/transport/in_memory.py +111 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/trust/__init__.py +1 -0
- nest_plugins_reference-0.1.0/nest_plugins_reference/trust/score_average.py +93 -0
- nest_plugins_reference-0.1.0/pyproject.toml +32 -0
- nest_plugins_reference-0.1.0/tests/test_imports.py +9 -0
- nest_plugins_reference-0.1.0/tests/test_plugins.py +481 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nest-plugins-reference
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NEST reference plugins: default implementations for all 12 layers
|
|
5
|
+
Project-URL: Homepage, https://github.com/mariagorskikh/nest
|
|
6
|
+
Project-URL: Repository, https://github.com/mariagorskikh/nest
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: nest-core
|
|
17
|
+
Requires-Dist: nest-sdk
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# nest-plugins-reference
|
|
21
|
+
|
|
22
|
+
NEST reference plugins: default implementations for all 12 layers
|
|
23
|
+
|
|
24
|
+
Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install nest-plugins-reference
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# nest-plugins-reference
|
|
2
|
+
|
|
3
|
+
NEST reference plugins: default implementations for all 12 layers
|
|
4
|
+
|
|
5
|
+
Part of [NEST](https://github.com/mariagorskikh/nest) (Network Environment for Swarm Testing), built at MIT Media Lab.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install nest-plugins-reference
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
See the [main repository](https://github.com/mariagorskikh/nest) for full documentation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""JWT auth plugin — sign tokens with HMAC-SHA256 for simulation.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
auth = JwtAuth(secret=b"my-secret")
|
|
7
|
+
token = await auth.issue(AgentId("a1"), ["read", "write"])
|
|
8
|
+
ctx = await auth.verify(token)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import hmac
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
from nest_core.types import AgentId, AuthContext, Token
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JwtAuth:
|
|
22
|
+
"""Simplified JWT-style auth using HMAC-SHA256.
|
|
23
|
+
|
|
24
|
+
Example::
|
|
25
|
+
|
|
26
|
+
auth = JwtAuth(secret=b"secret")
|
|
27
|
+
token = await auth.issue(AgentId("a1"), ["read"])
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, secret: bytes = b"nest-default-secret", clock: float | None = None) -> None:
|
|
31
|
+
self._secret = secret
|
|
32
|
+
self._clock = clock
|
|
33
|
+
self._revoked: set[str] = set()
|
|
34
|
+
|
|
35
|
+
def _now(self) -> float:
|
|
36
|
+
if self._clock is not None:
|
|
37
|
+
return self._clock
|
|
38
|
+
return time.time()
|
|
39
|
+
|
|
40
|
+
def _sign(self, payload: str) -> str:
|
|
41
|
+
return hmac.new(self._secret, payload.encode(), hashlib.sha256).hexdigest()
|
|
42
|
+
|
|
43
|
+
async def issue(self, subject: AgentId, scopes: list[str]) -> Token:
|
|
44
|
+
"""Issue a token for a subject with given scopes.
|
|
45
|
+
|
|
46
|
+
Example::
|
|
47
|
+
|
|
48
|
+
token = await auth.issue(AgentId("a1"), ["read", "write"])
|
|
49
|
+
"""
|
|
50
|
+
now = self._now()
|
|
51
|
+
payload = json.dumps(
|
|
52
|
+
{
|
|
53
|
+
"sub": str(subject),
|
|
54
|
+
"scopes": scopes,
|
|
55
|
+
"iat": now,
|
|
56
|
+
"exp": now + 3600,
|
|
57
|
+
},
|
|
58
|
+
sort_keys=True,
|
|
59
|
+
)
|
|
60
|
+
sig = self._sign(payload)
|
|
61
|
+
return Token(f"{payload}|{sig}")
|
|
62
|
+
|
|
63
|
+
async def verify(self, token: Token) -> AuthContext:
|
|
64
|
+
"""Verify a token and return its context.
|
|
65
|
+
|
|
66
|
+
Example::
|
|
67
|
+
|
|
68
|
+
ctx = await auth.verify(token)
|
|
69
|
+
assert ctx.subject == AgentId("a1")
|
|
70
|
+
"""
|
|
71
|
+
raw = str(token)
|
|
72
|
+
if raw in self._revoked:
|
|
73
|
+
msg = "Token has been revoked"
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
parts = raw.rsplit("|", 1)
|
|
77
|
+
if len(parts) != 2:
|
|
78
|
+
msg = "Invalid token format"
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
|
|
81
|
+
payload_str, sig = parts
|
|
82
|
+
expected = self._sign(payload_str)
|
|
83
|
+
if not hmac.compare_digest(sig, expected):
|
|
84
|
+
msg = "Invalid token signature"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
|
|
87
|
+
data = json.loads(payload_str)
|
|
88
|
+
return AuthContext(
|
|
89
|
+
subject=AgentId(data["sub"]),
|
|
90
|
+
scopes=data["scopes"],
|
|
91
|
+
issued_at=data["iat"],
|
|
92
|
+
expires_at=data["exp"],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def revoke(self, token: Token) -> None:
|
|
96
|
+
"""Revoke a token.
|
|
97
|
+
|
|
98
|
+
Example::
|
|
99
|
+
|
|
100
|
+
await auth.revoke(token)
|
|
101
|
+
"""
|
|
102
|
+
self._revoked.add(str(token))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Nest-native communication plugin — minimal JSON envelope.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
comms = NestNativeComms(AgentId("a1"), transport, registry)
|
|
7
|
+
raw = comms.serialize(msg)
|
|
8
|
+
msg2 = comms.deserialize(raw)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from nest_core.types import (
|
|
18
|
+
AgentCard,
|
|
19
|
+
AgentId,
|
|
20
|
+
Message,
|
|
21
|
+
MessageId,
|
|
22
|
+
Query,
|
|
23
|
+
Response,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NestNativeComms:
|
|
28
|
+
"""Minimal JSON-based communication protocol.
|
|
29
|
+
|
|
30
|
+
Example::
|
|
31
|
+
|
|
32
|
+
comms = NestNativeComms(AgentId("a1"), transport=t, registry=r)
|
|
33
|
+
resp = await comms.send(AgentId("a2"), msg)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
agent_id: AgentId,
|
|
39
|
+
transport: Any = None,
|
|
40
|
+
registry: Any = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._agent_id = agent_id
|
|
43
|
+
self._transport = transport
|
|
44
|
+
self._registry = registry
|
|
45
|
+
|
|
46
|
+
def serialize(self, msg: Message) -> bytes:
|
|
47
|
+
"""Serialize a Message to JSON bytes.
|
|
48
|
+
|
|
49
|
+
Example::
|
|
50
|
+
|
|
51
|
+
raw = comms.serialize(msg)
|
|
52
|
+
"""
|
|
53
|
+
data = {
|
|
54
|
+
"id": str(msg.id),
|
|
55
|
+
"sender": str(msg.sender),
|
|
56
|
+
"receiver": str(msg.receiver),
|
|
57
|
+
"payload": base64.b64encode(msg.payload).decode("ascii"),
|
|
58
|
+
"correlation_id": str(msg.correlation_id) if msg.correlation_id else None,
|
|
59
|
+
"timestamp": msg.timestamp,
|
|
60
|
+
"metadata": msg.metadata,
|
|
61
|
+
}
|
|
62
|
+
return json.dumps(data, sort_keys=True).encode("utf-8")
|
|
63
|
+
|
|
64
|
+
def deserialize(self, raw: bytes) -> Message:
|
|
65
|
+
"""Deserialize JSON bytes back to a Message.
|
|
66
|
+
|
|
67
|
+
Example::
|
|
68
|
+
|
|
69
|
+
msg = comms.deserialize(raw)
|
|
70
|
+
"""
|
|
71
|
+
data = json.loads(raw)
|
|
72
|
+
return Message(
|
|
73
|
+
id=MessageId(data["id"]),
|
|
74
|
+
sender=AgentId(data["sender"]),
|
|
75
|
+
receiver=AgentId(data["receiver"]),
|
|
76
|
+
payload=base64.b64decode(data["payload"]),
|
|
77
|
+
correlation_id=data.get("correlation_id"),
|
|
78
|
+
timestamp=data.get("timestamp"),
|
|
79
|
+
metadata=data.get("metadata", {}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def send(self, to: AgentId, msg: Message) -> Response:
|
|
83
|
+
"""Send a message via the transport layer.
|
|
84
|
+
|
|
85
|
+
Example::
|
|
86
|
+
|
|
87
|
+
resp = await comms.send(AgentId("a2"), msg)
|
|
88
|
+
"""
|
|
89
|
+
raw = self.serialize(msg)
|
|
90
|
+
if self._transport is not None:
|
|
91
|
+
await self._transport.send(to, raw)
|
|
92
|
+
return Response(success=True)
|
|
93
|
+
|
|
94
|
+
async def advertise(self, card: AgentCard) -> None:
|
|
95
|
+
"""Advertise an agent card to the registry.
|
|
96
|
+
|
|
97
|
+
Example::
|
|
98
|
+
|
|
99
|
+
await comms.advertise(my_card)
|
|
100
|
+
"""
|
|
101
|
+
if self._registry is not None:
|
|
102
|
+
await self._registry.register(card)
|
|
103
|
+
|
|
104
|
+
async def discover(self, query: Query) -> list[AgentCard]:
|
|
105
|
+
"""Discover agents via the registry.
|
|
106
|
+
|
|
107
|
+
Example::
|
|
108
|
+
|
|
109
|
+
cards = await comms.discover(Query(capabilities=["sell"]))
|
|
110
|
+
"""
|
|
111
|
+
if self._registry is not None:
|
|
112
|
+
result: list[AgentCard] = await self._registry.lookup(query)
|
|
113
|
+
return result
|
|
114
|
+
return []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Contract Net coordination plugin — classic FIPA Contract Net Protocol.
|
|
3
|
+
|
|
4
|
+
The Round object is shared between manager and workers. Bids are stored
|
|
5
|
+
in the round's metadata so any party can resolve them.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
coord = ContractNet(AgentId("manager"))
|
|
10
|
+
rnd = await coord.propose(task)
|
|
11
|
+
bid = await coord.participate(rnd)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from nest_core.types import (
|
|
19
|
+
AgentId,
|
|
20
|
+
Bid,
|
|
21
|
+
Money,
|
|
22
|
+
Outcome,
|
|
23
|
+
Round,
|
|
24
|
+
Task,
|
|
25
|
+
Vote,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ContractNet:
|
|
30
|
+
"""FIPA Contract Net Protocol implementation.
|
|
31
|
+
|
|
32
|
+
Example::
|
|
33
|
+
|
|
34
|
+
coord = ContractNet(AgentId("a1"))
|
|
35
|
+
rnd = await coord.propose(Task(id="t1", description="work"))
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, agent_id: AgentId) -> None:
|
|
39
|
+
self._agent_id = agent_id
|
|
40
|
+
|
|
41
|
+
async def propose(self, task: Task) -> Round:
|
|
42
|
+
"""Propose a task for bidding.
|
|
43
|
+
|
|
44
|
+
Example::
|
|
45
|
+
|
|
46
|
+
rnd = await coord.propose(task)
|
|
47
|
+
"""
|
|
48
|
+
round_id = str(uuid.uuid4())
|
|
49
|
+
rnd = Round(
|
|
50
|
+
id=round_id,
|
|
51
|
+
task=task,
|
|
52
|
+
participants=[],
|
|
53
|
+
metadata={"bids": []},
|
|
54
|
+
)
|
|
55
|
+
return rnd
|
|
56
|
+
|
|
57
|
+
async def participate(self, round: Round) -> Vote | Bid:
|
|
58
|
+
"""Submit a bid for a round.
|
|
59
|
+
|
|
60
|
+
Example::
|
|
61
|
+
|
|
62
|
+
bid = await coord.participate(rnd)
|
|
63
|
+
"""
|
|
64
|
+
bid = Bid(
|
|
65
|
+
bidder=self._agent_id,
|
|
66
|
+
round_id=round.id,
|
|
67
|
+
amount=Money(amount=1),
|
|
68
|
+
)
|
|
69
|
+
bids: list[dict[str, object]] = round.metadata.setdefault("bids", [])
|
|
70
|
+
bids.append({"bidder": str(bid.bidder), "amount": bid.amount.amount})
|
|
71
|
+
if self._agent_id not in round.participants:
|
|
72
|
+
round.participants.append(self._agent_id)
|
|
73
|
+
return bid
|
|
74
|
+
|
|
75
|
+
async def resolve(self, round: Round) -> Outcome:
|
|
76
|
+
"""Resolve a round by selecting the lowest bidder.
|
|
77
|
+
|
|
78
|
+
Example::
|
|
79
|
+
|
|
80
|
+
outcome = await coord.resolve(rnd)
|
|
81
|
+
"""
|
|
82
|
+
bids: list[dict[str, object]] = round.metadata.get("bids", [])
|
|
83
|
+
winner: AgentId | None = None
|
|
84
|
+
if bids:
|
|
85
|
+
best = min(bids, key=lambda b: int(str(b["amount"])))
|
|
86
|
+
winner = AgentId(str(best["bidder"]))
|
|
87
|
+
return Outcome(round_id=round.id, winner=winner, task=round.task)
|
|
88
|
+
|
|
89
|
+
async def commit(self, outcome: Outcome) -> None:
|
|
90
|
+
"""Commit to an outcome.
|
|
91
|
+
|
|
92
|
+
Example::
|
|
93
|
+
|
|
94
|
+
await coord.commit(outcome)
|
|
95
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""DataFacts v1 plugin — dataset metadata registry.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
df = DataFactsV1()
|
|
7
|
+
url = await df.publish(DatasetMetadata(name="weather", owner=AgentId("a1")))
|
|
8
|
+
meta = await df.fetch(url)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from nest_core.types import AccessGrant, AgentId, DataFactsUrl, DatasetMetadata
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DataFactsV1:
|
|
19
|
+
"""In-memory DataFacts metadata registry.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
df = DataFactsV1()
|
|
24
|
+
url = await df.publish(meta)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._datasets: dict[DataFactsUrl, DatasetMetadata] = {}
|
|
29
|
+
self._grants: dict[DataFactsUrl, list[AccessGrant]] = {}
|
|
30
|
+
self._timestamps: dict[DataFactsUrl, float] = {}
|
|
31
|
+
|
|
32
|
+
async def publish(self, dataset: DatasetMetadata) -> DataFactsUrl:
|
|
33
|
+
"""Publish dataset metadata and return its URL.
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
url = await df.publish(DatasetMetadata(name="weather", owner=AgentId("a1")))
|
|
38
|
+
"""
|
|
39
|
+
url = DataFactsUrl(f"df://{dataset.name}")
|
|
40
|
+
self._datasets[url] = dataset
|
|
41
|
+
self._timestamps[url] = time.time()
|
|
42
|
+
return url
|
|
43
|
+
|
|
44
|
+
async def fetch(self, url: DataFactsUrl) -> DatasetMetadata:
|
|
45
|
+
"""Fetch metadata for a dataset URL.
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
meta = await df.fetch(DataFactsUrl("df://weather"))
|
|
50
|
+
"""
|
|
51
|
+
meta = self._datasets.get(url)
|
|
52
|
+
if meta is None:
|
|
53
|
+
msg = f"Dataset not found: {url}"
|
|
54
|
+
raise KeyError(msg)
|
|
55
|
+
return meta
|
|
56
|
+
|
|
57
|
+
async def request_access(self, url: DataFactsUrl, requester: AgentId) -> AccessGrant:
|
|
58
|
+
"""Request access to a dataset (always grants in v1).
|
|
59
|
+
|
|
60
|
+
Example::
|
|
61
|
+
|
|
62
|
+
grant = await df.request_access(url, AgentId("a2"))
|
|
63
|
+
"""
|
|
64
|
+
grant = AccessGrant(url=url, grantee=requester, tier="read")
|
|
65
|
+
self._grants.setdefault(url, []).append(grant)
|
|
66
|
+
return grant
|
|
67
|
+
|
|
68
|
+
async def verify_freshness(self, url: DataFactsUrl) -> bool:
|
|
69
|
+
"""Check if a dataset was published within the last hour.
|
|
70
|
+
|
|
71
|
+
Example::
|
|
72
|
+
|
|
73
|
+
fresh = await df.verify_freshness(url)
|
|
74
|
+
"""
|
|
75
|
+
ts = self._timestamps.get(url)
|
|
76
|
+
if ts is None:
|
|
77
|
+
return False
|
|
78
|
+
return (time.time() - ts) < 3600
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""DID:key identity plugin — Ed25519 key-based identity.
|
|
3
|
+
|
|
4
|
+
Uses hashlib for deterministic key derivation in simulation (no real crypto
|
|
5
|
+
needed for testing). For real deployments, swap to a proper Ed25519 library.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
identity = DidKeyIdentity(AgentId("a1"), seed=b"secret")
|
|
10
|
+
sig = identity.sign(b"payload")
|
|
11
|
+
ok = identity.verify(b"payload", sig, AgentId("a1"))
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
|
|
19
|
+
from nest_core.types import AgentId, AgentIdentity, Signature
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DidKeyIdentity:
|
|
23
|
+
"""Ed25519-style identity using HMAC-SHA256 for simulation.
|
|
24
|
+
|
|
25
|
+
Example::
|
|
26
|
+
|
|
27
|
+
ident = DidKeyIdentity(AgentId("a1"), seed=b"seed")
|
|
28
|
+
sig = ident.sign(b"hello")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, agent_id: AgentId, seed: bytes = b"") -> None:
|
|
32
|
+
self._agent_id = agent_id
|
|
33
|
+
self._seed = seed
|
|
34
|
+
key_material = hashlib.sha256(seed + agent_id.encode()).digest()
|
|
35
|
+
self._private_key = key_material
|
|
36
|
+
self._public_key = hashlib.sha256(key_material).digest()
|
|
37
|
+
self._known_keys: dict[AgentId, bytes] = {agent_id: self._public_key}
|
|
38
|
+
self._private_keys: dict[AgentId, bytes] = {agent_id: self._private_key}
|
|
39
|
+
|
|
40
|
+
def register_peer(
|
|
41
|
+
self,
|
|
42
|
+
agent_id: AgentId,
|
|
43
|
+
public_key: bytes,
|
|
44
|
+
private_key: bytes | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Register a peer's public key (and optionally private key) for verification.
|
|
47
|
+
|
|
48
|
+
Example::
|
|
49
|
+
|
|
50
|
+
ident.register_peer(AgentId("a2"), peer_pk)
|
|
51
|
+
"""
|
|
52
|
+
self._known_keys[agent_id] = public_key
|
|
53
|
+
if private_key is not None:
|
|
54
|
+
self._private_keys[agent_id] = private_key
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def public_key(self) -> bytes:
|
|
58
|
+
"""This agent's public key.
|
|
59
|
+
|
|
60
|
+
Example::
|
|
61
|
+
|
|
62
|
+
pk = ident.public_key
|
|
63
|
+
"""
|
|
64
|
+
return self._public_key
|
|
65
|
+
|
|
66
|
+
def sign(self, payload: bytes) -> Signature:
|
|
67
|
+
"""Sign a payload with this agent's private key.
|
|
68
|
+
|
|
69
|
+
Example::
|
|
70
|
+
|
|
71
|
+
sig = ident.sign(b"data")
|
|
72
|
+
"""
|
|
73
|
+
sig_bytes = hmac.new(self._private_key, payload, hashlib.sha256).digest()
|
|
74
|
+
return Signature(signer=self._agent_id, value=sig_bytes, algorithm="hmac-sha256")
|
|
75
|
+
|
|
76
|
+
def verify(self, payload: bytes, sig: Signature, agent: AgentId) -> bool:
|
|
77
|
+
"""Verify a signature from a given agent.
|
|
78
|
+
|
|
79
|
+
Example::
|
|
80
|
+
|
|
81
|
+
ok = ident.verify(b"data", sig, AgentId("a1"))
|
|
82
|
+
"""
|
|
83
|
+
private_key = self._private_keys.get(agent)
|
|
84
|
+
if private_key is None:
|
|
85
|
+
return False
|
|
86
|
+
expected = hmac.new(private_key, payload, hashlib.sha256).digest()
|
|
87
|
+
return hmac.compare_digest(sig.value, expected)
|
|
88
|
+
|
|
89
|
+
async def resolve(self, agent: AgentId) -> AgentIdentity:
|
|
90
|
+
"""Resolve an agent ID to its identity record.
|
|
91
|
+
|
|
92
|
+
Example::
|
|
93
|
+
|
|
94
|
+
info = await ident.resolve(AgentId("a1"))
|
|
95
|
+
"""
|
|
96
|
+
pk = self._known_keys.get(agent, b"")
|
|
97
|
+
return AgentIdentity(
|
|
98
|
+
agent_id=agent,
|
|
99
|
+
public_key=pk,
|
|
100
|
+
method="did:key",
|
|
101
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Blackboard memory plugin — shared key-value store.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
mem = Blackboard()
|
|
7
|
+
await mem.write("key", b"value")
|
|
8
|
+
val = await mem.read("key")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Blackboard:
|
|
18
|
+
"""Shared key-value blackboard for agent state.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
bb = Blackboard()
|
|
23
|
+
await bb.write("counter", b"42")
|
|
24
|
+
val = await bb.read("counter")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self) -> None:
|
|
28
|
+
self._store: dict[str, bytes] = {}
|
|
29
|
+
self._subscribers: dict[str, list[asyncio.Queue[bytes]]] = {}
|
|
30
|
+
|
|
31
|
+
async def read(self, key: str) -> bytes | None:
|
|
32
|
+
"""Read a value by key.
|
|
33
|
+
|
|
34
|
+
Example::
|
|
35
|
+
|
|
36
|
+
val = await bb.read("counter")
|
|
37
|
+
"""
|
|
38
|
+
return self._store.get(key)
|
|
39
|
+
|
|
40
|
+
async def write(self, key: str, value: bytes) -> None:
|
|
41
|
+
"""Write a value for a key, notifying subscribers.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
await bb.write("counter", b"42")
|
|
46
|
+
"""
|
|
47
|
+
self._store[key] = value
|
|
48
|
+
for q in self._subscribers.get(key, []):
|
|
49
|
+
await q.put(value)
|
|
50
|
+
|
|
51
|
+
async def subscribe(self, key: str) -> AsyncIterator[bytes]:
|
|
52
|
+
"""Subscribe to changes for a key.
|
|
53
|
+
|
|
54
|
+
Example::
|
|
55
|
+
|
|
56
|
+
async for val in bb.subscribe("counter"):
|
|
57
|
+
print(val)
|
|
58
|
+
"""
|
|
59
|
+
q: asyncio.Queue[bytes] = asyncio.Queue()
|
|
60
|
+
self._subscribers.setdefault(key, []).append(q)
|
|
61
|
+
try:
|
|
62
|
+
while True:
|
|
63
|
+
yield await q.get()
|
|
64
|
+
finally:
|
|
65
|
+
self._subscribers[key].remove(q)
|
|
66
|
+
|
|
67
|
+
async def cas(self, key: str, expected: bytes, new: bytes) -> bool:
|
|
68
|
+
"""Compare-and-swap: update only if current value matches expected.
|
|
69
|
+
|
|
70
|
+
Example::
|
|
71
|
+
|
|
72
|
+
ok = await bb.cas("counter", b"42", b"43")
|
|
73
|
+
"""
|
|
74
|
+
current = self._store.get(key)
|
|
75
|
+
if current == expected:
|
|
76
|
+
self._store[key] = new
|
|
77
|
+
for q in self._subscribers.get(key, []):
|
|
78
|
+
await q.put(new)
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|