beeq 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.
- beeq-1.0.0/PKG-INFO +130 -0
- beeq-1.0.0/README.md +89 -0
- beeq-1.0.0/beeq/__init__.py +22 -0
- beeq-1.0.0/beeq/_vendor/__init__.py +3 -0
- beeq-1.0.0/beeq/_vendor/aap/__init__.py +25 -0
- beeq-1.0.0/beeq/_vendor/aap/audit.py +160 -0
- beeq-1.0.0/beeq/_vendor/aap/authorization.py +149 -0
- beeq-1.0.0/beeq/_vendor/aap/exceptions.py +57 -0
- beeq-1.0.0/beeq/_vendor/aap/identity.py +197 -0
- beeq-1.0.0/beeq/_vendor/aap/provenance.py +107 -0
- beeq-1.0.0/beeq/cli/__init__.py +0 -0
- beeq-1.0.0/beeq/cli/main.py +297 -0
- beeq-1.0.0/beeq/core.py +165 -0
- beeq-1.0.0/beeq/dashboard/__init__.py +1 -0
- beeq-1.0.0/beeq/dashboard/server.py +595 -0
- beeq-1.0.0/beeq/governance/__init__.py +1 -0
- beeq-1.0.0/beeq/governance/hive.py +142 -0
- beeq-1.0.0/beeq/mcp/__init__.py +3 -0
- beeq-1.0.0/beeq/mcp/client.py +75 -0
- beeq-1.0.0/beeq/mcp/server.py +111 -0
- beeq-1.0.0/beeq/providers/__init__.py +12 -0
- beeq-1.0.0/beeq/providers/base.py +41 -0
- beeq-1.0.0/beeq/providers/claude.py +33 -0
- beeq-1.0.0/beeq/providers/manager.py +66 -0
- beeq-1.0.0/beeq/providers/ollama.py +48 -0
- beeq-1.0.0/beeq/providers/openai_p.py +42 -0
- beeq-1.0.0/beeq/queen/__init__.py +1 -0
- beeq-1.0.0/beeq/queen/queen.py +164 -0
- beeq-1.0.0/beeq/workers/__init__.py +11 -0
- beeq-1.0.0/beeq/workers/base.py +81 -0
- beeq-1.0.0/beeq/workers/code.py +128 -0
- beeq-1.0.0/beeq/workers/hardware.py +37 -0
- beeq-1.0.0/beeq/workers/memory.py +269 -0
- beeq-1.0.0/beeq/workers/ops.py +176 -0
- beeq-1.0.0/beeq/workers/research.py +134 -0
- beeq-1.0.0/beeq/workers/writer.py +104 -0
- beeq-1.0.0/install.sh +89 -0
- beeq-1.0.0/pyproject.toml +74 -0
- beeq-1.0.0/tests/__init__.py +0 -0
- beeq-1.0.0/tests/conftest.py +4 -0
- beeq-1.0.0/tests/test_beeq.py +187 -0
beeq-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beeq
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The first AI hive. One curl. One queen. An army that works.
|
|
5
|
+
Project-URL: Homepage, https://beeq.run
|
|
6
|
+
Project-URL: Documentation, https://beeq.run/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/ElmadaniS/beeq
|
|
8
|
+
Project-URL: Verify actions, https://beeq.run/verify/
|
|
9
|
+
Author-email: Elmadani SALKA <Elmadani.SALKA@proton.me>
|
|
10
|
+
License: BSL-1.1
|
|
11
|
+
Keywords: aap,agents,ai,beeq,hive,llm,orchestration,queen
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
20
|
+
Requires-Dist: anthropic>=0.25.0
|
|
21
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
22
|
+
Requires-Dist: click>=8.1.0
|
|
23
|
+
Requires-Dist: cryptography>=42.0.0
|
|
24
|
+
Requires-Dist: fastapi>=0.111.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: mcp>=1.0.0
|
|
27
|
+
Requires-Dist: ollama>=0.2.0
|
|
28
|
+
Requires-Dist: openai>=1.30.0
|
|
29
|
+
Requires-Dist: pydantic>=2.7.0
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
31
|
+
Requires-Dist: rich>=13.7.0
|
|
32
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
33
|
+
Requires-Dist: websockets>=12.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: build; extra == 'dev'
|
|
36
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: twine; extra == 'dev'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# BeeQ
|
|
43
|
+
|
|
44
|
+
**The first AI hive. One curl. One queen. An army that works.**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -fsSL https://beeq.run/install | bash
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
BeeQ Hive Status
|
|
52
|
+
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
|
|
53
|
+
┃ Component ┃ Status ┃
|
|
54
|
+
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
|
|
55
|
+
│ Queen │ ready │
|
|
56
|
+
│ Audit chain │ valid — 0 entries │
|
|
57
|
+
│ Worker-research │ active │
|
|
58
|
+
│ Worker-code │ active │
|
|
59
|
+
│ Worker-write │ active │
|
|
60
|
+
│ Worker-ops │ active │
|
|
61
|
+
└─────────────────┴───────────────────┘
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Every action signed. Every action traceable.
|
|
65
|
+
Every action reversible. You remain sovereign.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Option 1 — one curl (recommended)
|
|
73
|
+
curl -fsSL https://beeq.run/install | bash
|
|
74
|
+
|
|
75
|
+
# Option 2 — pip
|
|
76
|
+
pip install beeq
|
|
77
|
+
beeq connect ollama # or: beeq connect claude --key sk-ant-...
|
|
78
|
+
beeq start
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
beeq start # start your hive interactively
|
|
85
|
+
beeq mission "prepare my weekly report" # single mission
|
|
86
|
+
beeq status # hive status
|
|
87
|
+
beeq audit # view full audit chain
|
|
88
|
+
beeq revoke research # revoke a worker (LEX §5)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## What makes BeeQ different
|
|
92
|
+
|
|
93
|
+
| | Others | BeeQ |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Agent cryptographic identity | ✗ | ✅ AAP |
|
|
96
|
+
| Signed delegation chain | ✗ | ✅ scope(worker) ⊂ scope(queen) |
|
|
97
|
+
| Tamper-evident audit | ✗ | ✅ hash chain |
|
|
98
|
+
| Physical world (robots/IoT) | ✗ | ✅ NRP |
|
|
99
|
+
| Physical World Rule | ✗ | ✅ Level 4 forbidden |
|
|
100
|
+
| LLM-agnostic | partial | ✅ Claude/OpenAI/Ollama/GGUF |
|
|
101
|
+
| Local-first | ✗ | ✅ your data never leaves |
|
|
102
|
+
| Single curl install | partial | ✅ |
|
|
103
|
+
|
|
104
|
+
## Architecture
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
HUMAN (sovereign — gives mission, validates decisions)
|
|
108
|
+
↓
|
|
109
|
+
QUEEN (orchestrates — decomposes, delegates, reports)
|
|
110
|
+
↓
|
|
111
|
+
WORKERS (execute within signed AAP scope)
|
|
112
|
+
├─ research web · APIs · documents
|
|
113
|
+
├─ code files · git · execution
|
|
114
|
+
├─ write emails · articles · reports
|
|
115
|
+
└─ ops infrastructure · monitoring
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Governed by
|
|
119
|
+
|
|
120
|
+
- **LEX** — 11 civilizational laws (MIT)
|
|
121
|
+
- **PHY** — 7 physical laws for embodied agents (MIT)
|
|
122
|
+
- **AAP** — Agent Accountability Protocol (MIT)
|
|
123
|
+
- **NRP** — Node Reach Protocol for physical world (MIT)
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
BSL-1.1 — Free for individuals, research, open source.
|
|
128
|
+
Commercial use requires a license.
|
|
129
|
+
|
|
130
|
+
See [beeq.run](https://beeq.run) for details.
|
beeq-1.0.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# BeeQ
|
|
2
|
+
|
|
3
|
+
**The first AI hive. One curl. One queen. An army that works.**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
curl -fsSL https://beeq.run/install | bash
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
BeeQ Hive Status
|
|
11
|
+
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
|
|
12
|
+
┃ Component ┃ Status ┃
|
|
13
|
+
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
|
|
14
|
+
│ Queen │ ready │
|
|
15
|
+
│ Audit chain │ valid — 0 entries │
|
|
16
|
+
│ Worker-research │ active │
|
|
17
|
+
│ Worker-code │ active │
|
|
18
|
+
│ Worker-write │ active │
|
|
19
|
+
│ Worker-ops │ active │
|
|
20
|
+
└─────────────────┴───────────────────┘
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every action signed. Every action traceable.
|
|
24
|
+
Every action reversible. You remain sovereign.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Option 1 — one curl (recommended)
|
|
32
|
+
curl -fsSL https://beeq.run/install | bash
|
|
33
|
+
|
|
34
|
+
# Option 2 — pip
|
|
35
|
+
pip install beeq
|
|
36
|
+
beeq connect ollama # or: beeq connect claude --key sk-ant-...
|
|
37
|
+
beeq start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
beeq start # start your hive interactively
|
|
44
|
+
beeq mission "prepare my weekly report" # single mission
|
|
45
|
+
beeq status # hive status
|
|
46
|
+
beeq audit # view full audit chain
|
|
47
|
+
beeq revoke research # revoke a worker (LEX §5)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What makes BeeQ different
|
|
51
|
+
|
|
52
|
+
| | Others | BeeQ |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Agent cryptographic identity | ✗ | ✅ AAP |
|
|
55
|
+
| Signed delegation chain | ✗ | ✅ scope(worker) ⊂ scope(queen) |
|
|
56
|
+
| Tamper-evident audit | ✗ | ✅ hash chain |
|
|
57
|
+
| Physical world (robots/IoT) | ✗ | ✅ NRP |
|
|
58
|
+
| Physical World Rule | ✗ | ✅ Level 4 forbidden |
|
|
59
|
+
| LLM-agnostic | partial | ✅ Claude/OpenAI/Ollama/GGUF |
|
|
60
|
+
| Local-first | ✗ | ✅ your data never leaves |
|
|
61
|
+
| Single curl install | partial | ✅ |
|
|
62
|
+
|
|
63
|
+
## Architecture
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
HUMAN (sovereign — gives mission, validates decisions)
|
|
67
|
+
↓
|
|
68
|
+
QUEEN (orchestrates — decomposes, delegates, reports)
|
|
69
|
+
↓
|
|
70
|
+
WORKERS (execute within signed AAP scope)
|
|
71
|
+
├─ research web · APIs · documents
|
|
72
|
+
├─ code files · git · execution
|
|
73
|
+
├─ write emails · articles · reports
|
|
74
|
+
└─ ops infrastructure · monitoring
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Governed by
|
|
78
|
+
|
|
79
|
+
- **LEX** — 11 civilizational laws (MIT)
|
|
80
|
+
- **PHY** — 7 physical laws for embodied agents (MIT)
|
|
81
|
+
- **AAP** — Agent Accountability Protocol (MIT)
|
|
82
|
+
- **NRP** — Node Reach Protocol for physical world (MIT)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
BSL-1.1 — Free for individuals, research, open source.
|
|
87
|
+
Commercial use requires a license.
|
|
88
|
+
|
|
89
|
+
See [beeq.run](https://beeq.run) for details.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""BeeQ — The First AI Hive."""
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# Ensure vendored AAP is available if not installed globally
|
|
6
|
+
_vendor = os.path.join(os.path.dirname(__file__), "_vendor")
|
|
7
|
+
if _vendor not in sys.path:
|
|
8
|
+
sys.path.insert(0, _vendor)
|
|
9
|
+
|
|
10
|
+
# Verify AAP is importable
|
|
11
|
+
try:
|
|
12
|
+
import aap # noqa: F401
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"BeeQ requires AAP (Agent Accountability Protocol). "
|
|
16
|
+
"Please run: pip install aap-protocol\n"
|
|
17
|
+
"Or it should be bundled — check your installation."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "1.0.0"
|
|
21
|
+
__author__ = "Elmadani SALKA"
|
|
22
|
+
__url__ = "https://beeq.run"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AAP — Agent Accountability Protocol
|
|
3
|
+
Python SDK v0.1 — Reference Implementation
|
|
4
|
+
|
|
5
|
+
https://aap-protocol.dev
|
|
6
|
+
License: MIT
|
|
7
|
+
"""
|
|
8
|
+
from .identity import AAPIdentity, AAPKeyPair
|
|
9
|
+
from .authorization import AAPAuthorization, AuthorizationLevel
|
|
10
|
+
from .provenance import AAPProvenance
|
|
11
|
+
from .audit import AAPAuditChain, AAPAuditEntry
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
AAPError, AAPValidationError, AAPSignatureError,
|
|
14
|
+
AAPPhysicalWorldViolation, AAPScopeError, AAPRevocationError
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AAPIdentity", "AAPKeyPair",
|
|
20
|
+
"AAPAuthorization", "AuthorizationLevel",
|
|
21
|
+
"AAPProvenance",
|
|
22
|
+
"AAPAuditChain", "AAPAuditEntry",
|
|
23
|
+
"AAPError", "AAPValidationError", "AAPSignatureError",
|
|
24
|
+
"AAPPhysicalWorldViolation", "AAPScopeError", "AAPRevocationError",
|
|
25
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""AAP AuditChain — tamper-evident append-only log of all agent actions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib, json, uuid
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
from .exceptions import AAPChainError, AAPValidationError
|
|
11
|
+
from .identity import _signable, _verify_signature, AAPKeyPair
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _hash_entry(entry_dict: dict) -> str:
|
|
15
|
+
canonical = json.dumps(entry_dict, sort_keys=True, separators=(",", ":")).encode()
|
|
16
|
+
return f"sha256:{hashlib.sha256(canonical).hexdigest()}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class AAPAuditEntry:
|
|
21
|
+
"""A single tamper-evident entry in the audit chain."""
|
|
22
|
+
AAP_VERSION: ClassVar[str] = "0.1"
|
|
23
|
+
|
|
24
|
+
entry_id: str
|
|
25
|
+
prev_hash: str # sha256:... or "genesis"
|
|
26
|
+
agent_id: str
|
|
27
|
+
action: str
|
|
28
|
+
result: str # success | failure | blocked | revoked
|
|
29
|
+
timestamp: datetime
|
|
30
|
+
provenance_id: str
|
|
31
|
+
signature: str
|
|
32
|
+
authorization_level: int = 0
|
|
33
|
+
physical: bool = False
|
|
34
|
+
result_detail: str | None = None
|
|
35
|
+
|
|
36
|
+
VALID_RESULTS: ClassVar[frozenset] = frozenset({"success", "failure", "blocked", "revoked"})
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
if self.result not in self.VALID_RESULTS:
|
|
40
|
+
raise AAPValidationError(
|
|
41
|
+
f"Invalid result: '{self.result}'. Must be one of: {self.VALID_RESULTS}",
|
|
42
|
+
field="result"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> dict:
|
|
46
|
+
d = {
|
|
47
|
+
"aap_version": self.AAP_VERSION,
|
|
48
|
+
"entry_id": self.entry_id,
|
|
49
|
+
"prev_hash": self.prev_hash,
|
|
50
|
+
"agent_id": self.agent_id,
|
|
51
|
+
"action": self.action,
|
|
52
|
+
"result": self.result,
|
|
53
|
+
"timestamp": self.timestamp.isoformat(),
|
|
54
|
+
"provenance_id": self.provenance_id,
|
|
55
|
+
"authorization_level": self.authorization_level,
|
|
56
|
+
"physical": self.physical,
|
|
57
|
+
"signature": self.signature,
|
|
58
|
+
}
|
|
59
|
+
if self.result_detail:
|
|
60
|
+
d["result_detail"] = self.result_detail
|
|
61
|
+
return d
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: dict) -> "AAPAuditEntry":
|
|
65
|
+
return cls(
|
|
66
|
+
entry_id=data["entry_id"],
|
|
67
|
+
prev_hash=data["prev_hash"],
|
|
68
|
+
agent_id=data["agent_id"],
|
|
69
|
+
action=data["action"],
|
|
70
|
+
result=data["result"],
|
|
71
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
72
|
+
provenance_id=data["provenance_id"],
|
|
73
|
+
signature=data["signature"],
|
|
74
|
+
authorization_level=data.get("authorization_level", 0),
|
|
75
|
+
physical=data.get("physical", False),
|
|
76
|
+
result_detail=data.get("result_detail"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def verify_signature(self, agent_public_key: str) -> None:
|
|
80
|
+
_verify_signature(agent_public_key, _signable(self.to_dict()), self.signature)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AAPAuditChain:
|
|
84
|
+
"""
|
|
85
|
+
Tamper-evident audit chain.
|
|
86
|
+
Append-only. Each entry hashes the previous.
|
|
87
|
+
Tampering any entry breaks all subsequent hashes.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, storage_path: Path | None = None) -> None:
|
|
91
|
+
self._entries: list[AAPAuditEntry] = []
|
|
92
|
+
self._storage_path = storage_path
|
|
93
|
+
if storage_path and storage_path.exists():
|
|
94
|
+
self._load(storage_path)
|
|
95
|
+
|
|
96
|
+
def append(
|
|
97
|
+
self,
|
|
98
|
+
agent_id: str,
|
|
99
|
+
action: str,
|
|
100
|
+
result: str,
|
|
101
|
+
provenance_id: str,
|
|
102
|
+
agent_keypair: AAPKeyPair,
|
|
103
|
+
authorization_level: int = 0,
|
|
104
|
+
physical: bool = False,
|
|
105
|
+
result_detail: str | None = None,
|
|
106
|
+
) -> AAPAuditEntry:
|
|
107
|
+
"""Append a new entry. Signs it with the agent's key."""
|
|
108
|
+
prev_hash = self._last_hash()
|
|
109
|
+
entry = AAPAuditEntry(
|
|
110
|
+
entry_id=str(uuid.uuid4()),
|
|
111
|
+
prev_hash=prev_hash,
|
|
112
|
+
agent_id=agent_id,
|
|
113
|
+
action=action,
|
|
114
|
+
result=result,
|
|
115
|
+
timestamp=datetime.now(timezone.utc),
|
|
116
|
+
provenance_id=provenance_id,
|
|
117
|
+
signature="",
|
|
118
|
+
authorization_level=authorization_level,
|
|
119
|
+
physical=physical,
|
|
120
|
+
result_detail=result_detail,
|
|
121
|
+
)
|
|
122
|
+
doc = entry.to_dict()
|
|
123
|
+
entry.signature = agent_keypair.sign(_signable(doc))
|
|
124
|
+
self._entries.append(entry)
|
|
125
|
+
if self._storage_path:
|
|
126
|
+
self._persist(entry)
|
|
127
|
+
return entry
|
|
128
|
+
|
|
129
|
+
def verify(self) -> tuple[bool, int, str | None]:
|
|
130
|
+
"""
|
|
131
|
+
Verify chain integrity.
|
|
132
|
+
Returns: (valid, entries_checked, broken_at_entry_id)
|
|
133
|
+
"""
|
|
134
|
+
prev_hash = "genesis"
|
|
135
|
+
for i, entry in enumerate(self._entries):
|
|
136
|
+
expected_prev = prev_hash
|
|
137
|
+
if entry.prev_hash != expected_prev:
|
|
138
|
+
return False, i, entry.entry_id
|
|
139
|
+
prev_hash = _hash_entry(entry.to_dict())
|
|
140
|
+
return True, len(self._entries), None
|
|
141
|
+
|
|
142
|
+
def _last_hash(self) -> str:
|
|
143
|
+
if not self._entries:
|
|
144
|
+
return "genesis"
|
|
145
|
+
return _hash_entry(self._entries[-1].to_dict())
|
|
146
|
+
|
|
147
|
+
def to_list(self) -> list[dict]:
|
|
148
|
+
return [e.to_dict() for e in self._entries]
|
|
149
|
+
|
|
150
|
+
def __len__(self) -> int:
|
|
151
|
+
return len(self._entries)
|
|
152
|
+
|
|
153
|
+
def _persist(self, entry: AAPAuditEntry) -> None:
|
|
154
|
+
with open(self._storage_path, "a") as f:
|
|
155
|
+
f.write(json.dumps(entry.to_dict()) + "\n")
|
|
156
|
+
|
|
157
|
+
def _load(self, path: Path) -> None:
|
|
158
|
+
for line in path.read_text().splitlines():
|
|
159
|
+
if line.strip():
|
|
160
|
+
self._entries.append(AAPAuditEntry.from_dict(json.loads(line)))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""AAP Authorization — human-granted permission for an agent to act."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from enum import IntEnum
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
|
|
11
|
+
from .exceptions import AAPPhysicalWorldViolation, AAPValidationError, AAPRevocationError
|
|
12
|
+
from .identity import _signable, _verify_signature, AAPKeyPair
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthorizationLevel(IntEnum):
|
|
16
|
+
OBSERVE = 0 # Read-only. No side effects.
|
|
17
|
+
SUGGEST = 1 # Propose actions. Human executes.
|
|
18
|
+
ASSISTED = 2 # Agent executes. Human confirms each action.
|
|
19
|
+
SUPERVISED = 3 # Agent executes. Human can intervene.
|
|
20
|
+
AUTONOMOUS = 4 # Agent executes within scope. Full audit required.
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name_str(self) -> str:
|
|
24
|
+
return self.name.lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
PHYSICAL_MAX_LEVEL = AuthorizationLevel.SUPERVISED
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class AAPAuthorization:
|
|
32
|
+
"""
|
|
33
|
+
Authorization token granting an agent permission to act.
|
|
34
|
+
Signed by a human supervisor.
|
|
35
|
+
|
|
36
|
+
PHYSICAL WORLD RULE: Level 4 (Autonomous) is FORBIDDEN for physical nodes.
|
|
37
|
+
This is enforced at construction time. It is not configurable.
|
|
38
|
+
"""
|
|
39
|
+
AAP_VERSION: ClassVar[str] = "0.1"
|
|
40
|
+
|
|
41
|
+
agent_id: str
|
|
42
|
+
level: AuthorizationLevel
|
|
43
|
+
scope: list[str]
|
|
44
|
+
granted_by: str # did:key:z...
|
|
45
|
+
granted_at: datetime
|
|
46
|
+
signature: str
|
|
47
|
+
physical: bool = False
|
|
48
|
+
expires_at: datetime | None = None
|
|
49
|
+
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
50
|
+
_revoked: bool = field(default=False, init=False, repr=False)
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
self._enforce_physical_world_rule()
|
|
54
|
+
|
|
55
|
+
def _enforce_physical_world_rule(self) -> None:
|
|
56
|
+
"""
|
|
57
|
+
PHYSICAL WORLD RULE: Autonomous (Level 4) is forbidden for physical nodes.
|
|
58
|
+
This rule cannot be bypassed, overridden, or configured away.
|
|
59
|
+
"""
|
|
60
|
+
if self.physical and self.level > PHYSICAL_MAX_LEVEL:
|
|
61
|
+
raise AAPPhysicalWorldViolation(self.agent_id)
|
|
62
|
+
|
|
63
|
+
def revoke(self) -> None:
|
|
64
|
+
"""Revoke this authorization."""
|
|
65
|
+
self._revoked = True
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def is_revoked(self) -> bool:
|
|
69
|
+
return self._revoked
|
|
70
|
+
|
|
71
|
+
def is_expired(self) -> bool:
|
|
72
|
+
if not self.expires_at:
|
|
73
|
+
return False
|
|
74
|
+
return datetime.now(timezone.utc) > self.expires_at
|
|
75
|
+
|
|
76
|
+
def is_valid(self) -> bool:
|
|
77
|
+
return not self.is_revoked and not self.is_expired()
|
|
78
|
+
|
|
79
|
+
def check(self) -> None:
|
|
80
|
+
"""Raise if this authorization is not valid."""
|
|
81
|
+
if self.is_revoked:
|
|
82
|
+
raise AAPRevocationError(f"Authorization '{self.session_id}' has been revoked")
|
|
83
|
+
if self.is_expired():
|
|
84
|
+
raise AAPRevocationError(f"Authorization '{self.session_id}' has expired")
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> dict:
|
|
87
|
+
d = {
|
|
88
|
+
"aap_version": self.AAP_VERSION,
|
|
89
|
+
"agent_id": self.agent_id,
|
|
90
|
+
"level": int(self.level),
|
|
91
|
+
"level_name": self.level.name_str,
|
|
92
|
+
"scope": self.scope,
|
|
93
|
+
"physical": self.physical,
|
|
94
|
+
"granted_by": self.granted_by,
|
|
95
|
+
"granted_at": self.granted_at.isoformat(),
|
|
96
|
+
"session_id": self.session_id,
|
|
97
|
+
"signature": self.signature,
|
|
98
|
+
}
|
|
99
|
+
if self.expires_at:
|
|
100
|
+
d["expires_at"] = self.expires_at.isoformat()
|
|
101
|
+
return d
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dict(cls, data: dict) -> "AAPAuthorization":
|
|
105
|
+
required = {"aap_version", "agent_id", "level", "scope", "granted_by", "granted_at", "signature"}
|
|
106
|
+
missing = required - set(data.keys())
|
|
107
|
+
if missing:
|
|
108
|
+
raise AAPValidationError(f"Missing required fields: {missing}")
|
|
109
|
+
return cls(
|
|
110
|
+
agent_id=data["agent_id"],
|
|
111
|
+
level=AuthorizationLevel(data["level"]),
|
|
112
|
+
scope=data["scope"],
|
|
113
|
+
physical=data.get("physical", False),
|
|
114
|
+
granted_by=data["granted_by"],
|
|
115
|
+
granted_at=datetime.fromisoformat(data["granted_at"]),
|
|
116
|
+
signature=data["signature"],
|
|
117
|
+
session_id=data.get("session_id", str(uuid.uuid4())),
|
|
118
|
+
expires_at=datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def verify_signature(self, supervisor_public_key: str) -> None:
|
|
122
|
+
_verify_signature(supervisor_public_key, _signable(self.to_dict()), self.signature)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def create(
|
|
126
|
+
cls,
|
|
127
|
+
agent_id: str,
|
|
128
|
+
level: AuthorizationLevel,
|
|
129
|
+
scope: list[str],
|
|
130
|
+
supervisor_keypair: AAPKeyPair,
|
|
131
|
+
supervisor_did: str,
|
|
132
|
+
physical: bool = False,
|
|
133
|
+
expires_at: datetime | None = None,
|
|
134
|
+
) -> "AAPAuthorization":
|
|
135
|
+
"""Create and sign a new authorization. Raises AAPPhysicalWorldViolation if applicable."""
|
|
136
|
+
now = datetime.now(timezone.utc)
|
|
137
|
+
auth = cls(
|
|
138
|
+
agent_id=agent_id,
|
|
139
|
+
level=level,
|
|
140
|
+
scope=scope,
|
|
141
|
+
physical=physical,
|
|
142
|
+
granted_by=supervisor_did,
|
|
143
|
+
granted_at=now,
|
|
144
|
+
signature="",
|
|
145
|
+
expires_at=expires_at,
|
|
146
|
+
)
|
|
147
|
+
doc = auth.to_dict()
|
|
148
|
+
auth.signature = supervisor_keypair.sign(_signable(doc))
|
|
149
|
+
return auth
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""AAP exception hierarchy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AAPError(Exception):
|
|
5
|
+
"""Base class for all AAP errors."""
|
|
6
|
+
code: str = "AAP-000"
|
|
7
|
+
|
|
8
|
+
def __init__(self, message: str, field: str | None = None, detail: str | None = None):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.field = field
|
|
11
|
+
self.detail = detail
|
|
12
|
+
|
|
13
|
+
def to_dict(self) -> dict:
|
|
14
|
+
d = {"code": self.code, "message": str(self)}
|
|
15
|
+
if self.field:
|
|
16
|
+
d["field"] = self.field
|
|
17
|
+
if self.detail:
|
|
18
|
+
d["detail"] = self.detail
|
|
19
|
+
return d
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AAPValidationError(AAPError):
|
|
23
|
+
"""Schema validation failed."""
|
|
24
|
+
code = "AAP-001"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AAPSignatureError(AAPError):
|
|
28
|
+
"""Cryptographic signature is invalid."""
|
|
29
|
+
code = "AAP-002"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AAPPhysicalWorldViolation(AAPError):
|
|
33
|
+
"""Physical World Rule violation: Level 4 forbidden for physical nodes."""
|
|
34
|
+
code = "AAP-003"
|
|
35
|
+
|
|
36
|
+
def __init__(self, agent_id: str):
|
|
37
|
+
super().__init__(
|
|
38
|
+
f"Physical World Rule: Autonomous (Level 4) is forbidden for physical agent '{agent_id}'. "
|
|
39
|
+
f"Maximum level for physical nodes is Supervised (Level 3). "
|
|
40
|
+
f"This rule is not configurable.",
|
|
41
|
+
field="level"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AAPScopeError(AAPError):
|
|
46
|
+
"""Action is outside the agent's authorized scope."""
|
|
47
|
+
code = "AAP-004"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AAPRevocationError(AAPError):
|
|
51
|
+
"""Identity or authorization has been revoked."""
|
|
52
|
+
code = "AAP-005"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AAPChainError(AAPError):
|
|
56
|
+
"""Audit chain integrity violation."""
|
|
57
|
+
code = "AAP-006"
|