cf15500fb8
Implements PRD prd-new-trusted-agent-forge-identity. Moves author identity and named forge configurations onto the trusted, host-only agent definition, and lets a bottle repository optionally associate a git-gate repo with one of the agent's forge aliases. - Agent-owned identity: new `author` (name/email) and `forge-accounts` (alias -> canonical Gitea /api/v1 origin + host `token_secret` ref) on the agent manifest. `author` populates the bottle's git user.name/user.email. - Remove `git-gate.user` from both agents and bottles; fail with a migration pointer to `author`. `git-gate` is no longer accepted on an agent. - Bottle git-gate repos gain optional `forge: <alias>`, resolved against the selected agent's forge-accounts at composition (fail-closed on unknown). - Fail-closed Gitea API URL validation (https, no userinfo/query/fragment, /api/v1 base, host lowercased, trailing slash normalized, host dedup). - Proxy-held credential: synthesize one inspected, token-authenticated egress route per referenced forge alias, scoped to the origin + API prefix. The token is resolved from the host env at launch into the egress proxy only — never the bottle env, prompt, gitconfig, or workspace. - Generated, non-secret forge workflow guidance appended to the agent prompt for associated repos (API base, branch-backed PR flow, AGit-ref prohibition, mutation verification); omitted when no repo declares a forge. - Agents become home-only: cwd `.bot-bottle/agents` no longer contributes, overrides, or is selectable; warned-and-ignored like cwd bottles. - Docs: README examples, PRD 0011 supersession note, manifest schema docstring. - Tests: new test_forge_identity suite; legacy git-gate.user/cwd tests updated to the agent-owned identity model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
3.8 KiB
Python
118 lines
3.8 KiB
Python
"""Unit: lazy (on-disk) ManifestIndex loader branches (coverage ratchet).
|
|
|
|
The eager from_json_obj path is covered by test_manifest_validation.py;
|
|
this drives the lazy resolve()/from_md_dirs path — all_agent_names with a
|
|
cwd overlay, load_for_agent on an unknown / malformed agent file, and
|
|
require_agent's names-only file-existence checks — so manifest.py's
|
|
core-module coverage doesn't depend on the integration suite."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from bot_bottle.manifest import ManifestError, ManifestIndex
|
|
|
|
|
|
def _write(p: Path, text: str) -> None:
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(textwrap.dedent(text).lstrip("\n"))
|
|
|
|
|
|
_BOTTLE_DEV = """
|
|
---
|
|
egress:
|
|
routes:
|
|
- host: example.com
|
|
---
|
|
The dev bottle.
|
|
"""
|
|
|
|
_AGENT = """
|
|
---
|
|
bottle: dev
|
|
---
|
|
An agent.
|
|
"""
|
|
|
|
# Tab in the frontmatter indent -> YamlSubsetError on parse.
|
|
_AGENT_BAD_FM = "---\nskills:\n\t- x\n---\nbody\n"
|
|
|
|
|
|
class _LazyCase(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.home_root = Path(tempfile.mkdtemp(prefix="cb-home-"))
|
|
self.cwd_root = Path(tempfile.mkdtemp(prefix="cb-cwd-"))
|
|
self._orig_home = os.environ.get("HOME")
|
|
os.environ["HOME"] = str(self.home_root)
|
|
|
|
def tearDown(self) -> None:
|
|
if self._orig_home is None:
|
|
os.environ.pop("HOME", None)
|
|
else:
|
|
os.environ["HOME"] = self._orig_home
|
|
shutil.rmtree(self.home_root, ignore_errors=True)
|
|
shutil.rmtree(self.cwd_root, ignore_errors=True)
|
|
|
|
@property
|
|
def home_cb(self) -> Path:
|
|
return self.home_root / ".bot-bottle"
|
|
|
|
@property
|
|
def cwd_cb(self) -> Path:
|
|
return self.cwd_root / ".bot-bottle"
|
|
|
|
def resolve(self) -> ManifestIndex:
|
|
return ManifestIndex.resolve(str(self.cwd_root))
|
|
|
|
|
|
class TestAllAgentNamesLazy(_LazyCase):
|
|
def test_cwd_agents_ignored_home_only(self) -> None:
|
|
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity):
|
|
# a cwd agents/ dir is warned-and-ignored, so only the home agent
|
|
# appears in all_agent_names.
|
|
_write(self.home_cb / "bottles" / "dev.md", _BOTTLE_DEV)
|
|
_write(self.home_cb / "agents" / "alpha.md", _AGENT)
|
|
_write(self.cwd_cb / "agents" / "beta.md", _AGENT)
|
|
self.assertEqual(["alpha"], self.resolve().all_agent_names)
|
|
|
|
|
|
class TestLoadForAgentLazy(_LazyCase):
|
|
def test_unknown_agent_raises(self) -> None:
|
|
_write(self.home_cb / "agents" / "alpha.md", _AGENT)
|
|
with self.assertRaises(ManifestError):
|
|
self.resolve().load_for_agent("nope")
|
|
|
|
def test_malformed_frontmatter_raises(self) -> None:
|
|
_write(self.home_cb / "bottles" / "dev.md", _BOTTLE_DEV)
|
|
_write(self.home_cb / "agents" / "broken.md", _AGENT_BAD_FM)
|
|
with self.assertRaises(ManifestError):
|
|
self.resolve().load_for_agent("broken")
|
|
|
|
|
|
class TestRequireAgentLazy(_LazyCase):
|
|
def test_existing_home_agent_ok(self) -> None:
|
|
_write(self.home_cb / "agents" / "alpha.md", _AGENT)
|
|
self.resolve().require_agent("alpha") # no raise
|
|
|
|
def test_cwd_only_agent_not_selectable(self) -> None:
|
|
# Agents are home-only (PRD prd-new-trusted-agent-forge-identity):
|
|
# a cwd-only agent file is never selectable, so require_agent raises.
|
|
_write(self.home_cb / "agents" / "alpha.md", _AGENT)
|
|
_write(self.cwd_cb / "agents" / "beta.md", _AGENT)
|
|
with self.assertRaises(ManifestError):
|
|
self.resolve().require_agent("beta")
|
|
|
|
def test_unknown_agent_raises(self) -> None:
|
|
_write(self.home_cb / "agents" / "alpha.md", _AGENT)
|
|
with self.assertRaises(ManifestError):
|
|
self.resolve().require_agent("nope")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|