nest-plugins-reference 0.1.0__py3-none-any.whl
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/__init__.py +4 -0
- nest_plugins_reference/auth/__init__.py +1 -0
- nest_plugins_reference/auth/jwt_auth.py +102 -0
- nest_plugins_reference/comms/__init__.py +1 -0
- nest_plugins_reference/comms/nest_native.py +114 -0
- nest_plugins_reference/coordination/__init__.py +1 -0
- nest_plugins_reference/coordination/contract_net.py +95 -0
- nest_plugins_reference/datafacts/__init__.py +1 -0
- nest_plugins_reference/datafacts/datafacts_v1.py +78 -0
- nest_plugins_reference/identity/__init__.py +1 -0
- nest_plugins_reference/identity/did_key.py +101 -0
- nest_plugins_reference/memory/__init__.py +1 -0
- nest_plugins_reference/memory/blackboard.py +80 -0
- nest_plugins_reference/negotiation/__init__.py +1 -0
- nest_plugins_reference/negotiation/alternating_offers.py +98 -0
- nest_plugins_reference/payments/__init__.py +1 -0
- nest_plugins_reference/payments/prepaid_credits.py +99 -0
- nest_plugins_reference/privacy/__init__.py +1 -0
- nest_plugins_reference/privacy/noop.py +59 -0
- nest_plugins_reference/py.typed +0 -0
- nest_plugins_reference/registry/__init__.py +1 -0
- nest_plugins_reference/registry/in_memory.py +87 -0
- nest_plugins_reference/transport/__init__.py +1 -0
- nest_plugins_reference/transport/in_memory.py +111 -0
- nest_plugins_reference/trust/__init__.py +1 -0
- nest_plugins_reference/trust/score_average.py +93 -0
- nest_plugins_reference-0.1.0.dist-info/METADATA +32 -0
- nest_plugins_reference-0.1.0.dist-info/RECORD +29 -0
- nest_plugins_reference-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Alternating offers negotiation plugin — Rubinstein-style bargaining.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
neg = AlternatingOffers(AgentId("a1"), patience=0.9)
|
|
7
|
+
session = await neg.open(AgentId("a2"), Terms(price=Money(amount=100)))
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import uuid
|
|
13
|
+
|
|
14
|
+
from nest_core.types import (
|
|
15
|
+
AgentId,
|
|
16
|
+
Agreement,
|
|
17
|
+
NegotiationResponse,
|
|
18
|
+
NegotiationSession,
|
|
19
|
+
NegotiationStatus,
|
|
20
|
+
Terms,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AlternatingOffers:
|
|
25
|
+
"""Rubinstein-style alternating-offers negotiation.
|
|
26
|
+
|
|
27
|
+
Example::
|
|
28
|
+
|
|
29
|
+
neg = AlternatingOffers(AgentId("a1"))
|
|
30
|
+
session = await neg.open(AgentId("a2"), terms)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, agent_id: AgentId, patience: float = 0.9) -> None:
|
|
34
|
+
self._agent_id = agent_id
|
|
35
|
+
self._patience = patience
|
|
36
|
+
self._sessions: dict[str, NegotiationSession] = {}
|
|
37
|
+
|
|
38
|
+
async def open(self, partner: AgentId, terms: Terms) -> NegotiationSession:
|
|
39
|
+
"""Open a negotiation with initial terms.
|
|
40
|
+
|
|
41
|
+
Example::
|
|
42
|
+
|
|
43
|
+
session = await neg.open(AgentId("a2"), terms)
|
|
44
|
+
"""
|
|
45
|
+
session = NegotiationSession(
|
|
46
|
+
id=str(uuid.uuid4()),
|
|
47
|
+
initiator=self._agent_id,
|
|
48
|
+
partner=partner,
|
|
49
|
+
status=NegotiationStatus.OPEN,
|
|
50
|
+
current_terms=terms,
|
|
51
|
+
history=[terms],
|
|
52
|
+
)
|
|
53
|
+
self._sessions[session.id] = session
|
|
54
|
+
return session
|
|
55
|
+
|
|
56
|
+
async def offer(self, session: NegotiationSession, terms: Terms) -> None:
|
|
57
|
+
"""Make a counter-offer.
|
|
58
|
+
|
|
59
|
+
Example::
|
|
60
|
+
|
|
61
|
+
await neg.offer(session, Terms(price=Money(amount=80)))
|
|
62
|
+
"""
|
|
63
|
+
session.current_terms = terms
|
|
64
|
+
session.history.append(terms)
|
|
65
|
+
|
|
66
|
+
async def respond(self, session: NegotiationSession) -> NegotiationResponse:
|
|
67
|
+
"""Respond to the current offer using the patience discount.
|
|
68
|
+
|
|
69
|
+
Example::
|
|
70
|
+
|
|
71
|
+
resp = await neg.respond(session)
|
|
72
|
+
"""
|
|
73
|
+
if session.current_terms is None or session.current_terms.price is None:
|
|
74
|
+
return NegotiationResponse(accepted=True)
|
|
75
|
+
|
|
76
|
+
rounds = len(session.history)
|
|
77
|
+
threshold = session.current_terms.price.amount * (self._patience**rounds)
|
|
78
|
+
if session.current_terms.price.amount <= threshold or rounds >= 10:
|
|
79
|
+
return NegotiationResponse(accepted=True)
|
|
80
|
+
|
|
81
|
+
return NegotiationResponse(accepted=False, counter_terms=session.current_terms)
|
|
82
|
+
|
|
83
|
+
async def close(self, session: NegotiationSession) -> Agreement | None:
|
|
84
|
+
"""Close a session, returning an agreement if both parties accepted.
|
|
85
|
+
|
|
86
|
+
Example::
|
|
87
|
+
|
|
88
|
+
agreement = await neg.close(session)
|
|
89
|
+
"""
|
|
90
|
+
if session.status == NegotiationStatus.AGREED or session.current_terms is not None:
|
|
91
|
+
session.status = NegotiationStatus.AGREED
|
|
92
|
+
return Agreement(
|
|
93
|
+
session_id=session.id,
|
|
94
|
+
terms=session.current_terms or Terms(),
|
|
95
|
+
parties=[session.initiator, session.partner],
|
|
96
|
+
)
|
|
97
|
+
session.status = NegotiationStatus.REJECTED
|
|
98
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Prepaid credits payment plugin — simple debit/credit ledger.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
payments = PrepaidCredits(AgentId("a1"), initial_balance=1000)
|
|
7
|
+
receipt = await payments.pay(AgentId("a2"), Money(amount=50), PaymentRef("p1"))
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from nest_core.types import (
|
|
13
|
+
AgentId,
|
|
14
|
+
Money,
|
|
15
|
+
PaymentRef,
|
|
16
|
+
PaymentStatus,
|
|
17
|
+
Quote,
|
|
18
|
+
Receipt,
|
|
19
|
+
ServiceRef,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PrepaidCredits:
|
|
24
|
+
"""Simple debit/credit ledger for payments.
|
|
25
|
+
|
|
26
|
+
Example::
|
|
27
|
+
|
|
28
|
+
pay = PrepaidCredits(AgentId("a1"), initial_balance=1000)
|
|
29
|
+
receipt = await pay.pay(AgentId("a2"), Money(amount=50), PaymentRef("p1"))
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, agent_id: AgentId, initial_balance: int = 1000) -> None:
|
|
33
|
+
self._agent_id = agent_id
|
|
34
|
+
self._balances: dict[AgentId, int] = {agent_id: initial_balance}
|
|
35
|
+
self._payments: dict[PaymentRef, Receipt] = {}
|
|
36
|
+
|
|
37
|
+
def balance(self, agent: AgentId) -> int:
|
|
38
|
+
"""Check an agent's balance.
|
|
39
|
+
|
|
40
|
+
Example::
|
|
41
|
+
|
|
42
|
+
bal = pay.balance(AgentId("a1"))
|
|
43
|
+
"""
|
|
44
|
+
return self._balances.get(agent, 0)
|
|
45
|
+
|
|
46
|
+
async def quote(self, service: ServiceRef) -> Quote:
|
|
47
|
+
"""Return a fixed quote for any service.
|
|
48
|
+
|
|
49
|
+
Example::
|
|
50
|
+
|
|
51
|
+
q = await pay.quote(ServiceRef("svc"))
|
|
52
|
+
"""
|
|
53
|
+
return Quote(service=service, price=Money(amount=10))
|
|
54
|
+
|
|
55
|
+
async def pay(self, to: AgentId, amount: Money, ref: PaymentRef) -> Receipt:
|
|
56
|
+
"""Execute a payment from this agent to another.
|
|
57
|
+
|
|
58
|
+
Example::
|
|
59
|
+
|
|
60
|
+
receipt = await pay.pay(AgentId("a2"), Money(amount=50), PaymentRef("p1"))
|
|
61
|
+
"""
|
|
62
|
+
payer_balance = self._balances.get(self._agent_id, 0)
|
|
63
|
+
if payer_balance < amount.amount:
|
|
64
|
+
msg = f"Insufficient balance: {payer_balance} < {amount.amount}"
|
|
65
|
+
raise ValueError(msg)
|
|
66
|
+
|
|
67
|
+
self._balances[self._agent_id] = payer_balance - amount.amount
|
|
68
|
+
self._balances[to] = self._balances.get(to, 0) + amount.amount
|
|
69
|
+
|
|
70
|
+
receipt = Receipt(ref=ref, payer=self._agent_id, payee=to, amount=amount)
|
|
71
|
+
self._payments[ref] = receipt
|
|
72
|
+
return receipt
|
|
73
|
+
|
|
74
|
+
async def verify_payment(self, ref: PaymentRef) -> PaymentStatus:
|
|
75
|
+
"""Verify a payment status by reference.
|
|
76
|
+
|
|
77
|
+
Example::
|
|
78
|
+
|
|
79
|
+
status = await pay.verify_payment(PaymentRef("p1"))
|
|
80
|
+
"""
|
|
81
|
+
if ref in self._payments:
|
|
82
|
+
return PaymentStatus.CONFIRMED
|
|
83
|
+
return PaymentStatus.FAILED
|
|
84
|
+
|
|
85
|
+
async def refund(self, ref: PaymentRef) -> None:
|
|
86
|
+
"""Refund a payment.
|
|
87
|
+
|
|
88
|
+
Example::
|
|
89
|
+
|
|
90
|
+
await pay.refund(PaymentRef("p1"))
|
|
91
|
+
"""
|
|
92
|
+
receipt = self._payments.get(ref)
|
|
93
|
+
if receipt is None:
|
|
94
|
+
msg = f"Payment not found: {ref}"
|
|
95
|
+
raise ValueError(msg)
|
|
96
|
+
|
|
97
|
+
self._balances[receipt.payee] = self._balances.get(receipt.payee, 0) - receipt.amount.amount
|
|
98
|
+
self._balances[receipt.payer] = self._balances.get(receipt.payer, 0) + receipt.amount.amount
|
|
99
|
+
del self._payments[ref]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Noop privacy plugin — transparent passthrough, no actual encryption.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
priv = NoopPrivacy()
|
|
7
|
+
ct = await priv.encrypt(b"data", [AgentId("a1")])
|
|
8
|
+
assert ct == b"data"
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from nest_core.types import AgentId, Proof, Statement, Witness
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NoopPrivacy:
|
|
17
|
+
"""Transparent passthrough — no encryption, mock proofs always valid.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
priv = NoopPrivacy()
|
|
22
|
+
ct = await priv.encrypt(b"data", [AgentId("a1")])
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
async def encrypt(self, data: bytes, audience: list[AgentId]) -> bytes:
|
|
26
|
+
"""Return data unchanged (no encryption).
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
ct = await priv.encrypt(b"secret", [AgentId("a1")])
|
|
31
|
+
"""
|
|
32
|
+
return data
|
|
33
|
+
|
|
34
|
+
async def decrypt(self, data: bytes) -> bytes:
|
|
35
|
+
"""Return data unchanged (no decryption).
|
|
36
|
+
|
|
37
|
+
Example::
|
|
38
|
+
|
|
39
|
+
pt = await priv.decrypt(ct)
|
|
40
|
+
"""
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
async def prove(self, statement: Statement, witness: Witness) -> Proof:
|
|
44
|
+
"""Generate a mock proof (always valid).
|
|
45
|
+
|
|
46
|
+
Example::
|
|
47
|
+
|
|
48
|
+
proof = await priv.prove(stmt, witness)
|
|
49
|
+
"""
|
|
50
|
+
return Proof(statement=statement, data=b"mock-proof", scheme="noop")
|
|
51
|
+
|
|
52
|
+
async def verify_proof(self, statement: Statement, proof: Proof) -> bool:
|
|
53
|
+
"""Verify a mock proof (always True).
|
|
54
|
+
|
|
55
|
+
Example::
|
|
56
|
+
|
|
57
|
+
ok = await priv.verify_proof(stmt, proof)
|
|
58
|
+
"""
|
|
59
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""In-memory registry plugin — local dictionary-based agent discovery.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
registry = InMemoryRegistry()
|
|
7
|
+
await registry.register(card)
|
|
8
|
+
results = await registry.lookup(Query(capabilities=["sell"]))
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
from collections.abc import AsyncIterator
|
|
15
|
+
|
|
16
|
+
from nest_core.types import AgentCard, AgentId, Query
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InMemoryRegistry:
|
|
20
|
+
"""Dictionary-backed agent registry.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
reg = InMemoryRegistry()
|
|
25
|
+
await reg.register(AgentCard(agent_id=AgentId("a1"), name="Agent1"))
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._cards: dict[AgentId, AgentCard] = {}
|
|
30
|
+
self._subscribers: list[asyncio.Queue[AgentCard]] = []
|
|
31
|
+
|
|
32
|
+
async def register(self, card: AgentCard) -> None:
|
|
33
|
+
"""Register an agent card.
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
await reg.register(card)
|
|
38
|
+
"""
|
|
39
|
+
self._cards[card.agent_id] = card
|
|
40
|
+
for q in self._subscribers:
|
|
41
|
+
await q.put(card)
|
|
42
|
+
|
|
43
|
+
async def lookup(self, query: Query) -> list[AgentCard]:
|
|
44
|
+
"""Look up agents matching a query.
|
|
45
|
+
|
|
46
|
+
Example::
|
|
47
|
+
|
|
48
|
+
results = await reg.lookup(Query(capabilities=["sell"]))
|
|
49
|
+
"""
|
|
50
|
+
results: list[AgentCard] = []
|
|
51
|
+
for card in self._cards.values():
|
|
52
|
+
if self._matches(card, query):
|
|
53
|
+
results.append(card)
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
async def subscribe(self, query: Query) -> AsyncIterator[AgentCard]:
|
|
57
|
+
"""Subscribe to new agent registrations matching a query.
|
|
58
|
+
|
|
59
|
+
Example::
|
|
60
|
+
|
|
61
|
+
async for card in reg.subscribe(query):
|
|
62
|
+
print(card.name)
|
|
63
|
+
"""
|
|
64
|
+
q: asyncio.Queue[AgentCard] = asyncio.Queue()
|
|
65
|
+
self._subscribers.append(q)
|
|
66
|
+
try:
|
|
67
|
+
while True:
|
|
68
|
+
card = await q.get()
|
|
69
|
+
if self._matches(card, query):
|
|
70
|
+
yield card
|
|
71
|
+
finally:
|
|
72
|
+
self._subscribers.remove(q)
|
|
73
|
+
|
|
74
|
+
async def deregister(self, agent: AgentId) -> None:
|
|
75
|
+
"""Remove an agent from the registry.
|
|
76
|
+
|
|
77
|
+
Example::
|
|
78
|
+
|
|
79
|
+
await reg.deregister(AgentId("a1"))
|
|
80
|
+
"""
|
|
81
|
+
self._cards.pop(agent, None)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _matches(card: AgentCard, query: Query) -> bool:
|
|
85
|
+
if query.capabilities and not all(cap in card.capabilities for cap in query.capabilities):
|
|
86
|
+
return False
|
|
87
|
+
return not (query.name_pattern and query.name_pattern not in card.name)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""In-memory transport plugin — standalone version for non-simulator use.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
transport = StandaloneInMemoryTransport(AgentId("a1"), network)
|
|
7
|
+
await transport.send(AgentId("a2"), b"hello")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
|
|
14
|
+
from nest_core.types import AgentId, TransportCapabilities
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InMemoryNetwork:
|
|
18
|
+
"""Shared network that routes messages between in-memory transports.
|
|
19
|
+
|
|
20
|
+
Example::
|
|
21
|
+
|
|
22
|
+
network = InMemoryNetwork()
|
|
23
|
+
t1 = StandaloneInMemoryTransport(AgentId("a1"), network)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._queues: dict[AgentId, asyncio.Queue[tuple[AgentId, bytes]]] = {}
|
|
28
|
+
self._agents: list[AgentId] = []
|
|
29
|
+
|
|
30
|
+
def register(self, agent_id: AgentId) -> asyncio.Queue[tuple[AgentId, bytes]]:
|
|
31
|
+
"""Register an agent and return its message queue.
|
|
32
|
+
|
|
33
|
+
Example::
|
|
34
|
+
|
|
35
|
+
queue = network.register(AgentId("a1"))
|
|
36
|
+
"""
|
|
37
|
+
q: asyncio.Queue[tuple[AgentId, bytes]] = asyncio.Queue()
|
|
38
|
+
self._queues[agent_id] = q
|
|
39
|
+
self._agents.append(agent_id)
|
|
40
|
+
return q
|
|
41
|
+
|
|
42
|
+
def get_agents(self) -> list[AgentId]:
|
|
43
|
+
"""Return all registered agent IDs.
|
|
44
|
+
|
|
45
|
+
Example::
|
|
46
|
+
|
|
47
|
+
agents = network.get_agents()
|
|
48
|
+
"""
|
|
49
|
+
return list(self._agents)
|
|
50
|
+
|
|
51
|
+
async def deliver(self, sender: AgentId, to: AgentId, payload: bytes) -> None:
|
|
52
|
+
"""Deliver a message to the target agent's queue.
|
|
53
|
+
|
|
54
|
+
Example::
|
|
55
|
+
|
|
56
|
+
await network.deliver(AgentId("a1"), AgentId("a2"), b"hi")
|
|
57
|
+
"""
|
|
58
|
+
q = self._queues.get(to)
|
|
59
|
+
if q is not None:
|
|
60
|
+
await q.put((sender, payload))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StandaloneInMemoryTransport:
|
|
64
|
+
"""In-memory transport for use outside the simulator (e.g., Tier 2).
|
|
65
|
+
|
|
66
|
+
Example::
|
|
67
|
+
|
|
68
|
+
network = InMemoryNetwork()
|
|
69
|
+
transport = StandaloneInMemoryTransport(AgentId("a1"), network)
|
|
70
|
+
await transport.send(AgentId("a2"), b"hello")
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
capabilities = TransportCapabilities(
|
|
74
|
+
supports_streaming=False,
|
|
75
|
+
ordered=True,
|
|
76
|
+
reliable=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __init__(self, agent_id: AgentId, network: InMemoryNetwork) -> None:
|
|
80
|
+
self._agent_id = agent_id
|
|
81
|
+
self._network = network
|
|
82
|
+
self._queue = network.register(agent_id)
|
|
83
|
+
|
|
84
|
+
async def send(self, to: AgentId, payload: bytes) -> None:
|
|
85
|
+
"""Send a payload to a specific agent.
|
|
86
|
+
|
|
87
|
+
Example::
|
|
88
|
+
|
|
89
|
+
await transport.send(AgentId("a2"), b"hello")
|
|
90
|
+
"""
|
|
91
|
+
await self._network.deliver(self._agent_id, to, payload)
|
|
92
|
+
|
|
93
|
+
async def receive(self) -> tuple[AgentId, bytes]:
|
|
94
|
+
"""Wait for the next message and return (sender, payload).
|
|
95
|
+
|
|
96
|
+
Example::
|
|
97
|
+
|
|
98
|
+
sender, data = await transport.receive()
|
|
99
|
+
"""
|
|
100
|
+
return await self._queue.get()
|
|
101
|
+
|
|
102
|
+
async def broadcast(self, payload: bytes) -> None:
|
|
103
|
+
"""Broadcast to all agents on the network.
|
|
104
|
+
|
|
105
|
+
Example::
|
|
106
|
+
|
|
107
|
+
await transport.broadcast(b"announcement")
|
|
108
|
+
"""
|
|
109
|
+
for aid in self._network.get_agents():
|
|
110
|
+
if aid != self._agent_id:
|
|
111
|
+
await self.send(aid, payload)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Score-average trust plugin — running mean of feedback scores.
|
|
3
|
+
|
|
4
|
+
Example::
|
|
5
|
+
|
|
6
|
+
trust = ScoreAverageTrust(identity)
|
|
7
|
+
await trust.report(AgentId("a1"), evidence)
|
|
8
|
+
score = await trust.score(AgentId("a1"))
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from nest_core.types import (
|
|
16
|
+
AgentId,
|
|
17
|
+
Attestation,
|
|
18
|
+
Claim,
|
|
19
|
+
Evidence,
|
|
20
|
+
ReputationScore,
|
|
21
|
+
Signature,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ScoreAverageTrust:
|
|
26
|
+
"""Running-mean reputation based on reported feedback.
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
trust = ScoreAverageTrust()
|
|
31
|
+
score = await trust.score(AgentId("a1"))
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, identity: Any = None) -> None:
|
|
35
|
+
self._identity = identity
|
|
36
|
+
self._scores: dict[AgentId, list[float]] = {}
|
|
37
|
+
self._stakes: dict[AgentId, int] = {}
|
|
38
|
+
|
|
39
|
+
async def score(self, agent: AgentId) -> ReputationScore:
|
|
40
|
+
"""Get the running-mean reputation score for an agent.
|
|
41
|
+
|
|
42
|
+
Example::
|
|
43
|
+
|
|
44
|
+
rep = await trust.score(AgentId("a1"))
|
|
45
|
+
"""
|
|
46
|
+
entries = self._scores.get(agent, [])
|
|
47
|
+
if not entries:
|
|
48
|
+
return ReputationScore(agent_id=agent, score=0.5, confidence=0.0, sample_count=0)
|
|
49
|
+
avg = sum(entries) / len(entries)
|
|
50
|
+
confidence = min(1.0, len(entries) / 100.0)
|
|
51
|
+
return ReputationScore(
|
|
52
|
+
agent_id=agent,
|
|
53
|
+
score=avg,
|
|
54
|
+
confidence=confidence,
|
|
55
|
+
sample_count=len(entries),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def attest(self, agent: AgentId, claim: Claim) -> Attestation:
|
|
59
|
+
"""Create an attestation about an agent.
|
|
60
|
+
|
|
61
|
+
Example::
|
|
62
|
+
|
|
63
|
+
att = await trust.attest(AgentId("a1"), claim)
|
|
64
|
+
"""
|
|
65
|
+
sig = Signature(signer=AgentId("system"), value=b"attestation", algorithm="none")
|
|
66
|
+
if self._identity is not None:
|
|
67
|
+
sig = self._identity.sign(claim.model_dump_json().encode())
|
|
68
|
+
return Attestation(issuer=AgentId("system"), claim=claim, signature=sig)
|
|
69
|
+
|
|
70
|
+
async def report(self, agent: AgentId, evidence: Evidence) -> None:
|
|
71
|
+
"""Report evidence, updating the agent's score.
|
|
72
|
+
|
|
73
|
+
Evidence kind 'positive' adds 1.0, 'negative' adds 0.0, 'byzantine' adds 0.0.
|
|
74
|
+
|
|
75
|
+
Example::
|
|
76
|
+
|
|
77
|
+
await trust.report(AgentId("a1"), Evidence(reporter=..., subject=..., kind="negative"))
|
|
78
|
+
"""
|
|
79
|
+
score_val = 0.5
|
|
80
|
+
if evidence.kind == "positive":
|
|
81
|
+
score_val = 1.0
|
|
82
|
+
elif evidence.kind in ("negative", "byzantine"):
|
|
83
|
+
score_val = 0.0
|
|
84
|
+
self._scores.setdefault(agent, []).append(score_val)
|
|
85
|
+
|
|
86
|
+
async def stake(self, agent: AgentId, amount: int) -> None:
|
|
87
|
+
"""Stake reputation on an agent.
|
|
88
|
+
|
|
89
|
+
Example::
|
|
90
|
+
|
|
91
|
+
await trust.stake(AgentId("a1"), 100)
|
|
92
|
+
"""
|
|
93
|
+
self._stakes[agent] = self._stakes.get(agent, 0) + amount
|
|
@@ -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,29 @@
|
|
|
1
|
+
nest_plugins_reference/__init__.py,sha256=JqKQjeEjHA1_vRzgRj0ZHR4QeW1U1tnrbljaAP22uMI,134
|
|
2
|
+
nest_plugins_reference/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
nest_plugins_reference/auth/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
4
|
+
nest_plugins_reference/auth/jwt_auth.py,sha256=Zcy_XCwxzaHfTDaOwcB-U1LsVcDDmjoRgW3ZOwKnypo,2736
|
|
5
|
+
nest_plugins_reference/comms/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
6
|
+
nest_plugins_reference/comms/nest_native.py,sha256=cQsavxpCus8tNu3VL2zWO-wSWVMUzzinIKr9LpJbXCU,3092
|
|
7
|
+
nest_plugins_reference/coordination/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
8
|
+
nest_plugins_reference/coordination/contract_net.py,sha256=gc48YV7jB4FECRikOppHGJ63La-tSq42_iUvqe53tIA,2454
|
|
9
|
+
nest_plugins_reference/datafacts/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
10
|
+
nest_plugins_reference/datafacts/datafacts_v1.py,sha256=igPD46EOJZFgHMS8cru38HpKDaNMzG0LFr2qpiEoIow,2255
|
|
11
|
+
nest_plugins_reference/identity/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
12
|
+
nest_plugins_reference/identity/did_key.py,sha256=6q2ypKi47iXJ5ydF9gtk7fOD0ihlxSsBh57oJpEuaXY,3055
|
|
13
|
+
nest_plugins_reference/memory/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
14
|
+
nest_plugins_reference/memory/blackboard.py,sha256=kbeYgoDelLfU5b_03PzAl7-jjDq5RkweoSFN2VGBVEQ,2085
|
|
15
|
+
nest_plugins_reference/negotiation/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
16
|
+
nest_plugins_reference/negotiation/alternating_offers.py,sha256=mbLbQhTyI7PZg2R_PL0Bmc8R2JcgTToOGfAlXaqKBoQ,3055
|
|
17
|
+
nest_plugins_reference/payments/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
18
|
+
nest_plugins_reference/payments/prepaid_credits.py,sha256=FmfY6T13U-7GoT4D_OEOOA7OGSl5dududnZHfvsH4xw,3020
|
|
19
|
+
nest_plugins_reference/privacy/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
20
|
+
nest_plugins_reference/privacy/noop.py,sha256=xBXRFkQ3ZLR9lAkPScxBO3hbZFFfGLqDojF7h7_qtLA,1495
|
|
21
|
+
nest_plugins_reference/registry/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
22
|
+
nest_plugins_reference/registry/in_memory.py,sha256=IRuHfgxS3TeiscI04nGWbWzrUqOYAEwVh-UfamF9ajs,2472
|
|
23
|
+
nest_plugins_reference/transport/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
24
|
+
nest_plugins_reference/transport/in_memory.py,sha256=seH6etRlnduW3YVwUTWMA7ZJxnXMTcUEbchMIYMrCBA,3116
|
|
25
|
+
nest_plugins_reference/trust/__init__.py,sha256=bG7Ffen4LvQtgnYPFEpFccsWs81t4zqqeqn9ZeirH6E,38
|
|
26
|
+
nest_plugins_reference/trust/score_average.py,sha256=spzqxBsCW8lWKF5wnxymTdjz2Xf5-f5sxDdTkjqz9Kk,2792
|
|
27
|
+
nest_plugins_reference-0.1.0.dist-info/METADATA,sha256=PPlhFGFSOxXs7WscPg1pZsSBiShFfchP8-u1APJ2hpc,1135
|
|
28
|
+
nest_plugins_reference-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
29
|
+
nest_plugins_reference-0.1.0.dist-info/RECORD,,
|