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
65 changes: 62 additions & 3 deletions gateway/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9960,6 +9960,8 @@ async def _hosted_scrub_removed(org: str, hid: str, before: list[dict], after: l
scrubbed += 1
if scrubbed:
print(f"[sql] {hid}: disconnected ({scrubbed} record(s) scrubbed)", flush=True)
for key in sorted(gone):
await _connection_vertex_drop(hid, key)


async def _mcp_write(hid: str, servers: list[dict]) -> None:
Expand Down Expand Up @@ -10164,6 +10166,53 @@ async def _db_validate(engine: str, conn: str) -> tuple[str, str, str, str]:
return eng, c, host, db


# ── the connection, on the graph ─────────────────────────────────────────────────────────────
# What a harness is connected to is configuration, and configuration is listed, shown and
# managed; a vault record is none of those things. So the attach below writes two things: the
# connection string into the vault (the only place a credential ever sits) and this vertex, which
# holds everything else and names the vault key. One per harness entry, rewritten on reconnect.
_CONNECTION_LABEL = "HarnessConnection"


def _connection_vertex_id(hid: str, entry_id: str) -> str:
return "hconn_" + hashlib.sha1(f"{hid}|{entry_id}".encode()).hexdigest()[:24]


async def _connection_vertex_put(org: str, hid: str, v: dict | None, entry_id: str, key: str,
engine: str, host: str, db: str, sample_rows: bool) -> None:
now = str(int(time.time() * 1000))
vid = _connection_vertex_id(hid, entry_id)
prev = await _vertex_get(vid)
await _vg_upsert(_CONNECTION_LABEL, vid, {
"org": org, "workspace": str((v or {}).get("workspace") or ""), "harness": hid, "entry": entry_id,
"kind": "database", "server": "database", "engine": engine, "host": host, "database": db,
"sample_rows": "1" if sample_rows else "0", "secret_key": key,
"created_at": str((prev or {}).get("created_at") or now), "updated_at": now, "deleted": "0"})


async def _connection_vertex_drop(hid: str, key: str) -> None:
"""The entry is gone, so the configuration is gone with it (the record was scrubbed already)."""
with contextlib.suppress(Exception):
for row in await BACKING.graph.find(_CONNECTION_LABEL, {"harness": hid, "secret_key": key}):
vid = str(row.get("id") or "")
if vid:
await _vg_upsert(_CONNECTION_LABEL, vid, {"deleted": "1", "updated_at": str(int(time.time() * 1000))})


async def _connections_of(org: str, hid: str) -> list[dict]:
"""A harness's connections as the console may see them: never the credential."""
out = []
for row in await BACKING.graph.find(_CONNECTION_LABEL, {"harness": hid, "org": org}):
if str(row.get("deleted") or "0") in ("1", "true", "True"):
continue
out.append({"id": str(row.get("id") or ""), "entry": str(row.get("entry") or ""),
"kind": str(row.get("kind") or ""), "engine": str(row.get("engine") or ""),
"host": str(row.get("host") or ""), "database": str(row.get("database") or ""),
"sampleRows": str(row.get("sample_rows") or "0") == "1",
"updatedAt": int(str(row.get("updated_at") or 0) or 0)})
return out


async def _hosted_db_entry(hid: str, v: dict | None, decl: dict) -> None:
"""The database ENTRY, with no credential in it.

Expand Down Expand Up @@ -10230,6 +10279,7 @@ async def _hosted_db_attach(org: str, hid: str, v: dict | None, decl: dict,
"auth": f"vault:{key}",
"enabled": str(prev.get("enabled", True)) not in ("False", "false", "0")}
await _mcp_write(hid, [e for e in cur if str(e.get("id") or "") != decl["id"]] + [entry])
await _connection_vertex_put(org, hid, v, decl["id"], key, engine, host, db, sample_rows)
# host and database only. The credential is not printed, here or anywhere.
print(f"[sql] {hid}: connected {engine} {host}/{db} "
f"(sample rows {'on' if sample_rows else 'off'})", flush=True)
Expand Down Expand Up @@ -14840,8 +14890,12 @@ async def list_kits(request: Request) -> dict:
org = p.get("org", "")
if not org:
raise uhp_error(401, "invalid_credential", "Missing or invalid API key.")
# One kit, one Harness PER WORKSPACE: a launched kit's Harness belongs to the workspace it was
# launched in, so "launched" here is a fact about the caller's workspace, not the org.
ws, wsd = str(p.get("workspace") or ""), bool(p.get("workspace_default"))
rows = await _vg_list_by_org("Harness", org)
by_kit = {str(r.get("kit") or ""): r for r in rows if str(r.get("deleted") or "0") != "1"}
by_kit = {str(r.get("kit") or ""): r for r in rows
if str(r.get("deleted") or "0") != "1" and _workspace_keep(str(r.get("workspace") or ""), ws, wsd)}
out = []
# Manifest order, NOT alphabetical. kits.json lists the kits in the order the catalogue wants
# them shown, install-kits.sh bundles them in that order, and _kits() builds its dict by
Expand Down Expand Up @@ -14978,16 +15032,21 @@ async def launch_kit(kit_id: str, request: Request, body_in: KitLaunchBody | Non
# a corrected form, not a half-configured Harness to find and fix.
checked = await _db_validate(db_in.engine, db_in.connection_string) if db_in else None

# Per workspace (see list_kits): launching in a second workspace makes that workspace its own
# Harness rather than handing back the first workspace's, which its members could not see.
ws, wsd = str(p.get("workspace") or ""), bool(p.get("workspace_default"))
existing = next((r for r in await _vg_list_by_org("Harness", org)
if str(r.get("kit") or "") == kit_id and str(r.get("deleted") or "0") != "1"),
if str(r.get("kit") or "") == kit_id and str(r.get("deleted") or "0") != "1"
and _workspace_keep(str(r.get("workspace") or ""), ws, wsd)),
None)
want_h = (body_in.harness if body_in else "").strip()
if want_h and want_h != str((existing or {}).get("id") or ""):
# Run the kit on a Harness the person already has. One kit, one Harness: the previous kit
# Harness keeps its sessions and its package but is no longer the one the app talks to,
# and a Harness already running another kit is not taken over.
target = await _vertex_get(want_h)
if not target or str(target.get("org") or "") != org or str(target.get("deleted") or "0") == "1":
if not target or str(target.get("org") or "") != org or str(target.get("deleted") or "0") == "1" \
or not _workspace_keep(str(target.get("workspace") or ""), ws, wsd):
raise uhp_error(404, "harness_not_found", "No harness with that id.", "harness")
other = str(target.get("kit") or "")
if other and other != kit_id:
Expand Down
118 changes: 118 additions & 0 deletions gateway/tests/test_kits_workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""A starter kit launches once per WORKSPACE: a launched kit's Harness belongs to the workspace it
was launched in, so a second workspace gets its own rather than the first workspace's, which its
members could not see. And a database connection's configuration is recorded on the graph beside
the vault record that alone holds the credential."""
from __future__ import annotations

import asyncio
import json
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import app as gw # noqa: E402

KIT = {"id": "slides", "title": "Slides", "harness": {"name": "Slides", "mcp_servers": [],
"recommended": [{"base": "hermes", "model": "deepseek-v4-pro"}]}, "app": {"route": "/kits/slides"}}
DB_KIT = {"id": "dashboard", "title": "Dashboards", "harness": {"name": "Dashboards", "mcp_servers": [],
"launch": {"database": {"engines": ["postgres"], "name": "database", "id": "mcp.database"}},
"recommended": [{"base": "hermes", "model": "deepseek-v4-pro"}]}, "app": {"route": "/kits/dashboard"}}


class _Req:
headers: dict = {}


@pytest.fixture
def world(monkeypatch):
store: dict = {}
who = {"org": "org.a", "member": "m@a", "workspace": "org.a__ws1", "workspace_default": False}

async def _principal(request):
return dict(who)

async def _vg_list_by_org(label, org):
return [dict(v, id=k) for k, v in store.items() if v.get("org") == org]

async def _vertex_get(vid):
return dict(store[vid], id=vid) if vid in store else None

async def _vg_upsert(label, vid, props, **kw):
store.setdefault(vid, {}).update(props)

async def _find(label, eq=None, neq=None):
rows = [dict(v, id=k) for k, v in store.items()]
return [r for r in rows if all(str(r.get(k)) == str(v) for k, v in (eq or {}).items())
and all(str(r.get(k)) != str(v) for k, v in (neq or {}).items())]

async def _servable_models(org, backend):
return None

async def _skills_prepare(skills):
return list(skills or [])

async def _plugins_prepare(body, org, previous=None, reserved_mcp=()):
return []

async def _mcp_migrate(org, hid, v):
return v
monkeypatch.setattr(gw.BACKING.graph, "find", _find)
for name, fn in (("_principal", _principal), ("_vg_list_by_org", _vg_list_by_org), ("_vertex_get", _vertex_get),
("_vg_upsert", _vg_upsert), ("_servable_models", _servable_models), ("_skills_prepare", _skills_prepare),
("_plugins_prepare", _plugins_prepare), ("_mcp_migrate", _mcp_migrate)):
monkeypatch.setattr(gw, name, fn)
monkeypatch.setattr(gw, "_kits", lambda: {"slides": KIT, "dashboard": DB_KIT})
monkeypatch.setattr(gw, "_kit_plugin", lambda kit: None)
monkeypatch.setattr(gw, "_kit_skills", lambda kit: [])
return store, who


def test_a_kit_launches_once_per_workspace(world):
store, who = world
first = asyncio.run(gw.launch_kit("slides", _Req(), None))
again = asyncio.run(gw.launch_kit("slides", _Req(), None))
assert first["created"] and not again["created"] and again["harnessId"] == first["harnessId"]
who["workspace"] = "org.a__ws2"
other = asyncio.run(gw.launch_kit("slides", _Req(), None))
assert other["created"] and other["harnessId"] != first["harnessId"]
assert store[other["harnessId"]]["workspace"] == "org.a__ws2"
slides = next(k for k in asyncio.run(gw.list_kits(_Req()))["kits"] if k["id"] == "slides")
assert slides["launched"] and slides["harnessId"] == other["harnessId"]
who["workspace"] = "org.a__ws3"
assert not next(k for k in asyncio.run(gw.list_kits(_Req()))["kits"] if k["id"] == "slides")["launched"]


def test_a_legacy_unstamped_kit_harness_belongs_to_the_default_workspace(world):
store, who = world
store["chrn_old"] = {"org": "org.a", "kit": "slides", "workspace": "", "deleted": "0", "name": "Slides", "base": "hermes"}
who.update(workspace="org.a__hr_default", workspace_default=True)
assert asyncio.run(gw.launch_kit("slides", _Req(), None))["harnessId"] == "chrn_old"
who.update(workspace="org.a__ws9", workspace_default=False)
assert asyncio.run(gw.launch_kit("slides", _Req(), None))["harnessId"] != "chrn_old"


def test_a_database_connection_is_recorded_on_the_graph_without_its_credential(world, monkeypatch):
store, who = world
records = {}

async def _db_validate(engine, conn):
return "postgres", conn, "db.example", "shop"

async def _hosted_put_record(org, key, record, *, secret=True, param="connection_string"):
records[key] = record

async def _mcp_write(hid, servers):
store[hid]["mcp_servers"] = json.dumps(servers)
monkeypatch.setattr(gw, "_db_validate", _db_validate)
monkeypatch.setattr(gw, "_hosted_put_record", _hosted_put_record)
monkeypatch.setattr(gw, "_mcp_write", _mcp_write)
monkeypatch.setattr(gw, "_own_origins", lambda: ["https://api.example"])
body = gw.KitLaunchBody(database=gw.KitDatabaseBody(engine="postgres", connection_string="postgresql://u:p@db.example/shop", sample_rows=False))
hid = asyncio.run(gw.launch_kit("dashboard", _Req(), body))["harnessId"]
key = next(iter(records))
conns = [v for v in store.values() if v.get("kind") == "database"]
assert len(conns) == 1 and conns[0]["harness"] == hid and conns[0]["secret_key"] == key
assert conns[0]["workspace"] == "org.a__ws1" and conns[0]["database"] == "shop" and "u:p@" not in json.dumps(conns[0])
assert asyncio.run(gw._connections_of("org.a", hid))[0]["database"] == "shop"
Loading