"""Tests for the .swarm bundle engine: skill round-trip, secret redaction, and the zip-hardening rejections. The skills store writes to ~/.claude/skills, so we monkeypatch it into a temp dir per test (the conftest only isolates browser state).""" import io import json import os import zipfile import pytest from backend.apps.skills import skills as store from backend.apps.swarm import closure from backend.apps.swarm.models import EntityType from backend.apps.swarm.redact import find_denied_keys, scrub_payload from backend.apps.swarm.ziputil import BundleError, pack, unpack @pytest.fixture def skill_store(tmp_path, monkeypatch): d = tmp_path / "skills" d.mkdir() monkeypatch.setattr(store, "SKILLS_DIR", str(d)) monkeypatch.setattr(store, "INDEX_PATH", str(d / ".skills_index.json")) return d def p_make_skill(d, slug, name, content, description="desc"): (d / f"{slug}.md").write_text(content, encoding="utf-8") index = store.load_index() index[slug] = {"name": name, "description": description, "command": slug} store.save_index(index) def test_skill_export_import_round_trip(skill_store): p_make_skill(skill_store, "my-skill", "My Skill", "# hello\nbody text") raw, name = closure.build_bundle(EntityType.skill, "my-skill") assert name == "My Skill" assert zipfile.is_zipfile(io.BytesIO(raw)) sandbox, manifest, warnings = closure.stage_upload(raw, "My Skill.swarm") try: assert manifest.root.type == EntityType.skill root_type, root_id, created, unresolved = closure.commit(sandbox, manifest, []) finally: import shutil shutil.rmtree(sandbox, ignore_errors=True) # Original is untouched, import lands under a fresh, non-clobbering slug. assert root_type == EntityType.skill assert root_id != "my-skill" assert (skill_store / "my-skill.md").exists() # original flat skill untouched assert (skill_store / root_id / "SKILL.md").read_text(encoding="utf-8") == "# hello\nbody text" assert created == {"skill": [root_id]} def test_bare_markdown_import(skill_store): sandbox, manifest, warnings = closure.stage_upload(b"# Just markdown", "Cool Trick.md") try: assert manifest.root.type == EntityType.skill assert manifest.root.name == "Cool Trick" p_t, root_id, created, p_u = closure.commit(sandbox, manifest, []) finally: import shutil shutil.rmtree(sandbox, ignore_errors=True) assert (skill_store / root_id / "SKILL.md").read_text(encoding="utf-8") == "# Just markdown" def test_content_secret_redacted_in_bundle(skill_store): secret = "sk-ant-api03-AAAAAAAAAAAAAAAAAAAAAAAA" p_make_skill(skill_store, "leaky", "Leaky", f"use this key: {secret}") raw, p_name = closure.build_bundle(EntityType.skill, "leaky") # Inspect the actual packed payload (zip entries are compressed, so grepping the raw bytes proves nothing). with zipfile.ZipFile(io.BytesIO(raw)) as zf: payload_name = next(n for n in zf.namelist() if n.endswith("payload.json")) payload = json.loads(zf.read(payload_name)) assert secret not in payload["content"] assert "[redacted]" in payload["content"] def test_redaction_drops_denied_keys(): payload = { "name": "ok", "anthropic_api_key": "sk-ant-secret", "nested": {"openswarm_bearer_token": "abc", "keep": 1}, "list": [{"oauth_tokens": {"x": 1}}, {"fine": 2}], } cleaned = scrub_payload(payload) assert find_denied_keys(cleaned) == [] assert cleaned["name"] == "ok" assert cleaned["nested"]["keep"] == 1 assert cleaned["list"][1]["fine"] == 2 def test_pack_refuses_denied_key(): # Defense in depth: even if redaction were skipped, pack must not ship a secret. with pytest.raises(BundleError): pack({"format_version": 1}, {"bid1": {"api_key": "leak"}}, {}) def test_pack_refuses_secret_in_workspace_file(): # A key hardcoded in app source (not .env) must not ride along; pack scans file bytes, not just payload keys. leak = b"const KEY = 'sk-ant-api03-AAAAAAAAAAAAAAAAAAAAAAAA';\n" with pytest.raises(BundleError): pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/config.js": leak}) def test_pack_allows_clean_workspace_file(): raw = pack({"format_version": 1}, {"bid1": {"name": "ok"}}, {"entities/bid1/files/app.js": b"export default 1"}) assert zipfile.is_zipfile(io.BytesIO(raw)) def test_app_export_drops_machine_env(tmp_path, monkeypatch): # The live .env holds the source machine's absolute paths + pinned port; it must never ride along. .env.example (portable) does. from backend.apps.swarm.entities import apps as appmod from backend.apps.outputs.models import Output ws = tmp_path / "ws" (ws / "frontend").mkdir(parents=True) (ws / ".env").write_text("FRONTEND_PORT=5\nOPENSWARM_TEMPLATE_BACKEND_PATH=/Users/SECRET/x\n") (ws / ".env.example").write_text("BACKEND_PORT=NONE\nFRONTEND_PORT=4949\n") (ws / "frontend" / "App.tsx").write_text("export default () => null") monkeypatch.setattr(appmod, "OUTPUTS_WORKSPACE_DIR", str(tmp_path)) ex = appmod.AppExportable(Output(name="A", workspace_id="ws")) files = ex.files() assert "workspace/.env.example" in files assert "workspace/.env" not in files assert "workspace/frontend/App.tsx" in files assert b"/Users/SECRET" not in b"".join(files.values()) def test_workflow_sanitize_disables_schedule_and_strips_pii(): from backend.apps.swarm.entities.workflows import sanitize_workflow raw = { "id": "wf123", "title": "Daily digest", "steps": [{"id": "s1", "text": "do thing"}], "schedule": {"enabled": True, "runs_count": 5, "next_run_at": "2026-01-01T00:00:00", "hour": 9}, "permissions": [{"kind": "text", "after_minutes": 30, "phone": "+15551234567"}], "source_session_id": "sess1", "dashboard_id": "dash1", "last_run_status": "success", "mode": "agent", "provider": "anthropic", } out = sanitize_workflow(raw) # An imported workflow must not auto-run or carry the sharer's identity. assert out["schedule"]["enabled"] is False assert out["schedule"]["runs_count"] == 0 assert out["schedule"]["hour"] == 9 # cadence shape preserved assert out["permissions"][0]["phone"] is None for dropped in ("id", "source_session_id", "dashboard_id", "last_run_status"): assert dropped not in out assert out["title"] == "Daily digest" def test_workflow_round_trips_through_the_store(isolated_workflows_data): # The workflow store landed on this branch, so a workflow bundle imports into an (isolated) store: an unknown id loads as None, import_ creates a fresh row with its schedule forced OFF (so an imported workflow never auto-runs on someone else's machine), and load reads it back. Supersedes test_workflow_unavailable_on_this_branch, which dated from before the workflow store was on eric/dev. from backend.apps.swarm.entities.workflows import WorkflowExportable from backend.apps.swarm.exportable import RemapTable from backend.apps.workflows import storage assert WorkflowExportable.load("nonexistent") is None new_id = WorkflowExportable.import_( {"title": "Shared WF", "schedule": {"enabled": True}}, {}, RemapTable() ) assert new_id loaded = WorkflowExportable.load(new_id) assert loaded is not None assert loaded.name == "Shared WF" # Read the persisted row back through the store's public API (not the entity's private data) to confirm the schedule was forced off on import. saved = storage.get_workflow(new_id) assert saved is not None and saved.schedule.enabled is False def test_session_export_carries_transcript_drops_runtime_and_secrets(): from backend.apps.swarm.entities.SessionExportable import SessionExportable from backend.apps.swarm.redact import scrub_payload data = { "name": "A", "provider": "anthropic", "model": "sonnet", "mode": "agent", "system_prompt": "hi", "allowed_tools": ["Read"], "messages": [ {"id": "m1", "role": "user", "content": "private chat", "branch_id": "main"}, {"id": "m2", "role": "assistant", "content": "token is sk-ant-abcdefghij0123456789"}, ], "branches": {"main": {"id": "main", "parent_branch_id": None, "fork_point_message_id": None}}, "active_branch_id": "main", "tool_group_meta": {"g1": {"label": "x"}}, "active_mcps": ["Gmail"], "cwd": "/Users/me/repo", "cost_usd": 9.9, "sdk_session_id": "x", } ex = SessionExportable("s1", "A", data) out = ex.serialize(None) # The transcript now rides along, that's the point of sharing an agent. assert out["messages"][0]["content"] == "private chat" assert out["active_branch_id"] == "main" and "main" in out["branches"] assert out["tool_group_meta"] == {"g1": {"label": "x"}} # Runtime, identity, and gate state still never leave. for gone in ("cwd", "active_mcps", "cost_usd", "sdk_session_id"): assert gone not in out # The closure runs scrub_payload on every payload, so a secret-shaped string sitting in the transcript is redacted before it ships. assert "sk-ant-" not in json.dumps(scrub_payload(out)) reqs = ex.requirements() assert any(r.kind.value == "mcp_action" and r.key == "Gmail" for r in reqs) def test_session_import_restores_transcript_without_granting_mcp(monkeypatch): from backend.apps.swarm.entities.SessionExportable import SessionExportable from backend.apps.swarm.exportable import RemapTable from backend.apps.agents.manager.session import session_store saved: dict = {} monkeypatch.setattr(session_store, "save_session", lambda sid, doc: saved.update({sid: doc})) payload = { "name": "A", "model": "sonnet", "mode": "agent", "messages": [{"id": "m1", "role": "user", "content": "hi", "branch_id": "main"}], "branches": {"main": {"id": "main", "parent_branch_id": None, "fork_point_message_id": None}}, "active_branch_id": "main", "tool_group_meta": {"g1": {"label": "x"}}, } sid = SessionExportable.import_(payload, {}, RemapTable()) doc = saved[sid] assert doc["messages"][0]["content"] == "hi" assert doc["active_branch_id"] == "main" assert doc["tool_group_meta"] == {"g1": {"label": "x"}} # The gate stays shut: a shared agent never arrives with MCP access. assert doc["active_mcps"] == [] # The dashboard import re-points this; it must never be the sharer's id. assert doc["dashboard_id"] is None def test_session_import_old_bundle_without_transcript(monkeypatch): # A bundle made before transcripts were carried has no messages; it must still import as a valid empty-history agent (single main branch), not crash. from backend.apps.swarm.entities.SessionExportable import SessionExportable from backend.apps.swarm.exportable import RemapTable from backend.apps.agents.manager.session import session_store saved: dict = {} monkeypatch.setattr(session_store, "save_session", lambda sid, doc: saved.update({sid: doc})) sid = SessionExportable.import_({"name": "Old", "model": "sonnet"}, {}, RemapTable()) doc = saved[sid] assert doc["messages"] == [] assert doc["active_branch_id"] == "main" and "main" in doc["branches"] def test_session_load_prefers_live_memory_over_stale_disk(tmp_path, monkeypatch): # The freshest transcript lives in memory; a disk-only load would ship a stale one. load() must read the live session first, disk only as fallback. from backend.apps.agents import agent_manager as am from backend.apps.swarm.entities.SessionExportable import SessionExportable sdir = tmp_path / "sessions" sdir.mkdir() monkeypatch.setattr(am, "SESSIONS_DIR", str(sdir)) (sdir / "s1.json").write_text(json.dumps( {"name": "Stale", "messages": [{"id": "old", "role": "user", "content": "old"}]})) class FakeSess: def model_dump(self, mode="json"): return {"name": "Live", "messages": [ {"id": "old", "role": "user", "content": "old"}, {"id": "new", "role": "assistant", "content": "fresh turn"}, ]} monkeypatch.setattr(am.agent_manager, "sessions", {"s1": FakeSess()}) out = SessionExportable.load("s1").serialize(None) assert out["name"] == "Live" # not the stale disk copy assert len(out["messages"]) == 2 # the unflushed turn is included def test_dashboard_export_import_carries_agent_cards_and_transcript(tmp_path, monkeypatch): # The path the single-session tests missed: a whole dashboard with agent cards + a browser card. Both agents (with their transcripts) and the browser must survive export -> import. An empty-history import is the bug the user hit ("the chats didn't even show up, let alone the history"). import shutil from backend.apps.agents import agent_manager as am import backend.config.paths as paths sdir = tmp_path / "sessions" ddir = tmp_path / "dashboards" sdir.mkdir() ddir.mkdir() monkeypatch.setattr(am, "SESSIONS_DIR", str(sdir)) monkeypatch.setattr(paths, "DASHBOARDS_DIR", str(ddir)) monkeypatch.setattr(am.agent_manager, "sessions", {}) # nothing live -> disk path did, sid1, sid2, bkey = "d1", "sA", "sB", "browser-1" def sess(sid, name, text): return { "id": sid, "name": name, "status": "completed", "provider": "anthropic", "model": "sonnet", "mode": "agent", "allowed_tools": [], "messages": [{"id": "m1", "role": "user", "content": text, "branch_id": "main"}], "branches": {"main": {"id": "main", "parent_branch_id": None, "fork_point_message_id": None, "created_at": "2026-01-01"}}, "active_branch_id": "main", "tool_group_meta": {}, "active_mcps": [], "dashboard_id": did, } (sdir / f"{sid1}.json").write_text(json.dumps(sess(sid1, "Agent One", "from one"))) (sdir / f"{sid2}.json").write_text(json.dumps(sess(sid2, "Agent Two", "from two"))) (ddir / f"{did}.json").write_text(json.dumps({"id": did, "name": "Board", "layout": { "cards": {sid1: {"session_id": sid1}, sid2: {"session_id": sid2}}, "view_cards": {}, "browser_cards": {bkey: {"browser_id": bkey, "url": "u", "spawned_by": None}}, "notes": {}, "expanded_session_ids": [sid1], }})) raw, _ = closure.build_bundle(EntityType.dashboard, did) sandbox, manifest, p_w = closure.stage_upload(raw, "board.swarm") try: p_rt, root_id, p_created, p_u = closure.commit(sandbox, manifest, []) finally: shutil.rmtree(sandbox, ignore_errors=True) L = json.loads((ddir / f"{root_id}.json").read_text())["layout"] assert len(L["cards"]) == 2, "both agent cards must survive import" assert len(L["browser_cards"]) == 1, "the browser card must survive too" total_msgs = 0 for sid in L["cards"]: doc = json.loads((sdir / f"{sid}.json").read_text()) total_msgs += len(doc.get("messages") or []) assert doc["active_mcps"] == [], "import must not grant MCP access" assert total_msgs == 2, "each agent's transcript must carry through" # The bug behind "the chats didn't even show up": after import the sessions are on disk but not in memory, and the dashboard-open fetch (get_all_sessions) was memory-only, so the cards rendered blank. The fetch must now see the freshly-imported sessions straight off disk. found = am.agent_manager.get_all_sessions(dashboard_id=root_id) assert len(found) == 2, f"dashboard-open fetch must see imported agent sessions, got {len(found)}" assert sum(len(s.messages) for s in found) == 2, "and with their transcripts" def test_get_all_sessions_does_not_resurrect_deleted_cards(tmp_path, monkeypatch): # Deleting a card removes it from the layout but the session keeps its dashboard_id on disk. get_all_sessions must surface only sessions the layout still has a card for, or deleted chats come back on every reopen. from backend.apps.agents import agent_manager as am import backend.config.paths as paths sdir = tmp_path / "sessions" ddir = tmp_path / "dashboards" sdir.mkdir() ddir.mkdir() monkeypatch.setattr(am, "SESSIONS_DIR", str(sdir)) monkeypatch.setattr(paths, "DASHBOARDS_DIR", str(ddir)) monkeypatch.setattr(am.agent_manager, "sessions", {}) did = "d1" def sess(sid): return { "id": sid, "name": sid, "status": "completed", "model": "sonnet", "mode": "agent", "messages": [], "branches": {}, "active_branch_id": "main", "dashboard_id": did, } (sdir / "kept.json").write_text(json.dumps(sess("kept"))) (sdir / "deleted.json").write_text(json.dumps(sess("deleted"))) # still tagged, card gone # The layout has a card only for "kept" (the user deleted "deleted"'s card). (ddir / f"{did}.json").write_text(json.dumps({"id": did, "layout": {"cards": {"kept": {"session_id": "kept"}}}})) ids = {s.id for s in am.agent_manager.get_all_sessions(dashboard_id=did)} assert "kept" in ids, "a session the layout still has a card for must surface" assert "deleted" not in ids, "a session whose card was deleted must NOT resurrect" def test_dashboard_serialize_rewrites_refs_to_bundle_ids(): from backend.apps.swarm.entities.dashboards import DashboardExportable from backend.apps.swarm.models import EntityType class Ctx: def bundle_id_for(self, t: EntityType, lid: str): return {("session", "S"): "SBID", ("app", "A"): "ABID"}.get((t.value, lid)) data = {"name": "D", "layout": { "cards": {"S": {"session_id": "S", "x": 1}}, "view_cards": {"A": {"output_id": "A", "x": 2, "parent_session_id": "S"}}, "browser_cards": {"b1": {"browser_id": "b1", "url": "u", "spawned_by": "S"}}, "expanded_session_ids": ["S"], }} L = DashboardExportable("d1", "D", data).serialize(Ctx())["layout"] assert L["cards"]["SBID"]["session_id"] == "SBID" assert L["view_cards"]["ABID"]["output_id"] == "ABID" # the app card's tether to its builder agent is a session id, so it remaps too assert L["view_cards"]["ABID"]["parent_session_id"] == "SBID" assert L["browser_cards"]["b1"]["spawned_by"] == "SBID" assert L["expanded_session_ids"] == ["SBID"] def test_dashboard_import_remaps_to_fresh_local_ids(monkeypatch): from backend.apps.swarm.entities import dashboards as dmod from backend.apps.swarm.exportable import RemapTable written: dict = {} monkeypatch.setattr(dmod, "p_write", lambda did, doc: written.update({did: doc})) monkeypatch.setattr(dmod, "p_retag_sessions", lambda ids, did: None) remap = RemapTable() remap.assign("SBID", "newsess") remap.assign("ABID", "newapp") payload = {"name": "D", "layout": { "cards": {"SBID": {"session_id": "SBID"}}, "view_cards": { "ABID": {"output_id": "ABID", "parent_session_id": "SBID"}, "ABID2": {"output_id": "ABID2", "parent_session_id": "GONE"}, }, "browser_cards": {"b1": {"browser_id": "b1", "spawned_by": "SBID"}}, "expanded_session_ids": ["SBID", "ORPHAN"], }} remap.assign("ABID2", "newapp2") did = dmod.DashboardExportable.import_(payload, {}, remap) L = written[did]["layout"] assert L["cards"]["newsess"]["session_id"] == "newsess" assert L["view_cards"]["newapp"]["parent_session_id"] == "newsess" assert L["view_cards"]["newapp2"]["parent_session_id"] is None # parent not in bundle assert list(L["browser_cards"].values())[0]["spawned_by"] == "newsess" assert L["expanded_session_ids"] == ["newsess"] # the dangling ref is dropped def test_dashboard_remap_invariant_generative(monkeypatch): # The hand-written remap tests only check the id-bearing fields I remembered. Generate random dashboards and assert the real invariant on a serialize -> import round-trip: no source-local id and no bundle id survives into the imported layout, and every card id is a freshly-minted local id. This is what catches "someone adds a new layout field holding a session id and forgets to remap it." import random from backend.apps.swarm.entities import dashboards as dmod from backend.apps.swarm.exportable import RemapTable from backend.apps.swarm.models import EntityType written: dict = {} monkeypatch.setattr(dmod, "p_write", lambda did, doc: written.update({did: doc})) monkeypatch.setattr(dmod, "p_retag_sessions", lambda ids, did: None) rng = random.Random(1234) for _ in range(60): sess = [f"S{i}" for i in range(rng.randint(0, 5))] apps = [f"A{i}" for i in range(rng.randint(0, 4))] s_bid = {s: f"sbid{i}" for i, s in enumerate(sess)} a_bid = {a: f"abid{i}" for i, a in enumerate(apps)} class Ctx: def bundle_id_for(self, t, lid): if t == EntityType.session: return s_bid.get(lid) if t == EntityType.app: return a_bid.get(lid) return None layout = { "cards": {s: {"session_id": s, "x": rng.randint(0, 9)} for s in sess}, "view_cards": { a: {"output_id": a, "parent_session_id": (rng.choice(sess + ["ORPHAN"]) if sess and rng.random() < 0.7 else None)} for a in apps }, "browser_cards": { f"b{i}": {"browser_id": f"b{i}", "url": "u", "spawned_by": (rng.choice(sess) if sess and rng.random() < 0.7 else None)} for i in range(rng.randint(0, 3)) }, "expanded_session_ids": (sess + ["ORPHAN"]) if rng.random() < 0.5 else list(sess), } payload = dmod.DashboardExportable("d-src", "D", {"name": "D", "layout": layout}).serialize(Ctx()) remap = RemapTable() fresh_sess = {s: f"new-{s_bid[s]}" for s in sess} fresh_apps = {a: f"new-{a_bid[a]}" for a in apps} for s in sess: remap.assign(s_bid[s], fresh_sess[s]) for a in apps: remap.assign(a_bid[a], fresh_apps[a]) did = dmod.DashboardExportable.import_(payload, {}, remap) L = written[did]["layout"] forbidden = set(sess) | set(apps) | set(s_bid.values()) | set(a_bid.values()) assert set(L["cards"]) == set(fresh_sess.values()) assert set(L["view_cards"]) == set(fresh_apps.values()) for cid, card in L["cards"].items(): assert cid not in forbidden and card["session_id"] == cid for oid, card in L["view_cards"].items(): assert oid not in forbidden and card["output_id"] == oid p = card["parent_session_id"] assert p is None or (p in set(fresh_sess.values()) and p not in forbidden) assert set(L["expanded_session_ids"]) <= set(fresh_sess.values()) for card in L["browser_cards"].values(): assert card["spawned_by"] is None or card["spawned_by"] in set(fresh_sess.values()) def test_checksum_rejects_tampering(skill_store): p_make_skill(skill_store, "tmp", "Tmp", "# original") raw, _ = closure.build_bundle(EntityType.skill, "tmp") # Rebuild the zip with the same manifest (old checksum) but an edited payload. src = zipfile.ZipFile(io.BytesIO(raw)) buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as out: for n in src.namelist(): data = src.read(n) if n.endswith("payload.json"): d = json.loads(data) d["content"] = "TAMPERED" data = json.dumps(d, indent=2).encode("utf-8") out.writestr(n, data) with pytest.raises(BundleError): closure.stage_upload(buf.getvalue(), "tmp.swarm") def test_skill_rollback_removes_it(skill_store): from backend.apps.swarm.entities.skills import SkillExportable from backend.apps.swarm.exportable import RemapTable sid = SkillExportable.import_({"slug": "rbk", "name": "Rbk", "content": "x"}, {}, RemapTable()) assert (skill_store / sid / "SKILL.md").exists() SkillExportable.rollback(sid) assert not (skill_store / sid).exists() assert sid not in store.load_index() def test_commit_rolls_back_created_on_failure(skill_store, tmp_path, monkeypatch): # A bundle of [skill, workflow]: the skill imports first and lands, then the workflow import fails, so the skill must be rolled back (all-or-nothing, no half-write). The failure used to come for free (no workflow store on this branch); now the store exists, so force it deterministically by making the workflow import raise. from backend.apps.swarm.models import BundlePreview, EntityRef, Manifest from backend.apps.swarm.entities.workflows import WorkflowExportable def p_boom(*a, **k): raise BundleError("simulated workflow import failure") monkeypatch.setattr(WorkflowExportable, "import_", p_boom) sb = tmp_path / "sb" skill_ref = EntityRef(type=EntityType.skill, bundle_id="s1", name="S", path="entities/s1") wf_ref = EntityRef(type=EntityType.workflow, bundle_id="w1", name="W", path="entities/w1") for ref, payload in ((skill_ref, {"slug": "rollme", "name": "Rollme", "content": "hi"}), (wf_ref, {"title": "W"})): d = sb / "entities" / ref.bundle_id d.mkdir(parents=True) (d / "payload.json").write_text(json.dumps(payload), encoding="utf-8") manifest = Manifest( bundle_id="b", root=skill_ref, entities=[skill_ref, wf_ref], preview=BundlePreview(root_type=EntityType.skill, root_name="S"), ) with pytest.raises(BundleError): closure.commit(str(sb), manifest, []) assert "rollme" not in store.load_index() assert not (skill_store / "rollme").exists() # the imported folder was rolled back def test_manifest_duplicate_ids_rejected(): # Two entities sharing a bundle_id silently collapse in the topo/summary dicts, dropping one; reject up front. (The manifest is outside the checksum.) from backend.apps.swarm.closure import validate_manifest from backend.apps.swarm.models import BundlePreview, EntityRef, Manifest ref = EntityRef(type=EntityType.skill, bundle_id="dup", name="A", path="entities/dup") m = Manifest(bundle_id="b", root=ref, entities=[ref, ref], preview=BundlePreview(root_type=EntityType.skill, root_name="A")) with pytest.raises(BundleError): validate_manifest(m) def test_manifest_root_not_in_entities_rejected(): from backend.apps.swarm.closure import validate_manifest from backend.apps.swarm.models import BundlePreview, EntityRef, Manifest root = EntityRef(type=EntityType.skill, bundle_id="root", name="A", path="entities/root") other = EntityRef(type=EntityType.skill, bundle_id="other", name="B", path="entities/other") m = Manifest(bundle_id="b", root=root, entities=[other], preview=BundlePreview(root_type=EntityType.skill, root_name="A")) with pytest.raises(BundleError): validate_manifest(m) def test_manifest_edge_to_unknown_entity_rejected(): from backend.apps.swarm.closure import validate_manifest from backend.apps.swarm.models import BundlePreview, DependencyEdge, EntityRef, Manifest ref = EntityRef(type=EntityType.dashboard, bundle_id="d", name="D", path="entities/d") m = Manifest(bundle_id="b", root=ref, entities=[ref], edges=[DependencyEdge(**{"from": "d", "to": "ghost"})], preview=BundlePreview(root_type=EntityType.dashboard, root_name="D")) with pytest.raises(BundleError): validate_manifest(m) def p_zip_with(name, data=b"x"): buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr(name, data) return buf.getvalue() def test_zip_slip_rejected(): with pytest.raises(BundleError): unpack(p_zip_with("../escape.txt")) def test_absolute_path_rejected(): with pytest.raises(BundleError): unpack(p_zip_with("/etc/evil")) def test_symlink_entry_rejected(): # A symlink entry could point outside the sandbox once followed; unpack must refuse it before writing anything. buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zi = zipfile.ZipInfo("link") zi.external_attr = 0o120777 << 16 zf.writestr(zi, "/etc/passwd") with pytest.raises(BundleError): unpack(buf.getvalue()) def test_too_many_entries_rejected(): buf = io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: for i in range(5001): zf.writestr(f"f{i}.txt", b"x") with pytest.raises(BundleError): unpack(buf.getvalue()) def test_newer_format_version_rejected(skill_store): # A bundle from a future OpenSwarm should fail clearly, not half-import. buf = io.BytesIO() manifest = { "format_version": 999, "bundle_id": "b", "root": {"type": "skill", "bundle_id": "x", "name": "n", "path": "entities/x"}, "entities": [{"type": "skill", "bundle_id": "x", "name": "n", "path": "entities/x"}], "preview": {"root_type": "skill", "root_name": "n"}, } with zipfile.ZipFile(buf, "w") as zf: zf.writestr("manifest.json", json.dumps(manifest)) zf.writestr("entities/x/payload.json", json.dumps({"slug": "n", "name": "n", "content": "c"})) with pytest.raises(BundleError): closure.stage_upload(buf.getvalue(), "x.swarm")