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.
darwin/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Darwin — adaptive software systems.
2
+
3
+ A family of modules:
4
+ darwin.agenticcloud — verifiable compute for AI agents
5
+ (more to come)
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "Vladimir J Edouard"
10
+ __license__ = "Apache-2.0"
@@ -0,0 +1,7 @@
1
+ """Darwin Agentic Cloud."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "Vladimir J Edouard"
5
+ __license__ = "Apache-2.0"
6
+
7
+ ATTESTATION_SCHEMA = "darwin.cloud/agenticcloud/attestation/v0.1"
@@ -0,0 +1,77 @@
1
+ """Build and verify signed attestations.
2
+
3
+ An attestation is the cryptographic proof that a workload ran. It binds
4
+ together: the workload that was requested, the result that was produced,
5
+ the substrate that ran it, the cost, and the signer's identity.
6
+
7
+ The signature covers the entire attestation payload. Any tampering with
8
+ any field breaks verification.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ import uuid
15
+ from dataclasses import asdict
16
+
17
+ from darwin.agenticcloud import ATTESTATION_SCHEMA
18
+ from darwin.agenticcloud.hashing import canonical_json, content_hash
19
+ from darwin.agenticcloud.signing import Signer, verify_signature
20
+ from darwin.agenticcloud.types import Attestation, ExecutionResult, SignedAttestation, WorkloadSpec
21
+
22
+
23
+ def build_signed_attestation(
24
+ spec: WorkloadSpec,
25
+ result: ExecutionResult,
26
+ signer: Signer,
27
+ ) -> SignedAttestation:
28
+ """Build a signed attestation from a workload spec and execution result."""
29
+ spec_dict = asdict(spec)
30
+ result_dict = asdict(result)
31
+
32
+ attestation = Attestation(
33
+ schema=ATTESTATION_SCHEMA,
34
+ attestation_id=str(uuid.uuid4()),
35
+ workload_spec_hash=content_hash(spec_dict),
36
+ workload_spec=spec_dict,
37
+ execution_result=result_dict,
38
+ signer_key_id=signer.key_id(),
39
+ issued_at=time.time(),
40
+ )
41
+
42
+ attestation_dict = asdict(attestation)
43
+ canonical = canonical_json(attestation_dict)
44
+
45
+ return SignedAttestation(
46
+ attestation=attestation_dict,
47
+ signature_b64=signer.sign(canonical),
48
+ public_key_b64=signer.public_key_b64(),
49
+ )
50
+
51
+
52
+ def verify_attestation(signed: SignedAttestation | dict) -> bool:
53
+ """Verify a signed attestation.
54
+
55
+ Returns True if the signature is valid for the attestation payload
56
+ under the included public key. Returns False on any tampering or
57
+ malformed input.
58
+
59
+ Accepts either a SignedAttestation dataclass or a dict (as produced
60
+ by serializing one — e.g. from JSON).
61
+ """
62
+ if isinstance(signed, SignedAttestation):
63
+ attestation = signed.attestation
64
+ signature_b64 = signed.signature_b64
65
+ public_key_b64 = signed.public_key_b64
66
+ elif isinstance(signed, dict):
67
+ try:
68
+ attestation = signed["attestation"]
69
+ signature_b64 = signed["signature_b64"]
70
+ public_key_b64 = signed["public_key_b64"]
71
+ except KeyError:
72
+ return False
73
+ else:
74
+ return False
75
+
76
+ canonical = canonical_json(attestation)
77
+ return verify_signature(canonical, signature_b64, public_key_b64)
@@ -0,0 +1,502 @@
1
+ """Darwin Agentic Cloud command-line interface.
2
+
3
+ Examples:
4
+ darwin run hello.py
5
+ darwin run hello.py --timeout 30 --memory 256
6
+ darwin keys show
7
+ darwin attest verify ./attestation.json
8
+ darwin serve
9
+ darwin mcp serve
10
+ darwin history list
11
+ darwin history stats
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from datetime import UTC
18
+ from pathlib import Path
19
+ from typing import Annotated
20
+
21
+ import typer
22
+ from rich.console import Console
23
+ from rich.panel import Panel
24
+ from rich.table import Table
25
+
26
+ from darwin.agenticcloud.attestation import verify_attestation
27
+ from darwin.agenticcloud.runtime import Runtime
28
+ from darwin.agenticcloud.signing import Signer
29
+ from darwin.agenticcloud.types import WorkloadSpec
30
+
31
+ app = typer.Typer(
32
+ name="darwin",
33
+ help="Darwin Agentic Cloud — verifiable compute for AI agents.",
34
+ no_args_is_help=True,
35
+ add_completion=False,
36
+ )
37
+ keys_app = typer.Typer(help="Manage signing keys.", no_args_is_help=True)
38
+ attest_app = typer.Typer(help="Work with attestations.", no_args_is_help=True)
39
+ history_app = typer.Typer(help="Query attestation history.", no_args_is_help=True)
40
+ mcp_app = typer.Typer(help="Model Context Protocol (MCP) server.", no_args_is_help=True)
41
+
42
+ app.add_typer(keys_app, name="keys")
43
+ app.add_typer(attest_app, name="attest")
44
+ app.add_typer(history_app, name="history")
45
+ app.add_typer(mcp_app, name="mcp")
46
+
47
+ console = Console()
48
+ err_console = Console(stderr=True)
49
+
50
+
51
+ # -------------------------------------------------------------------
52
+ # Top-level commands
53
+ # -------------------------------------------------------------------
54
+ @app.command()
55
+ def run(
56
+ file: Annotated[Path, typer.Argument(help="Path to the script to run.")],
57
+ language: Annotated[
58
+ str, typer.Option("--language", "-l", help="Language (python or node).")
59
+ ] = "python",
60
+ timeout: Annotated[int, typer.Option("--timeout", "-t", help="Timeout in seconds.")] = 30,
61
+ memory: Annotated[int, typer.Option("--memory", "-m", help="Memory limit in MB.")] = 512,
62
+ cost_cap: Annotated[float, typer.Option("--cost-cap", help="Cost ceiling in USD.")] = 0.01,
63
+ save: Annotated[
64
+ Path | None, typer.Option("--save", help="Write the signed attestation to this path.")
65
+ ] = None,
66
+ json_only: Annotated[
67
+ bool, typer.Option("--json", help="Print only the signed attestation JSON.")
68
+ ] = False,
69
+ ) -> None:
70
+ """Execute a script in the darwin.agenticcloud sandbox and emit a signed attestation."""
71
+ if not file.exists():
72
+ err_console.print(f"[red]File not found:[/red] {file}")
73
+ raise typer.Exit(code=2)
74
+
75
+ code = file.read_text(encoding="utf-8")
76
+ spec = WorkloadSpec(
77
+ code=code,
78
+ language=language,
79
+ timeout_sec=timeout,
80
+ memory_mb=memory,
81
+ cost_cap_usd=cost_cap,
82
+ )
83
+
84
+ runtime = Runtime()
85
+ signed = runtime.run(spec)
86
+
87
+ signed_dict = {
88
+ "attestation": signed.attestation,
89
+ "signature_b64": signed.signature_b64,
90
+ "public_key_b64": signed.public_key_b64,
91
+ }
92
+
93
+ if save is not None:
94
+ save.write_text(json.dumps(signed_dict, indent=2), encoding="utf-8")
95
+
96
+ if json_only:
97
+ print(json.dumps(signed_dict, indent=2))
98
+ return
99
+
100
+ _print_execution(signed_dict, save)
101
+
102
+
103
+ def _print_execution(signed_dict: dict, saved_to: Path | None) -> None:
104
+ a = signed_dict["attestation"]
105
+ er = a["execution_result"]
106
+
107
+ status = er["status"]
108
+ color = {
109
+ "ok": "green",
110
+ "error": "red",
111
+ "timeout": "yellow",
112
+ "oom": "yellow",
113
+ "cost_exceeded": "red",
114
+ }.get(status, "white")
115
+
116
+ table = Table(show_header=False, box=None, padding=(0, 2))
117
+ table.add_column(style="bold")
118
+ table.add_column()
119
+ table.add_row("status", f"[{color}]{status}[/{color}]")
120
+ table.add_row("exit_code", str(er["exit_code"]))
121
+ table.add_row("wall_time", f"{er['wall_time_sec']:.3f} s")
122
+ table.add_row("cost", f"${er['cost_usd']:.8f}")
123
+ table.add_row("substrate", er["substrate_id"])
124
+ table.add_row("workload_id", er["workload_id"])
125
+ table.add_row("attestation_id", a["attestation_id"])
126
+ table.add_row("signer_key_id", a["signer_key_id"])
127
+ table.add_row("output_hash", er["output_hash"][:16] + "…")
128
+
129
+ console.print(Panel(table, title="darwin.agenticcloud execution", border_style=color))
130
+
131
+ if er["stdout"]:
132
+ console.print(Panel(er["stdout"].rstrip("\n"), title="stdout", border_style="dim"))
133
+ if er["stderr"]:
134
+ console.print(Panel(er["stderr"].rstrip("\n"), title="stderr", border_style="yellow"))
135
+
136
+ if saved_to is not None:
137
+ console.print(f"[dim]Signed attestation saved to[/dim] {saved_to}")
138
+
139
+
140
+ @app.command()
141
+ def serve(
142
+ host: Annotated[str, typer.Option("--host", help="Bind address.")] = "127.0.0.1",
143
+ port: Annotated[int, typer.Option("--port", "-p", help="Bind port.")] = 8787,
144
+ reload: Annotated[
145
+ bool, typer.Option("--reload", help="Reload on file changes (dev only).")
146
+ ] = False,
147
+ ) -> None:
148
+ """Run the darwin.agenticcloud HTTP server."""
149
+ import uvicorn
150
+
151
+ uvicorn.run(
152
+ "darwin.agenticcloud.server:app", host=host, port=port, reload=reload, log_level="info"
153
+ )
154
+
155
+
156
+ @app.command()
157
+ def version() -> None:
158
+ """Print the Darwin version."""
159
+ import darwin
160
+
161
+ print(darwin.__version__)
162
+
163
+
164
+ # -------------------------------------------------------------------
165
+ # keys
166
+ # -------------------------------------------------------------------
167
+ @keys_app.command("show")
168
+ def keys_show() -> None:
169
+ """Show the current signing key identity."""
170
+ signer = Signer()
171
+ table = Table(show_header=False, box=None, padding=(0, 2))
172
+ table.add_column(style="bold")
173
+ table.add_column()
174
+ table.add_row("key_id", signer.key_id())
175
+ table.add_row("public_key_b64", signer.public_key_b64())
176
+ table.add_row("key_path", str(signer.key_path))
177
+ console.print(Panel(table, title="darwin.agenticcloud signing key", border_style="cyan"))
178
+
179
+
180
+ @keys_app.command("init")
181
+ def keys_init() -> None:
182
+ """Initialize the signing keypair (no-op if one already exists)."""
183
+ signer = Signer()
184
+ console.print(f"[green]Key ready:[/green] {signer.key_id()}")
185
+ console.print(f"[dim]Path:[/dim] {signer.key_path}")
186
+
187
+
188
+ # -------------------------------------------------------------------
189
+ # attest
190
+ # -------------------------------------------------------------------
191
+ @attest_app.command("verify")
192
+ def attest_verify(
193
+ file: Annotated[Path, typer.Argument(help="Path to a signed attestation JSON file.")],
194
+ ) -> None:
195
+ """Verify a signed attestation."""
196
+ if not file.exists():
197
+ err_console.print(f"[red]File not found:[/red] {file}")
198
+ raise typer.Exit(code=2)
199
+
200
+ try:
201
+ data = json.loads(file.read_text(encoding="utf-8"))
202
+ except json.JSONDecodeError as e:
203
+ err_console.print(f"[red]Invalid JSON:[/red] {e}")
204
+ raise typer.Exit(code=2) from e
205
+
206
+ ok = verify_attestation(data)
207
+ if ok:
208
+ a = data.get("attestation", {})
209
+ console.print("[green]✓ verified[/green]")
210
+ console.print(f" attestation_id: {a.get('attestation_id', '?')}")
211
+ console.print(f" signer_key_id: {a.get('signer_key_id', '?')}")
212
+ console.print(f" schema: {a.get('schema', '?')}")
213
+ raise typer.Exit(code=0)
214
+ else:
215
+ console.print("[red]✗ verification failed[/red]")
216
+ raise typer.Exit(code=1)
217
+
218
+
219
+ @attest_app.command("show")
220
+ def attest_show(
221
+ file: Annotated[Path, typer.Argument(help="Path to a signed attestation JSON file.")],
222
+ ) -> None:
223
+ """Pretty-print a signed attestation."""
224
+ if not file.exists():
225
+ err_console.print(f"[red]File not found:[/red] {file}")
226
+ raise typer.Exit(code=2)
227
+ data = json.loads(file.read_text(encoding="utf-8"))
228
+ print(json.dumps(data, indent=2))
229
+
230
+
231
+ # -------------------------------------------------------------------
232
+ # history
233
+ # -------------------------------------------------------------------
234
+ @history_app.command("list")
235
+ def history_list(
236
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Max rows to return.")] = 20,
237
+ status: Annotated[str | None, typer.Option("--status", help="Filter by status.")] = None,
238
+ ) -> None:
239
+ """List recent attestations."""
240
+ from darwin.agenticcloud.storage import AttestationStore
241
+
242
+ store = AttestationStore()
243
+ rows = store.list_by_status(status, limit=limit) if status else store.list_recent(limit=limit)
244
+
245
+ if not rows:
246
+ console.print("[dim]No attestations stored yet.[/dim]")
247
+ return
248
+
249
+ table = Table(show_header=True, box=None, padding=(0, 1))
250
+ table.add_column("issued_at", style="dim")
251
+ table.add_column("status")
252
+ table.add_column("workload_id")
253
+ table.add_column("cost", justify="right")
254
+ table.add_column("wall_time", justify="right")
255
+ table.add_column("substrate")
256
+ table.add_column("id (short)", style="dim")
257
+
258
+ from datetime import datetime
259
+
260
+ for r in rows:
261
+ color = {"ok": "green", "error": "red", "timeout": "yellow", "cost_exceeded": "red"}.get(
262
+ r.status, "white"
263
+ )
264
+ ts = datetime.fromtimestamp(r.issued_at, tz=UTC).strftime("%Y-%m-%d %H:%M:%S")
265
+ table.add_row(
266
+ ts,
267
+ f"[{color}]{r.status}[/{color}]",
268
+ r.workload_id,
269
+ f"${r.cost_usd:.8f}",
270
+ f"{r.wall_time_sec:.3f}s",
271
+ r.substrate_id,
272
+ r.attestation_id[:8],
273
+ )
274
+
275
+ console.print(table)
276
+
277
+
278
+ @history_app.command("stats")
279
+ def history_stats() -> None:
280
+ """Show aggregate stats across stored attestations."""
281
+ from darwin.agenticcloud.storage import AttestationStore
282
+
283
+ store = AttestationStore()
284
+ total_count = store.count()
285
+ total_cost = store.total_cost_usd()
286
+
287
+ ok_count = len(store.list_by_status("ok", limit=10**9))
288
+ err_count = len(store.list_by_status("error", limit=10**9))
289
+ timeout_count = len(store.list_by_status("timeout", limit=10**9))
290
+ rejected_count = len(store.list_by_status("cost_exceeded", limit=10**9))
291
+
292
+ table = Table(show_header=False, box=None, padding=(0, 2))
293
+ table.add_column(style="bold")
294
+ table.add_column()
295
+ table.add_row("total executions", str(total_count))
296
+ table.add_row("total cost", f"${total_cost:.8f}")
297
+ table.add_row("status: ok", str(ok_count))
298
+ table.add_row("status: error", str(err_count))
299
+ table.add_row("status: timeout", str(timeout_count))
300
+ table.add_row("status: cost_exceeded", str(rejected_count))
301
+
302
+ console.print(
303
+ Panel(table, title="darwin.agenticcloud attestation history", border_style="cyan")
304
+ )
305
+
306
+
307
+ @history_app.command("show")
308
+ def history_show(
309
+ attestation_id: Annotated[str, typer.Argument(help="Attestation ID (full or first 8 chars).")],
310
+ ) -> None:
311
+ """Show the full signed attestation for a given ID."""
312
+ from darwin.agenticcloud.storage import AttestationStore
313
+
314
+ store = AttestationStore()
315
+
316
+ if len(attestation_id) < 36:
317
+ candidates = [
318
+ a for a in store.list_recent(limit=10**9) if a.attestation_id.startswith(attestation_id)
319
+ ]
320
+ if not candidates:
321
+ err_console.print(f"[red]No attestation matching prefix:[/red] {attestation_id}")
322
+ raise typer.Exit(code=2)
323
+ if len(candidates) > 1:
324
+ err_console.print(
325
+ f"[red]Ambiguous prefix:[/red] {attestation_id} matches {len(candidates)} attestations"
326
+ )
327
+ raise typer.Exit(code=2)
328
+ attestation_id = candidates[0].attestation_id
329
+
330
+ fetched = store.get(attestation_id)
331
+ if fetched is None:
332
+ err_console.print(f"[red]Not found:[/red] {attestation_id}")
333
+ raise typer.Exit(code=2)
334
+
335
+ print(json.dumps(fetched.signed_attestation, indent=2))
336
+
337
+
338
+ # -------------------------------------------------------------------
339
+ # mcp
340
+ # -------------------------------------------------------------------
341
+ @mcp_app.command("serve")
342
+ def mcp_serve() -> None:
343
+ """Run the darwin.agenticcloud MCP server on stdio.
344
+
345
+ Intended to be spawned by an MCP client (Claude Desktop, Cursor, etc.)
346
+ over stdio. Do not run this manually unless you're piping JSON-RPC
347
+ into it.
348
+ """
349
+ from darwin.agenticcloud.mcp_server import run as run_mcp
350
+
351
+ run_mcp()
352
+
353
+
354
+ if __name__ == "__main__":
355
+ app()
356
+
357
+
358
+ @mcp_app.command("install")
359
+ def mcp_install(
360
+ client: Annotated[
361
+ str, typer.Option("--client", help="MCP client: 'claude-desktop' or 'cursor'.")
362
+ ] = "claude-desktop",
363
+ name: Annotated[
364
+ str, typer.Option("--name", help="Server entry name in the config.")
365
+ ] = "darwin",
366
+ force: Annotated[
367
+ bool, typer.Option("--force", help="Overwrite an existing entry without prompting.")
368
+ ] = False,
369
+ ) -> None:
370
+ """Install darwin.agenticcloud as an MCP server in a supported client.
371
+
372
+ Detects the client's config file, adds an entry that spawns
373
+ `python -m darwin.agenticcloud.mcp_server` using the current
374
+ Python interpreter, and writes the config back. Idempotent.
375
+ """
376
+ import os
377
+ import platform
378
+ import sys
379
+ from pathlib import Path
380
+
381
+ home = Path.home()
382
+ system = platform.system()
383
+
384
+ if client == "claude-desktop":
385
+ if system == "Darwin":
386
+ config_path = (
387
+ home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
388
+ )
389
+ elif system == "Windows":
390
+ appdata = os.environ.get("APPDATA")
391
+ if not appdata:
392
+ err_console.print("[red]APPDATA env var not set; can't locate Claude config.[/red]")
393
+ raise typer.Exit(code=2)
394
+ config_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
395
+ elif system == "Linux":
396
+ config_path = home / ".config" / "Claude" / "claude_desktop_config.json"
397
+ else:
398
+ err_console.print(f"[red]Unsupported OS for claude-desktop:[/red] {system}")
399
+ raise typer.Exit(code=2)
400
+ elif client == "cursor":
401
+ config_path = home / ".cursor" / "mcp.json"
402
+ else:
403
+ err_console.print(f"[red]Unknown client:[/red] {client}")
404
+ raise typer.Exit(code=2)
405
+
406
+ # Load existing config (or create empty)
407
+ if config_path.exists():
408
+ try:
409
+ config = json.loads(config_path.read_text(encoding="utf-8"))
410
+ except json.JSONDecodeError as e:
411
+ err_console.print(f"[red]Config file exists but is invalid JSON:[/red] {e}")
412
+ raise typer.Exit(code=2) from e
413
+ else:
414
+ config_path.parent.mkdir(parents=True, exist_ok=True)
415
+ config = {}
416
+
417
+ config.setdefault("mcpServers", {})
418
+
419
+ if name in config["mcpServers"] and not force:
420
+ existing = config["mcpServers"][name]
421
+ if existing.get("command") == sys.executable and existing.get("args") == [
422
+ "-m",
423
+ "darwin.agenticcloud.mcp_server",
424
+ ]:
425
+ console.print(f"[green]✓ {name} already installed in {client} (no changes).[/green]")
426
+ console.print(f" config: {config_path}")
427
+ console.print(f" python: {sys.executable}")
428
+ raise typer.Exit(code=0)
429
+ else:
430
+ err_console.print(f"[yellow]Entry '{name}' already exists in {config_path}:[/yellow]")
431
+ err_console.print(f" {json.dumps(existing, indent=2)}")
432
+ err_console.print("Use --force to overwrite, or pick a different --name.")
433
+ raise typer.Exit(code=1)
434
+
435
+ config["mcpServers"][name] = {
436
+ "command": sys.executable,
437
+ "args": ["-m", "darwin.agenticcloud.mcp_server"],
438
+ }
439
+
440
+ config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
441
+
442
+ console.print(
443
+ f"[green]✓ Installed darwin.agenticcloud as MCP server '{name}' in {client}.[/green]"
444
+ )
445
+ console.print(f" config: {config_path}")
446
+ console.print(f" command: {sys.executable}")
447
+ console.print(" args: -m darwin.agenticcloud.mcp_server")
448
+ console.print()
449
+ console.print("[dim]Restart your MCP client to pick up the change.[/dim]")
450
+
451
+
452
+ @mcp_app.command("uninstall")
453
+ def mcp_uninstall(
454
+ client: Annotated[
455
+ str, typer.Option("--client", help="MCP client: 'claude-desktop' or 'cursor'.")
456
+ ] = "claude-desktop",
457
+ name: Annotated[str, typer.Option("--name", help="Server entry name to remove.")] = "darwin",
458
+ ) -> None:
459
+ """Remove an MCP server entry from the client config."""
460
+ import os
461
+ import platform
462
+ from pathlib import Path
463
+
464
+ home = Path.home()
465
+ system = platform.system()
466
+
467
+ if client == "claude-desktop":
468
+ if system == "Darwin":
469
+ config_path = (
470
+ home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
471
+ )
472
+ elif system == "Windows":
473
+ appdata = os.environ.get("APPDATA")
474
+ if not appdata:
475
+ err_console.print("[red]APPDATA env var not set.[/red]")
476
+ raise typer.Exit(code=2)
477
+ config_path = Path(appdata) / "Claude" / "claude_desktop_config.json"
478
+ elif system == "Linux":
479
+ config_path = home / ".config" / "Claude" / "claude_desktop_config.json"
480
+ else:
481
+ err_console.print(f"[red]Unsupported OS for claude-desktop:[/red] {system}")
482
+ raise typer.Exit(code=2)
483
+ elif client == "cursor":
484
+ config_path = home / ".cursor" / "mcp.json"
485
+ else:
486
+ err_console.print(f"[red]Unknown client:[/red] {client}")
487
+ raise typer.Exit(code=2)
488
+
489
+ if not config_path.exists():
490
+ console.print(f"[dim]No config file at {config_path} (nothing to remove).[/dim]")
491
+ raise typer.Exit(code=0)
492
+
493
+ config = json.loads(config_path.read_text(encoding="utf-8"))
494
+ if "mcpServers" not in config or name not in config["mcpServers"]:
495
+ console.print(f"[dim]Entry '{name}' not found in {config_path} (nothing to remove).[/dim]")
496
+ raise typer.Exit(code=0)
497
+
498
+ del config["mcpServers"][name]
499
+ config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
500
+
501
+ console.print(f"[green]✓ Removed MCP server '{name}' from {client}.[/green]")
502
+ console.print(f" config: {config_path}")
@@ -0,0 +1,76 @@
1
+ """Cost model and budget enforcement for DAC.
2
+
3
+ Costs are computed per substrate. For v0, local Docker has a flat
4
+ wall-time rate. Real substrates (GPU, decentralized providers, sovereign
5
+ clouds) will have richer rate cards in v0.2.
6
+
7
+ Enforcement:
8
+ - Pre-flight: max_possible_cost(spec) is computed before sandbox launch.
9
+ If it would exceed the cap, we reject with BudgetExceeded — no
10
+ sandbox launched, no resources consumed.
11
+ - In-flight: tracked in v0.2 when variable-rate substrates land.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from darwin.agenticcloud.types import WorkloadSpec
17
+
18
+ # Rate card: USD per wall-time second by substrate.
19
+ # Tracked separately from substrate identity so a substrate can update
20
+ # its rate over time without breaking attestation verifiability.
21
+ SUBSTRATE_RATES_PER_SEC: dict[str, float] = {
22
+ "local-docker-v0": 0.0001,
23
+ }
24
+
25
+ DEFAULT_RATE_PER_SEC = 0.0001
26
+
27
+
28
+ class BudgetExceeded(Exception):
29
+ """Raised when a workload's cost cap is or would be exceeded."""
30
+
31
+ def __init__(self, message: str, *, projected_usd: float, cap_usd: float) -> None:
32
+ super().__init__(message)
33
+ self.projected_usd = projected_usd
34
+ self.cap_usd = cap_usd
35
+
36
+
37
+ def rate_for_substrate(substrate_id: str) -> float:
38
+ """Return USD per wall-time second for a substrate."""
39
+ return SUBSTRATE_RATES_PER_SEC.get(substrate_id, DEFAULT_RATE_PER_SEC)
40
+
41
+
42
+ def cost_for_seconds(seconds: float, substrate_id: str) -> float:
43
+ """Compute the cost (USD) for a given wall-time duration on a substrate."""
44
+ rate = rate_for_substrate(substrate_id)
45
+ return round(seconds * rate, 8)
46
+
47
+
48
+ def max_possible_cost(spec: WorkloadSpec, substrate_id: str) -> float:
49
+ """The largest cost this workload could incur if it runs to its timeout."""
50
+ return cost_for_seconds(spec.timeout_sec, substrate_id)
51
+
52
+
53
+ def check_budget(spec: WorkloadSpec, substrate_id: str) -> None:
54
+ """Pre-flight: raise BudgetExceeded if the workload's max cost exceeds its cap.
55
+
56
+ This is the cheap, defensive check. It runs before any sandbox is
57
+ launched. It rejects requests where timeout * rate would exceed cap,
58
+ so a hallucinating agent that asks for a 600-second timeout under a
59
+ $0.01 cap gets stopped before consuming any resources.
60
+ """
61
+ if spec.cost_cap_usd <= 0:
62
+ raise BudgetExceeded(
63
+ f"cost_cap_usd must be > 0; got {spec.cost_cap_usd}",
64
+ projected_usd=0.0,
65
+ cap_usd=spec.cost_cap_usd,
66
+ )
67
+
68
+ projected = max_possible_cost(spec, substrate_id)
69
+ if projected > spec.cost_cap_usd:
70
+ raise BudgetExceeded(
71
+ f"Projected max cost ${projected:.8f} exceeds cap ${spec.cost_cap_usd:.8f} "
72
+ f"(timeout={spec.timeout_sec}s @ ${rate_for_substrate(substrate_id):.8f}/s "
73
+ f"on {substrate_id})",
74
+ projected_usd=projected,
75
+ cap_usd=spec.cost_cap_usd,
76
+ )