26b4a5be60
Task 2-3-01: create(learner_id, task_id) — telemetry sandboxes run a persistent
helper/inner namespace topology (offline inner ns; agent joins mount ns only and stays
online to reach the loopback ingest). Capture agent copied into the workspace (visible
in-ns at the bind), launched via sh -c with in-ns absolute paths (host cwd invalid after
the nsenter mount swap), stdin=DEVNULL daemonizes the agent (lifecycle tied to sandbox:
destroy reaps agent -> inner -> helper). Wire contract: frames strip URL-owned identity
(ingest extra=forbid anti-spoofing); spool keeps full events.
E2E test (real uvicorn on ephemeral port): exec in a live namespace sandbox -> events
arrive at WS ingest -> SQLite, ordered, sandbox-scoped. Pure-shell path (task_id=None)
asserts no capture agent. Full suite 216 green; ruff clean.
---ci---
phase: 2
milestone: v0.3
status: execute
requirements: {covered: [REQ-3-003], partial: []}
---/ci---
80 lines
3.5 KiB
Python
80 lines
3.5 KiB
Python
"""Service settings — pydantic-settings, env prefix AI_, .env support."""
|
|
|
|
from pathlib import Path
|
|
from typing import Annotated
|
|
|
|
from pydantic import field_validator
|
|
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
|
|
|
# apps/ai-service/ (sandbox dir default is relative to the app, not the CWD)
|
|
_SERVICE_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_prefix="AI_", env_file=".env", extra="ignore")
|
|
|
|
port: int = 8420
|
|
provider: str = "mock"
|
|
model: str = "gemma4:31b"
|
|
|
|
ollama_cloud_base_url: str = "https://ollama.com/v1"
|
|
ollama_cloud_api_key: str = "" # SecretStr adds friction here; never logged, never echoed
|
|
local_base_url: str = "http://localhost:11434/v1"
|
|
|
|
# "auto" sends response_format and degrades on 400; "off" never sends it
|
|
json_mode: str = "auto"
|
|
|
|
# v0.3 sandbox fabric (REQ-3-001): root holding per-sandbox workdirs.
|
|
# Relative paths resolve against the app dir (apps/ai-service/), not the CWD.
|
|
sandbox_dir: Path = _SERVICE_ROOT / "sandboxes"
|
|
|
|
# D-032: single-box capacity, no queue — pool full → API maps to 503.
|
|
sandbox_max_concurrent: int = 5
|
|
|
|
# Wall-clock ceiling per sandbox; the manager's async reaper destroys
|
|
# sandboxes idle past this age (same timer runs the G-2 workdir sweep).
|
|
sandbox_timeout_s: float = 900.0
|
|
|
|
# G-2: soft disk cap per sandbox workdir, enforced best-effort by the
|
|
# manager sweep (NOT kernel-enforced — no cgroup delegation/sudo here).
|
|
sandbox_max_workdir_mb: int = 512
|
|
|
|
# G-5 abuse control (NOT auth — KYC/auth is deferred; these keep the
|
|
# single-box pilot from melting down before identity lands):
|
|
#
|
|
# Server-side learner allowlist. Env form is a COMMA-SEPARATED string
|
|
# (e.g. AI_LEARNER_ALLOWLIST="pilot-learner,learner-2"); NoDecode skips
|
|
# pydantic-settings' JSON decoding of complex types and the validator
|
|
# below splits/strips/drops empties. Default: the single mock pilot id.
|
|
learner_allowlist: Annotated[list[str], NoDecode] = ["pilot-learner"]
|
|
|
|
# Max ACTIVE sandboxes per learner → API maps excess to 429.
|
|
sandbox_max_per_learner: int = 1
|
|
|
|
# Global create-rate ceiling (creates per rolling 60s window, shared
|
|
# across learners) → API maps excess to 429. In-memory, process-local.
|
|
sandbox_creates_per_min: int = 10
|
|
|
|
@field_validator("learner_allowlist", mode="before")
|
|
@classmethod
|
|
def _split_allowlist_csv(cls, value: object) -> object:
|
|
if isinstance(value, str):
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
return value
|
|
|
|
# D-027: SQLite path for telemetry/grades/variants/defenses stores.
|
|
db_path: Path = _SERVICE_ROOT / "ai_service" / "data" / "nextcraft.db"
|
|
|
|
# G-3 flood control (NOT backpressure-by-silence): max events ingested per
|
|
# (learner_id, task_id) trace before the WS endpoint closes the connection
|
|
# with 1008 and marks the trace INCOMPLETE_FLOODED. Drop-oldest is
|
|
# FORBIDDEN — it corrupts grading input (GRILL G-3).
|
|
telemetry_max_events_per_task: int = 50000
|
|
|
|
# Sandbox telemetry wiring (REQ-3-003): loopback host the in-sandbox capture
|
|
# agent dials to reach this service's WS ingest (the agent joins the sandbox
|
|
# mount ns but NOT the net ns — exec namespaces are offline, so the agent
|
|
# shares the host network and reaches the app over loopback). Port reuses
|
|
# `port` (A-004); only the host is configurable — never a second port.
|
|
telemetry_ingest_host: str = "127.0.0.1"
|