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
1 change: 1 addition & 0 deletions apps/api/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ python_files = test_suite_v060.py test_suite_v062.py test_suite_v0610.py test_se
test_agent_versions.py
test_agent_redeploy.py
test_package_revocation.py
test_deps_allowlist.py
markers =
no_api_key: test does not require WAYFORTH_TEST_API_KEY (e.g. probes unauthenticated paths)
18 changes: 18 additions & 0 deletions apps/api/routers/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,24 @@ async def rollback_version(request: Request, agent_id: str, db=Depends(get_db))
return {"id": agent_id, "rolled_back_to": out["version_no"], "version_id": out["version_id"]}


def _allowlist_payload() -> dict:
"""The requirements-picker payload: name → sorted versions, + the dep cap. Hashes are
a build-time integrity detail and are dropped here — they NEVER reach the client."""
from services.agent_deps import MAX_DIRECT_DEPS, load_lockfile
lock = load_lockfile() # {name: {version: [hashes]}}
return {"packages": {name: sorted(versions.keys()) for name, versions in lock.items()},
"max_direct_deps": MAX_DIRECT_DEPS}


@router.get("/deps/allowlist")
@limiter.limit("60/minute")
async def deps_allowlist(request: Request, db=Depends(get_db)) -> dict:
"""Installable packages for the requirements picker: name → versions (no hashes)."""
_user_id, _, tier = await _resolve_caller(request, db)
require_tier(tier, "cloud_agents")
return _allowlist_payload()


@router.get("/agents")
@limiter.limit("60/minute")
async def list_agents(request: Request, db=Depends(get_db)) -> dict:
Expand Down
33 changes: 33 additions & 0 deletions apps/api/tests/test_deps_allowlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""test_deps_allowlist.py — GET /cloud/deps/allowlist payload (requirements picker source).

The load-bearing assertion: HASHES NEVER REACH THE CLIENT — the picker gets only package
names + installable versions. Tests the exact transform the endpoint serves.
"""
import json

from routers.cloud import _allowlist_payload
from services.agent_deps import MAX_DIRECT_DEPS, load_lockfile


def test_payload_shape():
p = _allowlist_payload()
assert set(p) == {"packages", "max_direct_deps"}
assert p["max_direct_deps"] == MAX_DIRECT_DEPS
assert isinstance(p["packages"], dict) and p["packages"] # non-empty


def test_no_hashes_reach_the_client():
blob = json.dumps(_allowlist_payload())
assert "sha256" not in blob # not a single hash
# every value is a list of version strings — never the {version: [hashes]} sub-dict
for name, versions in _allowlist_payload()["packages"].items():
assert isinstance(versions, list)
assert all(isinstance(v, str) and "sha256" not in v for v in versions)


def test_names_and_versions_match_the_lockfile():
lock = load_lockfile()
pkgs = _allowlist_payload()["packages"]
assert set(pkgs) == set(lock) # same package names
for name, versions in pkgs.items():
assert versions == sorted(lock[name].keys()) # same versions, sorted
19 changes: 9 additions & 10 deletions docs/agent-editor-frontend-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ mirror is up). When OFF, those endpoints return **`409 versioning_disabled`**.
| Endpoint | Flag-gated? | Testable now? |
|---|---|---|
| `GET /cloud/agents/{id}/versions` | no | **yes** (returns the backfilled v1 today) |
| `GET /cloud/deps/allowlist` *(to add — §2)* | no | yes, once implemented |
| `GET /cloud/deps/allowlist` | no | **yes** (live) |
| `GET /templates`, `GET /templates/{id}` | no | **yes** (already live) |
| `POST /cloud/agents/{id}/deploy` | **yes** | after the flip (409 until then) |
| `POST /cloud/agents/{id}/rollback` | **yes** | after the flip (409 until then) |
Expand Down Expand Up @@ -116,13 +116,12 @@ rebuild). Roll **forward or backward** to any prior version that was actually li

## 2. The requirements picker — the allowlist source

**Answer to "is there a GET, or is it static?":** the allowlist is currently a **static
server-side lockfile** (`services/agent_deps_lock.json`, shape `{name: {version: [hashes]}}`)
with **no public endpoint** yet. The frontend cannot and should not read that file or bundle
a copy (it drifts + leaks hashes). So this contract specifies a small read-only endpoint to
add:
**Answer to "is there a GET, or is it static?":** the allowlist is a static server-side
lockfile (`services/agent_deps_lock.json`, shape `{name: {version: [hashes]}}`). The
frontend must not read that file or bundle a copy (it drifts + leaks hashes), so it's
served — hashes stripped — by a dedicated endpoint:

### `GET /cloud/deps/allowlist` — **NOT YET IMPLEMENTED (trivial to add)**
### `GET /cloud/deps/allowlist` — **live**
**Response `200`**
```json
{
Expand All @@ -142,9 +141,9 @@ add:
`name==version` line into `requirements`. Anything not in this map will be rejected at
deploy with `not_allowed` (§4) — so gate the "add" button on this list.

> **Action needed (one-liner, your call):** implement `GET /cloud/deps/allowlist` returning
> the lockfile minus hashes. Until then, the picker has no data source. Flagging it here so
> it's not discovered mid-build.
> **Auth + security:** authenticated `cloud_agents` tier (same as the rest of `/cloud/*`).
> Hashes are a build-time integrity detail and are **never** sent — the payload is only
> names + versions.

---

Expand Down
Loading