mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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>
134 lines
4.9 KiB
Python
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()
|