Metadata-Version: 2.4
Name: k8s-agent-sandbox-awx
Version: 0.6.0
Summary: Broker-backed, kubeconfig-free overlay for the Agentic Sandbox client.
Keywords: kubernetes,sandbox,agent,broker,identity,attribution
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: k8s-agent-sandbox-base==0.5.3.post16
Requires-Dist: requests
Requires-Dist: pydantic
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Dynamic: license-file

# k8s-agent-sandbox-awx

A broker-backed, **kubeconfig-free** overlay for the Agentic Sandbox client. It
adds a `BrokerSandboxClient` that drives the sandbox control plane (create → wait
ready → terminate, commands, and files) through one **identity broker service over
HTTPS** instead of the Kubernetes API or a public router. The broker proxies
authorized data operations to its internal router, so client applications need
no kubeconfig and no direct Kubernetes or router access.

```bash
pip install k8s-agent-sandbox-awx==0.6.0
```

```python
from k8s_agent_sandbox_awx import Sandbox

sandbox = Sandbox.create(keepalive_ttl_seconds=0)
result = sandbox.commands.run('echo "Hello from Agent Sandbox!"')
print(result.stdout)
# Leave it available until its timeout; call sandbox.kill() for early cleanup.
```

## Streaming and background commands

SDK **0.6.0** adds managed commands on Python runtime **v0.3.0** and Codex
runtime **v0.2.0**, through broker **v0.11.0**. Create a new sandbox after the
runtime upgrade; existing claimed sandboxes are not replaced. Plain `run()`
calls fall back to the old execution endpoint on older runtimes, but the new
options require the updated runtime and broker.

Stream stdout and stderr while the command runs:

```python
result = sandbox.commands.run(
    ["python3", "-u", "-c", "import os,time; print(os.getenv('DEMO')); time.sleep(1); print('done')"],
    timeout=30,
    envs={"DEMO": "hello"},
    cwd="/app",
    on_stdout=lambda chunk: print(chunk, end="", flush=True),
    on_stderr=lambda chunk: print(chunk, end="", flush=True),
)
print(result.exit_code)
```

Callbacks receive text chunks, not necessarily complete lines. Use unbuffered
output (`python3 -u` or `flush=True`) in the program when you need prompt output.
The result still contains the collected stdout, stderr, and exit code.

Start a background command, wait later, or cancel its process group:

```python
task = sandbox.commands.run(["sleep", "60"], background=True, timeout=120)
print(task.command_id)
print(task.status()["state"])

# Reattach to a known command using the same sandbox/caller:
task = sandbox.commands.get(task.command_id)
# result = task.wait(timeout=10, on_stdout=print)  # wait timeout does NOT kill it
result = task.kill()  # cancel this command, not the sandbox; safe to repeat
print(result.state)   # cancelled
```

Optional stdin is available only for background commands:

```python
task = sandbox.commands.run(["cat"], background=True, stdin=True)
task.send_stdin("hello\n", eof=True)
print(task.wait().stdout)
```

`timeout` limits command execution (default 60 seconds), capped by the runtime's
`SANDBOX_EXEC_TIMEOUT_SECONDS` (default 300 seconds). `task.wait(timeout=...)`
only limits waiting; call `kill()` to cancel. Sandbox expiry still kills all
work; background commands do not extend its lifetime automatically. Cancellation
kills the process group, including normal descendants; it is not a separate
security boundary against processes deliberately escaping their group.

Environment overrides are per-command. `cwd` is an absolute path inside the
sandbox; choose the template's PVC path for durable outputs. String commands
still use `shlex` without an implicit shell; pass an argv list or explicitly use
`["sh", "-c", "..."]` for shell syntax.

The runtime retains up to **1 MiB** of combined output per command and reports
`output_truncated=True` when it reaches the byte/event limit. It drains further
output to avoid blocking the process. It allows **16 running managed commands**
and retains at most **64 handles**. Results expire after **15 minutes**, or
earlier under retention pressure. These are runtime safeguards, not per-caller
sandbox quotas. Handles/results are in-memory and do not survive runtime restart
or sandbox deletion. They are not part of the PostgreSQL audit store.

Output streams reconnect with a cursor every 20 seconds through the broker,
refreshing the caller token and rechecking ownership and `sandbox.exec` policy.
Disconnecting does not cancel background commands. Do not concurrently call
`wait()` on the same handle. A fresh handle replays its retained output.

## How it works

The client presents an audience-bound Kubernetes or GitLab OIDC token to the broker.
The broker **cryptographically verifies** the caller from that token, creates the
`SandboxClaim` under its own identity, and **stamps the verified caller identity**
onto the claim — attribution the client cannot forge.

Set both `broker_url` and `api_url` to the broker URL. The model rejects a
different `api_url` to prevent accidental direct-router bypass.

This package is a purely additive overlay: it builds on
[`k8s-agent-sandbox-base`](https://pypi.org/project/k8s-agent-sandbox-base/) (a
seamed re-publish of the upstream client) and edits zero upstream files. The import
package is `k8s_agent_sandbox_awx`.

## Configuration

The zero-argument API reads:

| Variable | Purpose |
|---|---|
| `AGENT_SANDBOX_BROKER_URL` | Public HTTPS broker endpoint |
| `AGENT_SANDBOX_TOKEN_PATH` | Rotating Kubernetes projected-token file |
| `AGENT_SANDBOX_TOKEN` | GitLab or other OIDC ID token |
| `AGENT_SANDBOX_WARMPOOL` | Approved warm pool; defaults to `python-sandbox-warmpool` |
| `AGENT_SANDBOX_NAMESPACE` | Broker-owned claim namespace; defaults to `agent-sandbox` |
| `AGENT_SANDBOX_TTL_SECONDS` | Sandbox timeout in seconds; defaults to 300 |
| `AGENT_SANDBOX_KEEPALIVE_TTL_SECONDS` | Opt-in activity timeout reset; defaults to 0 (disabled) |

### Timeouts

`Sandbox.create()` defaults to **300 seconds (five minutes)**. An explicit
`shutdown_after_seconds` overrides `AGENT_SANDBOX_TTL_SECONDS` (or legacy
`SANDBOX_TTL_SECONDS`), which overrides that default. Values must be positive.
Commands do not reset the deadline unless keepalive is explicitly enabled.
`sandbox.set_timeout(600)` resets it to ten minutes from the call and can shorten
or extend it. The broker enforces its hard lifetime from the original claim
creation time (24 hours in US nonprod); extensions cannot reset that cap, and
caller policy may impose a shorter per-request maximum.

Plain assignment leaves the sandbox running after the script exits.
`with Sandbox.create() as sandbox:` and `sandbox.kill()` intentionally delete
it immediately. Job `ttlSecondsAfterFinished` cleans the caller Job/Pod, not the
sandbox. A replacement caller Pod cannot take ownership of the previous Pod's
claim merely by using the same ServiceAccount.

These defaults apply to SDK **0.5.2**. When using older **0.5.0**, the broker
supplies the five-minute default, but pass `keepalive_ttl_seconds=0` explicitly
for fixed-deadline behavior.

Kubernetes normally needs no Python authentication code:

```python
from k8s_agent_sandbox_awx import Sandbox

with Sandbox.create() as sandbox:
    print(sandbox.commands.run("python3 -c 'print(6 * 7)'").stdout)
```

Mount a projected ServiceAccount token at the configured path with its
`audience` set to the broker URL. The file is re-read on every request so token
rotation is honored.

### Cleanup activity

If sandbox creation or readiness fails, upstream attempts a cleanup delete.
The broker overlay marks that delete with `request_purpose: create_cleanup`;
normal explicit deletes are not marked. This is diagnostic client intent only
and never bypasses admission, action-policy or ownership checks. Broker
`v0.10.2` and the console can distinguish create, delete and SDK-reported cleanup
even when caller admission fails. Older records cannot be retroactively labeled.
SDK **0.5.2** includes this cleanup attribution; older **0.5.0** does not.

GitLab supplies the same zero-argument API using a native ID token:

```yaml
job:
  id_tokens:
    AGENT_SANDBOX_TOKEN:
      aud: https://sandbox-broker.example
  variables:
    AGENT_SANDBOX_BROKER_URL: https://sandbox-broker.example
  script:
    - python app.py
```

Explicit authentication objects are available when environment discovery is
not appropriate:

```python
from k8s_agent_sandbox_awx import KubernetesAuth, OIDCTokenAuth, Sandbox

k8s = Sandbox.create(auth=KubernetesAuth("/var/run/secrets/tokens/broker"))
ci = Sandbox.create(auth=OIDCTokenAuth.from_env("MY_ID_TOKEN"))
```

The SDK deliberately has no `provider="gitlab"` or `provider="kubernetes"`
switch. It transports the token; the broker derives the provider from the
verified issuer and applies the matching policy.

## Requirements

- A deployed identity broker service reachable over HTTPS.
- An OIDC token whose audience matches the broker's. Kubernetes projected tokens
  and GitLab `id_tokens:` are supported by the broker policy.
- `k8s-agent-sandbox-base` (installed automatically as a dependency).

## License

Apache-2.0 — see `LICENSE`.
