darwin-agentic-cloud 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.
@@ -0,0 +1,230 @@
1
+ """DAC HTTP server (FastAPI).
2
+
3
+ Exposes the runtime over HTTP so any client — curl, a remote agent,
4
+ another service — can request signed execution and query history.
5
+
6
+ Endpoints:
7
+ GET /healthz — liveness
8
+ GET /v0/identity — server's public key + substrate id
9
+ POST /v0/run — execute workload, return signed attestation
10
+ POST /v0/attestations/verify — verify a signed attestation
11
+ GET /v0/attestations — list recent attestations
12
+ GET /v0/attestations/{id} — fetch a specific attestation
13
+ GET /v0/attestations/stats — aggregate stats
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from typing import Any
20
+
21
+ from fastapi import FastAPI, HTTPException, Query
22
+ from pydantic import BaseModel, Field
23
+
24
+ from darwin import agenticcloud as dac
25
+ from darwin.agenticcloud.attestation import verify_attestation
26
+ from darwin.agenticcloud.runtime import Runtime
27
+ from darwin.agenticcloud.sandbox import SUBSTRATE_ID
28
+ from darwin.agenticcloud.signing import Signer
29
+ from darwin.agenticcloud.storage import AttestationStore
30
+ from darwin.agenticcloud.types import WorkloadSpec
31
+
32
+
33
+ # -------------------------------------------------------------------
34
+ # Schemas
35
+ # -------------------------------------------------------------------
36
+ class RunRequest(BaseModel):
37
+ code: str = Field(..., description="Source code to execute.")
38
+ language: str = Field("python", description="Language: 'python' or 'node'.")
39
+ inputs: dict = Field(default_factory=dict, description="Optional inputs.")
40
+ cost_cap_usd: float = Field(0.01, ge=0, description="Cost ceiling in USD.")
41
+ timeout_sec: int = Field(30, ge=1, le=600, description="Timeout in seconds.")
42
+ memory_mb: int = Field(512, ge=64, le=8192, description="Memory limit in MB.")
43
+
44
+
45
+ class RunResponse(BaseModel):
46
+ attestation: dict
47
+ signature_b64: str
48
+ public_key_b64: str
49
+
50
+
51
+ class VerifyRequest(BaseModel):
52
+ attestation: dict
53
+ signature_b64: str
54
+ public_key_b64: str
55
+
56
+
57
+ class VerifyResponse(BaseModel):
58
+ verified: bool
59
+
60
+
61
+ class IdentityResponse(BaseModel):
62
+ key_id: str
63
+ public_key_b64: str
64
+ substrate_id: str
65
+ schema: str
66
+ version: str
67
+
68
+
69
+ class AttestationSummary(BaseModel):
70
+ attestation_id: str
71
+ workload_id: str
72
+ signer_key_id: str
73
+ substrate_id: str
74
+ status: str
75
+ issued_at: float
76
+ cost_usd: float
77
+ wall_time_sec: float
78
+ schema_version: str
79
+
80
+
81
+ class AttestationListResponse(BaseModel):
82
+ attestations: list[AttestationSummary]
83
+ count: int
84
+
85
+
86
+ class AttestationStatsResponse(BaseModel):
87
+ total_count: int
88
+ total_cost_usd: float
89
+ by_status: dict[str, int]
90
+
91
+
92
+ # -------------------------------------------------------------------
93
+ # App factory
94
+ # -------------------------------------------------------------------
95
+ def create_app(runtime: Runtime | None = None) -> FastAPI:
96
+ """Build a FastAPI app with the given runtime (or a default one)."""
97
+ rt = runtime or Runtime()
98
+ store: AttestationStore = rt.store
99
+
100
+ app = FastAPI(
101
+ title="Darwin Agentic Cloud",
102
+ description="Verifiable compute for AI agents with cryptographically signed attestations.",
103
+ version=dac.__version__,
104
+ docs_url="/docs/swagger",
105
+ redoc_url="/docs/redoc",
106
+ )
107
+
108
+ @app.get("/healthz", tags=["health"])
109
+ async def healthz() -> dict[str, str]:
110
+ return {"status": "ok"}
111
+
112
+ @app.get("/v0/identity", response_model=IdentityResponse, tags=["identity"])
113
+ async def identity() -> IdentityResponse:
114
+ signer: Signer = rt.signer
115
+ return IdentityResponse(
116
+ key_id=signer.key_id(),
117
+ public_key_b64=signer.public_key_b64(),
118
+ substrate_id=SUBSTRATE_ID,
119
+ schema=dac.ATTESTATION_SCHEMA,
120
+ version=dac.__version__,
121
+ )
122
+
123
+ @app.post("/v0/run", response_model=RunResponse, tags=["run"])
124
+ async def run(req: RunRequest) -> RunResponse:
125
+ spec = WorkloadSpec(
126
+ code=req.code,
127
+ language=req.language,
128
+ inputs=req.inputs,
129
+ cost_cap_usd=req.cost_cap_usd,
130
+ timeout_sec=req.timeout_sec,
131
+ memory_mb=req.memory_mb,
132
+ )
133
+ signed = await asyncio.to_thread(rt.run, spec)
134
+ return RunResponse(
135
+ attestation=signed.attestation,
136
+ signature_b64=signed.signature_b64,
137
+ public_key_b64=signed.public_key_b64,
138
+ )
139
+
140
+ @app.post("/v0/attestations/verify", response_model=VerifyResponse, tags=["attestations"])
141
+ async def verify(req: VerifyRequest) -> VerifyResponse:
142
+ try:
143
+ payload: dict[str, Any] = {
144
+ "attestation": req.attestation,
145
+ "signature_b64": req.signature_b64,
146
+ "public_key_b64": req.public_key_b64,
147
+ }
148
+ return VerifyResponse(verified=verify_attestation(payload))
149
+ except Exception as e:
150
+ raise HTTPException(status_code=400, detail=str(e)) from e
151
+
152
+ @app.get(
153
+ "/v0/attestations/stats",
154
+ response_model=AttestationStatsResponse,
155
+ tags=["attestations"],
156
+ )
157
+ async def stats() -> AttestationStatsResponse:
158
+ total = store.count()
159
+ total_cost = store.total_cost_usd()
160
+ by_status = {
161
+ "ok": len(store.list_by_status("ok", limit=10**9)),
162
+ "error": len(store.list_by_status("error", limit=10**9)),
163
+ "timeout": len(store.list_by_status("timeout", limit=10**9)),
164
+ "oom": len(store.list_by_status("oom", limit=10**9)),
165
+ "cost_exceeded": len(store.list_by_status("cost_exceeded", limit=10**9)),
166
+ }
167
+ return AttestationStatsResponse(
168
+ total_count=total,
169
+ total_cost_usd=total_cost,
170
+ by_status=by_status,
171
+ )
172
+
173
+ @app.get(
174
+ "/v0/attestations",
175
+ response_model=AttestationListResponse,
176
+ tags=["attestations"],
177
+ )
178
+ async def list_attestations(
179
+ limit: int = Query(20, ge=1, le=1000),
180
+ status: str | None = Query(None),
181
+ ) -> AttestationListResponse:
182
+ rows = (
183
+ store.list_by_status(status, limit=limit) if status else store.list_recent(limit=limit)
184
+ )
185
+ return AttestationListResponse(
186
+ attestations=[
187
+ AttestationSummary(
188
+ attestation_id=r.attestation_id,
189
+ workload_id=r.workload_id,
190
+ signer_key_id=r.signer_key_id,
191
+ substrate_id=r.substrate_id,
192
+ status=r.status,
193
+ issued_at=r.issued_at,
194
+ cost_usd=r.cost_usd,
195
+ wall_time_sec=r.wall_time_sec,
196
+ schema_version=r.schema_version,
197
+ )
198
+ for r in rows
199
+ ],
200
+ count=len(rows),
201
+ )
202
+
203
+ @app.get("/v0/attestations/{attestation_id}", tags=["attestations"])
204
+ async def get_attestation(attestation_id: str) -> dict:
205
+ fetched = store.get(attestation_id)
206
+ if fetched is None:
207
+ raise HTTPException(status_code=404, detail="Attestation not found")
208
+ return fetched.signed_attestation
209
+
210
+ # Custom branded docs page (Material 3, dark, brand colors)
211
+ from pathlib import Path as _Path
212
+
213
+ from fastapi.responses import HTMLResponse
214
+
215
+ _TEMPLATE_PATH = _Path(__file__).parent / "templates" / "docs.html"
216
+
217
+ @app.get("/docs", response_class=HTMLResponse, include_in_schema=False)
218
+ async def custom_docs() -> HTMLResponse:
219
+ """Serve the branded Darwin docs page."""
220
+ return HTMLResponse(_TEMPLATE_PATH.read_text(encoding="utf-8"))
221
+
222
+ @app.get("/", response_class=HTMLResponse, include_in_schema=False)
223
+ async def root() -> HTMLResponse:
224
+ """Root: redirect to the docs page."""
225
+ return HTMLResponse(_TEMPLATE_PATH.read_text(encoding="utf-8"))
226
+
227
+ return app
228
+
229
+
230
+ app = create_app()
@@ -0,0 +1,100 @@
1
+ """Ed25519 signing for DAC attestations.
2
+
3
+ Stores a local keypair at ~/.darwin/agenticcloud/keys/signing.pem. In production this
4
+ gets swapped for Sigstore + OIDC, but the Signer interface stays the
5
+ same so callers don't change.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ from pathlib import Path
12
+
13
+ from cryptography.hazmat.primitives import serialization
14
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
15
+ Ed25519PrivateKey,
16
+ Ed25519PublicKey,
17
+ )
18
+
19
+ from darwin.agenticcloud.hashing import sha256_hex
20
+
21
+
22
+ def _default_key_dir() -> Path:
23
+ """Default keys directory, honoring DARWIN_STATE_DIR env var."""
24
+ import os
25
+
26
+ state_dir = os.environ.get("DARWIN_STATE_DIR")
27
+ if state_dir:
28
+ return Path(state_dir) / "keys"
29
+ return Path.home() / ".darwin" / "agenticcloud" / "keys"
30
+
31
+
32
+ DEFAULT_KEY_DIR = _default_key_dir()
33
+ DEFAULT_KEY_PATH = DEFAULT_KEY_DIR / "signing.pem"
34
+
35
+
36
+ class Signer:
37
+ """Local Ed25519 signer."""
38
+
39
+ def __init__(self, key_path: Path | None = None) -> None:
40
+ self.key_path = key_path or DEFAULT_KEY_PATH
41
+ self._private_key = self._load_or_create()
42
+ self._public_key = self._private_key.public_key()
43
+
44
+ def _load_or_create(self) -> Ed25519PrivateKey:
45
+ if self.key_path.exists():
46
+ return self._load()
47
+ return self._create()
48
+
49
+ def _load(self) -> Ed25519PrivateKey:
50
+ with self.key_path.open("rb") as f:
51
+ key = serialization.load_pem_private_key(f.read(), password=None)
52
+ if not isinstance(key, Ed25519PrivateKey):
53
+ raise TypeError(f"Expected Ed25519 key at {self.key_path}, got {type(key).__name__}")
54
+ return key
55
+
56
+ def _create(self) -> Ed25519PrivateKey:
57
+ self.key_path.parent.mkdir(parents=True, exist_ok=True)
58
+ key = Ed25519PrivateKey.generate()
59
+ pem = key.private_bytes(
60
+ encoding=serialization.Encoding.PEM,
61
+ format=serialization.PrivateFormat.PKCS8,
62
+ encryption_algorithm=serialization.NoEncryption(),
63
+ )
64
+ self.key_path.write_bytes(pem)
65
+ self.key_path.chmod(0o600)
66
+ return key
67
+
68
+ def _public_key_raw(self) -> bytes:
69
+ return self._public_key.public_bytes(
70
+ encoding=serialization.Encoding.Raw,
71
+ format=serialization.PublicFormat.Raw,
72
+ )
73
+
74
+ def public_key_b64(self) -> str:
75
+ """Base64-encoded raw Ed25519 public key (32 bytes -> 44 chars)."""
76
+ return base64.b64encode(self._public_key_raw()).decode()
77
+
78
+ def key_id(self) -> str:
79
+ """Stable identifier for this signing key.
80
+
81
+ Format: dac-local-<first 16 hex chars of sha256(public_key)>
82
+ """
83
+ return "dac-local-" + sha256_hex(self._public_key_raw())[:16]
84
+
85
+ def sign(self, data: bytes) -> str:
86
+ """Sign data, return base64-encoded signature."""
87
+ sig = self._private_key.sign(data)
88
+ return base64.b64encode(sig).decode()
89
+
90
+
91
+ def verify_signature(data: bytes, signature_b64: str, public_key_b64: str) -> bool:
92
+ """Verify a base64-encoded signature against base64 public key."""
93
+ try:
94
+ sig = base64.b64decode(signature_b64)
95
+ pub_raw = base64.b64decode(public_key_b64)
96
+ pub = Ed25519PublicKey.from_public_bytes(pub_raw)
97
+ pub.verify(sig, data)
98
+ except Exception:
99
+ return False
100
+ return True
@@ -0,0 +1,230 @@
1
+ """Persistent storage of attestations.
2
+
3
+ Every signed attestation is written to a local SQLite database so it
4
+ can be queried and audited later. This is what turns DAC from a
5
+ stateless RPC into an evidentiary system of record.
6
+
7
+ Schema is intentionally minimal for v0:
8
+ - attestations: the full signed payload, plus indexed columns we'd
9
+ reasonably query by (issued_at, signer_key_id, status, workload_id,
10
+ substrate_id, attestation_id).
11
+
12
+ The 'signed_attestation_json' column holds the entire SignedAttestation
13
+ as JSON so verification is independent of schema changes — you can
14
+ always re-verify a stored attestation by deserializing that column.
15
+
16
+ In v0.2 we'll add a SQLAlchemy-backed Postgres implementation behind
17
+ the same AttestationStore interface for multi-tenant deployments.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import sqlite3
24
+ from collections.abc import Iterator
25
+ from contextlib import contextmanager
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+
29
+ from darwin.agenticcloud.types import SignedAttestation
30
+
31
+
32
+ def _default_db_path() -> Path:
33
+ """Return the default attestation DB path, honoring DARWIN_STATE_DIR."""
34
+ import os
35
+
36
+ state_dir = os.environ.get("DARWIN_STATE_DIR")
37
+ if state_dir:
38
+ return Path(state_dir) / "attestations.db"
39
+ return Path.home() / ".darwin" / "agenticcloud" / "attestations.db"
40
+
41
+
42
+ DEFAULT_DB_PATH = _default_db_path()
43
+
44
+ SCHEMA = """
45
+ CREATE TABLE IF NOT EXISTS attestations (
46
+ attestation_id TEXT PRIMARY KEY,
47
+ workload_id TEXT NOT NULL,
48
+ signer_key_id TEXT NOT NULL,
49
+ substrate_id TEXT NOT NULL,
50
+ status TEXT NOT NULL,
51
+ issued_at REAL NOT NULL,
52
+ cost_usd REAL NOT NULL,
53
+ wall_time_sec REAL NOT NULL,
54
+ schema_version TEXT NOT NULL,
55
+ signed_attestation_json TEXT NOT NULL
56
+ );
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_attestations_issued_at
59
+ ON attestations(issued_at DESC);
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_attestations_workload_id
62
+ ON attestations(workload_id);
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_attestations_signer_key_id
65
+ ON attestations(signer_key_id);
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_attestations_status
68
+ ON attestations(status);
69
+ """
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class StoredAttestation:
74
+ """A row in the attestations table, returned by queries."""
75
+
76
+ attestation_id: str
77
+ workload_id: str
78
+ signer_key_id: str
79
+ substrate_id: str
80
+ status: str
81
+ issued_at: float
82
+ cost_usd: float
83
+ wall_time_sec: float
84
+ schema_version: str
85
+ signed_attestation: dict # the full SignedAttestation re-hydrated
86
+
87
+
88
+ class AttestationStore:
89
+ """SQLite-backed attestation store.
90
+
91
+ Thread-safe at the connection level via SQLite's serialized access.
92
+ For higher concurrency in v0.2 we move to a connection pool or
93
+ swap the backend to Postgres.
94
+ """
95
+
96
+ def __init__(self, db_path: Path | None = None) -> None:
97
+ self.db_path = db_path or DEFAULT_DB_PATH
98
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
99
+ self._init_schema()
100
+
101
+ def _init_schema(self) -> None:
102
+ with self._connect() as conn:
103
+ conn.executescript(SCHEMA)
104
+
105
+ @contextmanager
106
+ def _connect(self) -> Iterator[sqlite3.Connection]:
107
+ conn = sqlite3.connect(self.db_path, isolation_level=None)
108
+ conn.row_factory = sqlite3.Row
109
+ try:
110
+ yield conn
111
+ finally:
112
+ conn.close()
113
+
114
+ # -------------------------------------------------------------
115
+ # Writes
116
+ # -------------------------------------------------------------
117
+ def save(self, signed: SignedAttestation) -> None:
118
+ """Persist a signed attestation. Idempotent on attestation_id."""
119
+ a = signed.attestation
120
+ er = a["execution_result"]
121
+ signed_json = json.dumps(
122
+ {
123
+ "attestation": signed.attestation,
124
+ "signature_b64": signed.signature_b64,
125
+ "public_key_b64": signed.public_key_b64,
126
+ }
127
+ )
128
+ with self._connect() as conn:
129
+ conn.execute(
130
+ """
131
+ INSERT OR REPLACE INTO attestations (
132
+ attestation_id, workload_id, signer_key_id, substrate_id,
133
+ status, issued_at, cost_usd, wall_time_sec,
134
+ schema_version, signed_attestation_json
135
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
136
+ """,
137
+ (
138
+ a["attestation_id"],
139
+ er["workload_id"],
140
+ a["signer_key_id"],
141
+ er["substrate_id"],
142
+ er["status"],
143
+ a["issued_at"],
144
+ er["cost_usd"],
145
+ er["wall_time_sec"],
146
+ a["schema"],
147
+ signed_json,
148
+ ),
149
+ )
150
+
151
+ # -------------------------------------------------------------
152
+ # Reads
153
+ # -------------------------------------------------------------
154
+ def get(self, attestation_id: str) -> StoredAttestation | None:
155
+ """Fetch a single attestation by ID. Returns None if not found."""
156
+ with self._connect() as conn:
157
+ row = conn.execute(
158
+ "SELECT * FROM attestations WHERE attestation_id = ?",
159
+ (attestation_id,),
160
+ ).fetchone()
161
+ return _row_to_stored(row) if row else None
162
+
163
+ def list_recent(self, limit: int = 50) -> list[StoredAttestation]:
164
+ """Return the most recent attestations, newest first."""
165
+ with self._connect() as conn:
166
+ rows = conn.execute(
167
+ "SELECT * FROM attestations ORDER BY issued_at DESC LIMIT ?",
168
+ (limit,),
169
+ ).fetchall()
170
+ return [_row_to_stored(r) for r in rows]
171
+
172
+ def list_by_status(self, status: str, limit: int = 50) -> list[StoredAttestation]:
173
+ """Return attestations with a given status (e.g. 'cost_exceeded')."""
174
+ with self._connect() as conn:
175
+ rows = conn.execute(
176
+ """
177
+ SELECT * FROM attestations
178
+ WHERE status = ?
179
+ ORDER BY issued_at DESC
180
+ LIMIT ?
181
+ """,
182
+ (status, limit),
183
+ ).fetchall()
184
+ return [_row_to_stored(r) for r in rows]
185
+
186
+ def list_by_signer(self, signer_key_id: str, limit: int = 50) -> list[StoredAttestation]:
187
+ """Return attestations issued by a given signer."""
188
+ with self._connect() as conn:
189
+ rows = conn.execute(
190
+ """
191
+ SELECT * FROM attestations
192
+ WHERE signer_key_id = ?
193
+ ORDER BY issued_at DESC
194
+ LIMIT ?
195
+ """,
196
+ (signer_key_id, limit),
197
+ ).fetchall()
198
+ return [_row_to_stored(r) for r in rows]
199
+
200
+ def count(self) -> int:
201
+ """Total number of stored attestations."""
202
+ with self._connect() as conn:
203
+ return conn.execute("SELECT COUNT(*) FROM attestations").fetchone()[0]
204
+
205
+ def total_cost_usd(self, signer_key_id: str | None = None) -> float:
206
+ """Sum of cost_usd across all (or one signer's) attestations."""
207
+ with self._connect() as conn:
208
+ if signer_key_id is None:
209
+ row = conn.execute("SELECT COALESCE(SUM(cost_usd), 0) FROM attestations").fetchone()
210
+ else:
211
+ row = conn.execute(
212
+ "SELECT COALESCE(SUM(cost_usd), 0) FROM attestations WHERE signer_key_id = ?",
213
+ (signer_key_id,),
214
+ ).fetchone()
215
+ return float(row[0])
216
+
217
+
218
+ def _row_to_stored(row: sqlite3.Row) -> StoredAttestation:
219
+ return StoredAttestation(
220
+ attestation_id=row["attestation_id"],
221
+ workload_id=row["workload_id"],
222
+ signer_key_id=row["signer_key_id"],
223
+ substrate_id=row["substrate_id"],
224
+ status=row["status"],
225
+ issued_at=row["issued_at"],
226
+ cost_usd=row["cost_usd"],
227
+ wall_time_sec=row["wall_time_sec"],
228
+ schema_version=row["schema_version"],
229
+ signed_attestation=json.loads(row["signed_attestation_json"]),
230
+ )