Files
computer-playbook/tests/lint/ansible/services/test_agent_identity.py
kevinveenbirkenbachandClaude Opus 5 ead858f481 feat(agents): let each sandboxed agent act as its own platform account
The platform reserves a user key per role, so hermes and openclaw existed as non-privileged accounts before either role claimed one. Claiming the key is what turns a reservation into an identity the agent may authenticate as, and gives the account its description and its mailbox. Neither role declares a password: provisioning stores one for every declared account, and a role whose registration rejects the shell-safe alphabet says so in its own meta/users.yml rather than carrying a second credential beside the account it already owns.

The lint derives the agent set from the sandbox-runtime scan rather than a list and holds three properties: the role claims its own reserved key, the claimed account carries no role because an agent employee is never an administrator, and no second credential sits beside it. Pointing an agent at an account that already exists needs no code, since the inventory users variable overrides the same key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 00:23:20 +02:00

134 lines
4.9 KiB
Python

"""Lint: a sandboxed agent acts as its own non-privileged platform account.
The users machinery reserves a key per role, so ``hermes`` and ``openclaw``
exist as accounts whether or not anyone claims them. Claiming the key is what
turns a reservation into an identity the agent may authenticate as, and gives
the account its description and its mailbox.
Two relations per sandboxed role:
* it claims its own reserved user key in ``meta/users.yml``;
* the claimed account carries no role, because an agent employee is a standard
user and never an administrator.
The password is deliberately not one of them. Provisioning gives every declared
account a stored password, and a role that needs a different alphabet says so
in its own ``meta/users.yml`` rather than carrying a second credential beside
the account it already owns.
Pointing an agent at an account that already exists stays possible: the
inventory ``users`` variable overrides the same key, which is the documented
override path and needs no change here.
Suppression (see ``docs/contributing/actions/testing/suppression.md``):
* ``# nocheck: agent-identity`` in the head of the role's ``meta/users.yml``.
"""
from __future__ import annotations
import unittest
from collections.abc import Mapping
from utils.annotations.suppress import is_suppressed_in_head
from utils.cache.files import read_text
from utils.cache.yaml import load_yaml_any
from utils.roles.entity.name import get_entity_name
from utils.roles.mapping import ROLE_FILE_META_SECRETS, ROLE_FILE_META_USERS
from . import PROJECT_ROOT
from .test_sandboxed_no_host_socket import _sandboxed_roles
_RULE = "agent-identity"
_CREDENTIAL = "identity_password"
def _users_of(role: str) -> tuple[Mapping | None, list[str]]:
path = PROJECT_ROOT / "roles" / role / ROLE_FILE_META_USERS
if not path.is_file():
return None, []
data = load_yaml_any(str(path), default_if_missing={})
lines = read_text(str(path)).splitlines()
return (data if isinstance(data, Mapping) else None), lines
def _claimed_roles() -> list[str]:
kept = []
for role in sorted(_sandboxed_roles()):
_users, lines = _users_of(role)
if lines and is_suppressed_in_head(lines, _RULE):
continue
kept.append(role)
return kept
class TestAgentIdentity(unittest.TestCase):
def test_every_agent_claims_its_own_account(self) -> None:
missing = []
for role in _claimed_roles():
users, _lines = _users_of(role)
key = get_entity_name(role)
if users is None or key not in users:
missing.append(
f"{role}: does not claim the reserved user key '{key}' in "
f"{ROLE_FILE_META_USERS}, so the agent has no account of "
f"its own to act as"
)
self.assertEqual(
[],
missing,
"sandboxed agent(s) without their own account:\n"
+ "\n".join(f" - {m}" for m in missing),
)
def test_no_agent_account_carries_a_role(self) -> None:
privileged = []
for role in _claimed_roles():
users, _lines = _users_of(role)
entry = (users or {}).get(get_entity_name(role))
if not isinstance(entry, Mapping):
continue
if entry.get("roles"):
privileged.append(
f"{role}: its account declares roles {entry['roles']}; an "
f"agent employee is a standard user, never privileged"
)
self.assertEqual(
[],
privileged,
"agent account(s) with privileges:\n"
+ "\n".join(f" - {p}" for p in privileged),
)
def test_no_agent_carries_a_credential_beside_its_own_account(self) -> None:
doubled = []
for role in _claimed_roles():
secrets = load_yaml_any(
str(PROJECT_ROOT / "roles" / role / ROLE_FILE_META_SECRETS),
default_if_missing={},
)
credentials = (secrets or {}).get("credentials")
if isinstance(credentials, Mapping) and _CREDENTIAL in credentials:
doubled.append(
f"{role}: declares secrets.credentials.{_CREDENTIAL} beside "
f"the account it already owns; an account password belongs "
f"in {ROLE_FILE_META_USERS}, where provisioning stores it"
)
self.assertEqual(
[],
doubled,
"agent(s) carrying a second credential for their own account:\n"
+ "\n".join(f" - {d}" for d in doubled),
)
def test_the_scan_finds_agents(self) -> None:
self.assertTrue(
_claimed_roles(),
"no sandboxed agent role found, so every rule here would pass "
"vacuously; check that the scan still reads the right topic",
)
if __name__ == "__main__":
unittest.main()