Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/local-backends.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ jobs:
- run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Lint
run: npm run lint
- name: Smoke suite
run: npm run test:smoke
- name: Static export
Expand Down
4 changes: 2 additions & 2 deletions src/agent_env/explorer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from agent_env.config import get_config, get_runner, runtime
from agent_env.config.errors import ConfigError
from agent_env.explorer.entity_ids import EncodedIdRouting
from agent_env.explorer.openapi_docs import docs_metadata, enrich_openapi_schema
from agent_env.explorer.openapi_docs import docs_metadata, enrich_openapi_schema, package_version
from agent_env.explorer.plugin import load_plugins
from agent_env.explorer.routers import conversations as conversations_router
from agent_env.explorer.routers import objects as objects_router
Expand Down Expand Up @@ -104,7 +104,7 @@ def create_app(static_dir: Optional[str] = None) -> FastAPI:
title="agent-env explorer",
description="Local control plane for agent-env: browse environments, tasks, "
"agents, universes and evals, and run tasks.",
version="0.1.0",
version=package_version("agentenv-framework"),
lifespan=lifespan,
# Swagger under /api; the UI owns /docs. openapi_url stays at the root for the UI.
docs_url="/api/docs",
Expand Down
6 changes: 3 additions & 3 deletions src/agent_env/explorer/openapi_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def enrich_openapi_schema(schema: dict[str, Any]) -> dict[str, Any]:
return schema


def _package_version(name: str) -> str:
def package_version(name: str) -> str:
try:
return pkg_version(name)
except PackageNotFoundError:
Expand All @@ -236,7 +236,7 @@ def docs_metadata(schema: dict[str, Any]) -> dict[str, Any]:
"generated_at": datetime.now(timezone.utc).isoformat(),
"openapi_version": schema.get("openapi", ""),
"versions": {
"agentenv-framework": _package_version("agentenv-framework"),
"agentenv-protocol": _package_version("agentenv-framework-protocol") or _package_version("agentenv-protocol"),
"agentenv-framework": package_version("agentenv-framework"),
"agentenv-protocol": package_version("agentenv-framework-protocol") or package_version("agentenv-protocol"),
},
}
11 changes: 7 additions & 4 deletions src/agent_env/explorer/routers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,13 @@ def list_versions(
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
) -> list[dict]:
"""Every stored version of one entity, newest first. Returns a bare array (not a
PaginatedResponse) — clients expect a list here."""
"""Every stored version of one entity, newest first, as a bare list; 404 for an unknown id."""
store = docs()
filt = Filter.of(**{id_field: entity_id})
return docs().query(collection, filt, sort=Sort.by("version", descending=True),
limit=limit, offset=offset)
page = store.query(collection, filt, sort=Sort.by("version", descending=True),
limit=limit, offset=offset)
if not page and store.find_one(collection, filt) is None:
raise HTTPException(status_code=404, detail=f"{tag} {entity_id} not found")
return page

return router
18 changes: 12 additions & 6 deletions src/agent_env/explorer/routers/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ def object_content(object_url: str = Query(...)):
return response


def _filename(object_url: str) -> str:
return object_url.rstrip("/").rsplit("/", 1)[-1] or "object"


def _content_type(object_url: str, meta) -> str:
"""The stored content type, else one guessed from the filename (local stores keep none)."""
return meta.content_type or mimetypes.guess_type(_filename(object_url))[0] or "application/octet-stream"


def _streamed(tmp: str, object_url: str, meta) -> StreamingResponse:
def _stream_and_cleanup():
try:
Expand All @@ -80,13 +89,10 @@ def _stream_and_cleanup():
finally:
os.unlink(tmp)

filename = object_url.rstrip("/").rsplit("/", 1)[-1] or "object"
# Local stores don't persist a content type; fall back to the filename so both the
# media type and the sandbox decision below are correct on every backend.
content_type = meta.content_type or mimetypes.guess_type(filename)[0] or "application/octet-stream"
content_type = _content_type(object_url, meta)
headers = {
"X-Content-Type-Options": "nosniff", # untrusted artifact bytes must not sniff-execute
"Content-Disposition": _content_disposition(filename),
"Content-Disposition": _content_disposition(_filename(object_url)),
}
if content_type.split(";")[0].strip().lower() in _SANDBOX_TYPES:
headers["Content-Security-Policy"] = "sandbox"
Expand All @@ -109,4 +115,4 @@ def object_metadata(object_url: str = Query(...)):
meta = store.get_object_metadata_at(object_url)
if meta is None:
raise HTTPException(status_code=404, detail="no object at the given object_url")
return {"size_bytes": meta.size, "content_type": meta.content_type}
return {"size_bytes": meta.size, "content_type": _content_type(object_url, meta)}
28 changes: 24 additions & 4 deletions src/agent_env/explorer/ui/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
# AgentEnv Explorer (UI)

The standalone web UI for the local AgentEnv control plane (`agent-env up`).
The web UI for the local AgentEnv control plane that `agent-env up` starts: browse environments,
universes, tasks and agents, start runs, and inspect their results, trajectories and triggers.
The user guide is at [www.agentenvframework.com/docs](https://www.agentenvframework.com/docs).

> **Docs are a work in progress.** A proper getting-started guide and feature
> walkthrough will land alongside the OSS release. For now, see
> `agent-env up --help` and the `[explorer]` section of `.agentenv/config.toml`.
## Develop

With `agent-env up` running, start the dev server from this folder:

```bash
npm ci
npm run dev # http://localhost:3000
```

The dev server proxies `/api/v1`, `/health` and `/openapi.json` to `http://127.0.0.1:8234`, the
explorer's default address. To point it somewhere else, copy `.env.example` to `.env` and set
`AGENT_ENV_HUB_BACKEND_PROXY_URL`.

## Build

```bash
npm run build:static # writes out/
```

`agent-env up` serves the built UI when `[explorer] static_dir` in `.agentenv/config.toml` points
at `out/`. Checks: `npm run typecheck`, `npm run test:smoke` and `npm run lint`.
10 changes: 10 additions & 0 deletions src/agent_env/explorer/ui/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FlatCompat } from '@eslint/eslintrc';

const compat = new FlatCompat({ baseDirectory: dirname(fileURLToPath(import.meta.url)) });

export default [

Check warning on line 7 in src/agent_env/explorer/ui/eslint.config.mjs

View workflow job for this annotation

GitHub Actions / ui

Assign array to a variable before exporting as module default
{ ignores: ['.next/**', 'out/**', 'node_modules/**', 'next-env.d.ts'] },
...compat.extends('next/core-web-vitals', 'next/typescript'),
];
Loading
Loading