Implement SAND-WP-0006: SaaS payments, routing, and ext.saas-stub
Add credits store, metering on create/destroy, extension routing resolver, metered SaaS stub extension, burst/saas profiles, credits CLI, docs, and tests.
This commit is contained in:
parent
eee336149e
commit
1415e17230
29 changed files with 878 additions and 18 deletions
|
|
@ -6,7 +6,7 @@ import uuid
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from sandboxer.models import Profile
|
||||
from sandboxer.models import MeterQuote, Profile
|
||||
|
||||
|
||||
class SandboxExtension(ABC):
|
||||
|
|
@ -31,4 +31,18 @@ class SandboxExtension(ABC):
|
|||
|
||||
@abstractmethod
|
||||
def teardown(self, handle: dict[str, str]) -> dict[str, str]:
|
||||
"""Release sandbox resources. Returns cleanup report fields."""
|
||||
"""Release sandbox resources. Returns cleanup report fields."""
|
||||
|
||||
def estimate_cost(
|
||||
self,
|
||||
profile: Profile,
|
||||
inputs: dict[str, str],
|
||||
*,
|
||||
duration_s: int = 3600,
|
||||
) -> MeterQuote | None:
|
||||
"""Optional pre-create cost quote (metered SaaS extensions)."""
|
||||
return None
|
||||
|
||||
def meter_actual(self, handle: dict[str, str], *, duration_s: float) -> float | None:
|
||||
"""Optional post-destroy actual cost in USD."""
|
||||
return None
|
||||
66
src/sandboxer/extensions/saas_stub.py
Normal file
66
src/sandboxer/extensions/saas_stub.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""ext.saas-stub — metered SaaS extension stub for routing and payments v0.
|
||||
|
||||
No external provider API. Exercises estimate_cost, credits debit, and routing
|
||||
fallback without E2B/Modal credentials.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sandboxer.extensions.base import SandboxExtension
|
||||
from sandboxer.models import MeterQuote, Profile
|
||||
|
||||
|
||||
class SaaSStubExtension(SandboxExtension):
|
||||
"""Simulated metered SaaS sandbox backend."""
|
||||
|
||||
def __init__(self, config: dict[str, Any] | None = None) -> None:
|
||||
super().__init__(config)
|
||||
self.rate_usd_per_hour: float = float(self.config.get("rate_usd_per_hour", 0.12))
|
||||
self.session_fee_usd: float = float(self.config.get("session_fee_usd", 0.01))
|
||||
self.provider: str = self.config.get("provider", "saas-stub")
|
||||
|
||||
def estimate_cost(
|
||||
self,
|
||||
profile: Profile,
|
||||
inputs: dict[str, str],
|
||||
*,
|
||||
duration_s: int = 3600,
|
||||
) -> MeterQuote:
|
||||
hours = max(duration_s / 3600.0, 1 / 3600)
|
||||
estimated = round(self.session_fee_usd + hours * self.rate_usd_per_hour, 4)
|
||||
return MeterQuote(
|
||||
extension_id="ext.saas-stub",
|
||||
estimated_usd=estimated,
|
||||
unit="per_hour",
|
||||
duration_s=duration_s,
|
||||
)
|
||||
|
||||
def meter_actual(self, handle: dict[str, str], *, duration_s: float) -> float:
|
||||
hours = max(duration_s / 3600.0, 1 / 3600)
|
||||
return round(self.session_fee_usd + hours * self.rate_usd_per_hour, 4)
|
||||
|
||||
def provision(
|
||||
self, profile: Profile, inputs: dict[str, str], host: str
|
||||
) -> dict[str, str]:
|
||||
sandbox_id = self.new_sandbox_id(inputs)
|
||||
endpoint = f"https://stub.sandboxer.local/{sandbox_id}"
|
||||
return {
|
||||
"sandbox_id": sandbox_id,
|
||||
"host": self.provider,
|
||||
"endpoint": endpoint,
|
||||
"provider": self.provider,
|
||||
}
|
||||
|
||||
def wait_ready(self, handle: dict[str, str]) -> dict[str, str]:
|
||||
return {
|
||||
"endpoint": handle["endpoint"],
|
||||
"host": handle.get("host"),
|
||||
}
|
||||
|
||||
def teardown(self, handle: dict[str, str]) -> dict[str, str]:
|
||||
return {
|
||||
"provider_removed": "true",
|
||||
"sandbox_id": handle.get("sandbox_id", ""),
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue