diff --git a/src/aipass/spawn/apps/handlers/passport_ops.py b/src/aipass/spawn/apps/handlers/passport_ops.py index be54043a..834fc5f5 100644 --- a/src/aipass/spawn/apps/handlers/passport_ops.py +++ b/src/aipass/spawn/apps/handlers/passport_ops.py @@ -98,6 +98,14 @@ def grant_passport( # Rename any placeholder paths _ = rename_placeholder_paths(target, folder_name) + # Set owner field — first agent in the project is the owner + passport_path = target / ".trinity" / "passport.json" + if passport_path.exists(): + passport_data = json_handler.read_json(passport_path) + if passport_data: + passport_data.setdefault("citizenship", {})["owner"] = citizen_number == 1 + json_handler.write_json(passport_path, passport_data) + # Regenerate template registry regenerate_template_registry(target) diff --git a/src/aipass/spawn/apps/modules/core.py b/src/aipass/spawn/apps/modules/core.py index 8d167b0a..85b2a772 100644 --- a/src/aipass/spawn/apps/modules/core.py +++ b/src/aipass/spawn/apps/modules/core.py @@ -217,6 +217,14 @@ def _spawn_agent( # Step 2: Rename any {{BRANCH}} dirs/files that weren't caught by path replacement renamed = rename_placeholder_paths(target, folder_name) + # Step 2b: Set owner field — first agent in the project is the owner + passport_path = target / ".trinity" / "passport.json" + if passport_path.exists(): + passport_data = json_handler.read_json(passport_path) + if passport_data: + passport_data.setdefault("citizenship", {})["owner"] = citizen_number == 1 + json_handler.write_json(passport_path, passport_data) + # Step 3: Regenerate .template_registry.json with fresh hashes regenerate_template_registry(target) diff --git a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json index cfc0d8cb..9ee2eea7 100644 --- a/src/aipass/spawn/templates/builder/.spawn/.template_registry.json +++ b/src/aipass/spawn/templates/builder/.spawn/.template_registry.json @@ -137,6 +137,12 @@ "content_hash": "2e4f4a0c1b47", "has_branch_placeholder": false }, + "f022": { + "path": "apps/handlers/__init__.py", + "name": "__init__.py", + "content_hash": "dbfc0e044461", + "has_branch_placeholder": false + }, "f044": { "path": "apps/integrations/README.md", "name": "README.md", @@ -149,7 +155,7 @@ "content_hash": "a4cf0a8e3b4f", "has_branch_placeholder": false }, - "f045": { + "f015": { "path": "apps/modules/__init__.py", "name": "__init__.py", "content_hash": "e3b0c44298fc", @@ -161,6 +167,12 @@ "content_hash": "d1e4e2b98c38", "has_branch_placeholder": false }, + "f027": { + "path": "apps/{{BRANCH}}.py", + "name": "{{BRANCH}}.py", + "content_hash": "39db2c4f8160", + "has_branch_placeholder": true + }, "f028": { "path": "artifacts/README.md", "name": "README.md", @@ -227,6 +239,12 @@ "content_hash": "881f06bb6574", "has_branch_placeholder": false }, + "f039": { + "path": "tests/conftest.py", + "name": "conftest.py", + "content_hash": "97f220799d19", + "has_branch_placeholder": false + }, "f040": { "path": "tools/README.md", "name": "README.md", @@ -245,25 +263,7 @@ "content_hash": "28e9ae373563", "has_branch_placeholder": false }, - "f022": { - "path": "apps/handlers/__init__.py", - "name": "__init__.py", - "content_hash": "dbfc0e044461", - "has_branch_placeholder": false - }, - "f027": { - "path": "apps/{{BRANCH}}.py", - "name": "{{BRANCH}}.py", - "content_hash": "39db2c4f8160", - "has_branch_placeholder": true - }, - "f039": { - "path": "tests/conftest.py", - "name": "conftest.py", - "content_hash": "97f220799d19", - "has_branch_placeholder": false - }, - "f015": { + "f026": { "path": "apps/plugins/__init__.py", "name": "__init__.py", "content_hash": "e3b0c44298fc", diff --git a/src/aipass/spawn/tests/test_citizen_classes.py b/src/aipass/spawn/tests/test_citizen_classes.py index d4b976c1..3372b738 100644 --- a/src/aipass/spawn/tests/test_citizen_classes.py +++ b/src/aipass/spawn/tests/test_citizen_classes.py @@ -13,6 +13,8 @@ class-aware update, and backward compatibility. """ import json +import unittest.mock + import pytest from pathlib import Path @@ -444,3 +446,88 @@ class TestMultiAgentCoexistence: passport = json.loads((tmp_path / name / ".trinity" / "passport.json").read_text()) assert passport["branch_info"]["branch_name"] == name.upper() assert passport["identity"]["citizen_class"] == "builder" + + +class TestPassportOwnerField: + """Tests verifying the owner field in passport.json.""" + + def test_first_agent_is_owner(self, tmp_path): + """First agent created in a project gets owner: true.""" + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + _spawn_agent(str(tmp_path / "first"), registry_path=str(reg)) + + passport = json.loads((tmp_path / "first" / ".trinity" / "passport.json").read_text()) + assert passport["citizenship"]["owner"] is True + + def test_second_agent_not_owner(self, tmp_path): + """Second agent created in a project gets owner: false.""" + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + _spawn_agent(str(tmp_path / "first"), registry_path=str(reg)) + _spawn_agent(str(tmp_path / "second"), registry_path=str(reg)) + + p1 = json.loads((tmp_path / "first" / ".trinity" / "passport.json").read_text()) + p2 = json.loads((tmp_path / "second" / ".trinity" / "passport.json").read_text()) + assert p1["citizenship"]["owner"] is True + assert p2["citizenship"]["owner"] is False + + def test_third_agent_not_owner(self, tmp_path): + """Third agent also gets owner: false.""" + from aipass.spawn.apps.modules.core import _spawn_agent + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + for name in ["alpha", "beta", "gamma"]: + _spawn_agent(str(tmp_path / name), registry_path=str(reg)) + + passports = {} + for name in ["alpha", "beta", "gamma"]: + passports[name] = json.loads((tmp_path / name / ".trinity" / "passport.json").read_text()) + + assert passports["alpha"]["citizenship"]["owner"] is True + assert passports["beta"]["citizenship"]["owner"] is False + assert passports["gamma"]["citizenship"]["owner"] is False + + def test_birthright_first_agent_is_owner(self, tmp_path): + """First birthright agent gets owner: true.""" + from aipass.spawn.apps.handlers.passport_ops import grant_passport + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + target = tmp_path / "citizen" + target.mkdir() + with unittest.mock.patch("aipass.spawn.apps.handlers.passport_ops.find_registry", return_value=reg): + grant_passport(str(target)) + + passport = json.loads((target / ".trinity" / "passport.json").read_text()) + assert passport["citizenship"]["owner"] is True + + def test_birthright_second_agent_not_owner(self, tmp_path): + """Second birthright agent gets owner: false.""" + from aipass.spawn.apps.handlers.passport_ops import grant_passport + + reg = tmp_path / "TEST_REGISTRY.json" + reg.write_text('{"metadata":{"version":"1.0.0","total_branches":0},"branches":[]}') + + first = tmp_path / "first" + first.mkdir() + second = tmp_path / "second" + second.mkdir() + + with unittest.mock.patch("aipass.spawn.apps.handlers.passport_ops.find_registry", return_value=reg): + grant_passport(str(first)) + grant_passport(str(second)) + + p1 = json.loads((first / ".trinity" / "passport.json").read_text()) + p2 = json.loads((second / ".trinity" / "passport.json").read_text()) + assert p1["citizenship"]["owner"] is True + assert p2["citizenship"]["owner"] is False