Metadata-Version: 2.4
Name: krysta
Version: 1.1.1
Summary: A unified multimodal model evaluation tracking and engineering report engine.
Author: Anshu Aditya
Project-URL: Homepage, https://github.com/Krysta-Wing/
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27.0
Requires-Dist: httpx-sse>=0.4.0

# krysta · noa

**Sandboxed code execution for AI agents.**

Run untrusted Python and JavaScript inside an isolated Docker sandbox — no network access, memory limits, live streaming telemetry, and structured crash reporting. Built for agent loops, eval harnesses, and production guardrails.

```bash
pip install krysta
```

---

## What you get

| | |
|---|---|
| **Sandboxed execution** | Python 3.11 or Node 20 in Docker — no network, 128 MB RAM cap |
| **Live SSE streaming** | stdout, stderr, rules, and lifecycle events in real time |
| **Dual mode** | `async for` live events, or `await` for a resolved `ExecutionTrace` |
| **Crash telemetry** | Structured `CrashInfo` with error type, message, line number, stack |
| **Stateful sessions** | `spawn()` mounts a persistent `/workspace` for multi-turn loops |
| **Client-side validation** | `RuleEngine` + `ExecutionTrace` for offline rule checks |

---

## Quickstart

```python
import asyncio
from krysta.noa import Noa

async def main():
    async with Noa() as noa:
        trace = await noa.execute(
            language="python",
            code='print("hello from noa")',
            timeout_ms=8000,
        )

    print(trace.exit_code)      # 0
    print(trace.duration_ms)    # 1020
    print(trace.stdout_lines)   # [{"type": "stdout", "text": "hello from noa"}]

asyncio.run(main())
```

The `async with` block manages the HTTP client lifecycle. Always enter it before calling `execute()`.

---

## execute()

```python
noa.execute(
    language="python",   # "python" | "javascript"
    code=None,           # source string
    code_path=None,      # or path to a local file (max 256 KB)
    timeout_ms=10000,    # 1–60000 ms
    session_id=None,     # pin to a persistent sandbox session
)
```

Either `code` or `code_path` is required.

### Await a full trace

```python
async with Noa() as noa:
    trace = await noa.execute(language="python", code="print('hi')")

print(trace.exit_code)
print(trace.duration_ms)
```

### Stream events live

```python
async with Noa() as noa:
    async for event in noa.execute(language="python", code=long_running_code):
        if event["type"] == "stdout":
            print("→", event["text"], end="", flush=True)
        elif event["type"] == "done":
            print("\n[done]")
```

---

## ExecutionTrace

| Field | Type | Description |
|---|---|---|
| `job_id` | `str` | Unique job identifier |
| `exit_code` | `int \| None` | `0` clean, `1` crash, `None` no terminal event |
| `duration_ms` | `int` | Wall-clock execution time (daemon-side) |
| `memory_used_mb` | `float` | Peak RSS in MB. `0.0` on very fast exits or crashes |
| `timeout_hit` | `bool` | `True` if killed by the timeout enforcer |
| `stdout_lines` | `list[dict]` | `[{"type": "stdout", "text": "..."}]` per line |
| `stderr_lines` | `list[str]` | Raw stderr. Includes full traceback on crash |
| `crash` | `CrashInfo \| None` | `None` on clean exit, populated on non-zero exit |

### CrashInfo

```python
trace = await noa.execute(language="python", code="x = 1 / 0")

if trace.crash:
    print(trace.crash.error_type)   # ZeroDivisionError
    print(trace.crash.message)      # division by zero
    print(trace.crash.line_number)  # 1
    print(trace.crash.stack_trace)  # [...]
```

---

## Streaming events

Every `execute()` call yields event dicts:

| `type` | When | Notes |
|---|---|---|
| `system` | Start | e.g. `EXECUTION_STARTED` |
| `stdout` | During run | One line per yield (server batches are expanded automatically) |
| `stderr` | During run | Error output, tracebacks |
| `rules` | Near end | `text` is JSON list of rule results |
| `metrics` | Near end | `text` contains `duration_ms` |
| `done` | Success | Exit code 0 |
| `timeout` | Killed | Exceeded `timeout_ms` |
| `error` | Failure | Non-zero exit or infrastructure error |

---

## Safety rules

Returned in the `rules` event and via `noa.validate(trace)`:

| Rule | Category | Checks |
|---|---|---|
| `NoNetworkCallsRule` | security | No outbound network calls |
| `ExitCodeZeroRule` | security | Clean exit (code 0) |
| `MemoryLimitRule` | security | Stayed under 128 MB |
| `NoFilesystemAccessRule` | security | No FS access outside `/workspace` |
| `ValidJsonRule` | optional | Stdout is valid JSON |

Static pre-checks run before Docker: code containing `urllib`, `requests`, `fetch`, or filesystem patterns like `open()` (without a session) is rejected immediately.

---

## Stateful sessions

Use `spawn()` when an agent writes files, reruns, and needs state to persist between executions:

```python
import uuid
from krysta.noa import spawn

async with spawn(runtime="python", session_id=str(uuid.uuid4())) as sandbox:
    async for _ in sandbox.execute(code="open('/workspace/data.txt','w').write('hello')"):
        pass
    async for event in sandbox.execute(code="print(open('/workspace/data.txt').read())"):
        if event["type"] == "stdout":
            print(event["text"])  # hello
```

- Without `session_id`, a random UUID is assigned automatically.
- File I/O is only permitted inside `/workspace` when a session is active.
- Network remains blocked in all cases.

---

## Agent patterns

### Generate → run → fix loop

```python
async def agent_loop(noa, code):
    for attempt in range(3):
        stderr_lines = []
        async for event in noa.execute(language="python", code=code, timeout_ms=15000):
            if event["type"] == "stderr":
                stderr_lines.append(event["text"])
            elif event["type"] == "done":
                return code
            elif event["type"] == "error":
                code = await your_llm_fix(code, stderr_lines)
                break
    raise RuntimeError("agent failed after retries")
```

### JSON contract

Agents should emit one JSON document on stdout for `ValidJsonRule` to pass:

```python
code = """
import json
result = {"answer": 42, "confidence": 0.99}
print(json.dumps(result))
"""
```

### Parallel jobs

```python
import asyncio

async with Noa() as noa:
    traces = await asyncio.gather(
        noa.execute(language="python", code='print("job 1")'),
        noa.execute(language="python", code='print("job 2")'),
        noa.execute(language="python", code='print("job 3")'),
    )
```

---

## Error handling

```python
from krysta.exceptions import KrystaGatewayError, KrystaTimeoutError

try:
    async with Noa() as noa:
        trace = await noa.execute(language="python", code=code)
except ValueError as e:
    # missing code/code_path, or payload > 256 KB
    ...
except RuntimeError as e:
    # execute() called outside async with, or stream dropped before jobId
    ...
except KrystaTimeoutError:
    # SSE channel timed out — network level, not sandbox level — retry
    ...
except KrystaGatewayError as e:
    # gateway down, bad status, SSE failure
    ...

# sandbox timeout is NOT an exception — check the trace
if trace.timeout_hit:
    print("job killed by sandbox timeout enforcer")
```

| Exception | Cause |
|---|---|
| `ValueError` | No `code`/`code_path`, or file > 256 KB |
| `RuntimeError` | Client not in `async with`, or stream dropped before job ID assigned |
| `KrystaGatewayError` | Gateway unreachable, non-202, non-JSON, SSE failure |
| `KrystaTimeoutError` | HTTP/SSE transport timeout (distinct from `timeout_hit`) |
| `KrystaSandboxError` | Sandbox-level failure from rule engine |

---

## Troubleshooting

| Symptom | Fix |
|---|---|
| `KrystaGatewayError` on submit | Check network connectivity and gateway status |
| Empty stdout but `done` received | Rare race on fast jobs — retry; SDK replays from Redis on reconnect |
| `NoNetworkCallsRule` FAIL | Remove `requests`, `urllib`, `fetch` from agent code |
| `NoFilesystemAccessRule` FAIL | Use `spawn(session_id=...)` and stay inside `/workspace` |
| `ValidJsonRule` FAIL | Print exactly one JSON object on stdout |
| SSE timeout | Increase `timeout_ms`; check Redis/broker health |
| `Connection dropped before jobId` | Network blip during submit — retry `execute()` |

---

## PyPI

[https://pypi.org/project/krysta/](https://pypi.org/project/krysta/)

---

## License

MIT
