diff --git a/test-tools/ascii-renderer/manifest.toml b/test-tools/ascii-renderer/manifest.toml index b9507ac605..d7a3fcdeb9 100644 --- a/test-tools/ascii-renderer/manifest.toml +++ b/test-tools/ascii-renderer/manifest.toml @@ -29,6 +29,7 @@ id = "ascii-renderer.draw" description = "Render a small piece of ASCII art. Optional `subject` selects the drawing (cat, dog, robot); defaults to robot. Takes no network access." effects = ["dispatch_capability"] default_permission = "allow" +origin_gate_matrix = { loop_run = "gated_unless_granted", product = "forbidden", automation = "forbidden" } visibility = "model" input_schema_ref = "schemas/ascii-renderer/draw.input.v1.json" output_schema_ref = "schemas/ascii-renderer/draw.output.v1.json" diff --git a/test-tools/hacker-news/manifest.toml b/test-tools/hacker-news/manifest.toml index 9dabb8f2ee..d8c647954c 100644 --- a/test-tools/hacker-news/manifest.toml +++ b/test-tools/hacker-news/manifest.toml @@ -32,6 +32,7 @@ id = "hacker-news.top_stories" description = "Return the current top Hacker News stories (title, url, score, author, comment count). Optional `limit` (1-10, default 5)." effects = ["dispatch_capability", "network"] default_permission = "allow" +origin_gate_matrix = { loop_run = "gated_unless_granted", product = "forbidden", automation = "forbidden" } visibility = "model" input_schema_ref = "schemas/hacker-news/top_stories.input.v1.json" output_schema_ref = "schemas/hacker-news/top_stories.output.v1.json" diff --git a/test-tools/market-data/manifest.toml b/test-tools/market-data/manifest.toml index 0cccc11d8f..de6aa52e5b 100644 --- a/test-tools/market-data/manifest.toml +++ b/test-tools/market-data/manifest.toml @@ -33,6 +33,7 @@ id = "market-data.snp500" description = "Get the current S&P 500 (SPX) index snapshot — level, daily change, and percent change. Takes no arguments." effects = ["dispatch_capability", "network", "use_secret"] default_permission = "allow" +origin_gate_matrix = { loop_run = "gated_unless_granted", product = "forbidden", automation = "forbidden" } visibility = "model" input_schema_ref = "schemas/market-data/snp500.input.v1.json" output_schema_ref = "schemas/market-data/snp500.output.v1.json" diff --git a/tests/e2e/reborn_webui_harness.py b/tests/e2e/reborn_webui_harness.py index 9f4726f6df..2c246270c5 100644 --- a/tests/e2e/reborn_webui_harness.py +++ b/tests/e2e/reborn_webui_harness.py @@ -206,9 +206,11 @@ async def kill_reborn_server(proc) -> None: await stop_process(proc, sig=signal.SIGKILL, timeout=5) -async def enable_reborn_global_auto_approve(base_url: str) -> None: +async def enable_reborn_global_auto_approve( + base_url: str, *, token: str = REBORN_V2_AUTH_TOKEN +) -> None: """Enable the Tools settings global auto-approve switch for this test user.""" - async with httpx.AsyncClient(headers=reborn_bearer_headers()) as client: + async with httpx.AsyncClient(headers=reborn_bearer_headers(token)) as client: response = await client.post( f"{base_url}/api/webchat/v2/settings/tools", json={"enabled": True}, @@ -417,8 +419,8 @@ async def open_reborn_v2_page(page, base_url: str, path: str = "/") -> None: await page.wait_for_selector(SEL_V2["chat_composer"], timeout=15000) -def reborn_bearer_headers() -> dict[str, str]: - return {"Authorization": f"Bearer {REBORN_V2_AUTH_TOKEN}"} +def reborn_bearer_headers(token: str = REBORN_V2_AUTH_TOKEN) -> dict[str, str]: + return {"Authorization": f"Bearer {token}"} def client_action_id() -> str: diff --git a/tests/e2e/scenarios/test_reborn_private_tool_installs.py b/tests/e2e/scenarios/test_reborn_private_tool_installs.py index 059d37ba61..ef9ebe22e8 100644 --- a/tests/e2e/scenarios/test_reborn_private_tool_installs.py +++ b/tests/e2e/scenarios/test_reborn_private_tool_installs.py @@ -30,6 +30,7 @@ import httpx from reborn_webui_harness import ( create_thread, + enable_reborn_global_auto_approve, reborn_bearer_headers, reborn_v2_private_installs_yolo_server, # noqa: F401 - imported fixture send_and_settle, @@ -107,22 +108,28 @@ async def test_private_tool_installs_full_path( reborn_v2_private_installs_yolo_server, test_tool_zips ): base_url = reborn_v2_private_installs_yolo_server - - async with httpx.AsyncClient( - base_url=base_url, headers=reborn_bearer_headers(), timeout=15 - ) as operator: - # 1. Operator imports the three test-tools/ fixture bundles. - for tool_id in ("ascii-renderer", "hacker-news", "market-data"): - await _import_tool(operator, base_url, test_tool_zips[tool_id]) - - # 2. Operator installs + activates ascii-renderer tenant-wide. - await _install_and_activate(operator, base_url, "ascii-renderer") - - # 3. Operator creates alice and bob. - alice = await _create_member_user(operator, base_url, display_name="Alice") - bob = await _create_member_user(operator, base_url, display_name="Bob") - + alice = None + bob = None try: + async with httpx.AsyncClient( + base_url=base_url, headers=reborn_bearer_headers(), timeout=15 + ) as operator: + # 1. Operator imports the three test-tools/ fixture bundles. + for tool_id in ("ascii-renderer", "hacker-news", "market-data"): + await _import_tool(operator, base_url, test_tool_zips[tool_id]) + + # 2. Operator installs + activates ascii-renderer tenant-wide. + await _install_and_activate(operator, base_url, "ascii-renderer") + + # 3. Operator creates alice and bob. + alice = await _create_member_user(operator, base_url, display_name="Alice") + bob = await _create_member_user(operator, base_url, display_name="Bob") + + # Auto-approve is caller-scoped, so the fixture's operator setting does + # not grant it to newly-created members. + await enable_reborn_global_auto_approve(base_url, token=alice["token"]) + await enable_reborn_global_auto_approve(base_url, token=bob["token"]) + async with _user_client(base_url, alice["token"]) as alice_client: # 4. Alice privately installs + activates hacker-news. await _install_and_activate(alice_client, base_url, "hacker-news") @@ -207,11 +214,13 @@ async def test_private_tool_installs_full_path( async with httpx.AsyncClient( base_url=base_url, headers=reborn_bearer_headers(), timeout=15 ) as operator: - alice_delete = await operator.delete( - f"{base_url}{ADMIN_BASE}/users/{alice['user_id']}" - ) - bob_delete = await operator.delete( - f"{base_url}{ADMIN_BASE}/users/{bob['user_id']}" - ) - assert alice_delete.status_code == 200, alice_delete.text - assert bob_delete.status_code == 200, bob_delete.text + if alice is not None: + alice_delete = await operator.delete( + f"{base_url}{ADMIN_BASE}/users/{alice['user_id']}" + ) + assert alice_delete.status_code == 200, alice_delete.text + if bob is not None: + bob_delete = await operator.delete( + f"{base_url}{ADMIN_BASE}/users/{bob['user_id']}" + ) + assert bob_delete.status_code == 200, bob_delete.text