Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backend/routers/settings/retention.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def acknowledge_retention_prune(

Human-only. Admin-role alone is NOT sufficient today: an agent-scoped MCP
key resolves to its owner *carrying the owner's role*, so on an install whose
agents are admin-owned (the default — see cornelius_agent_service.CORNELIUS_OWNER)
agents are admin-owned (the default — owned by `utils.admin_identity.admin_username()`)
an agent key passes the admin check. `reject_agent_principal` is therefore
applied explicitly here. See abilityai/trinity-ops-agent#232 for the
underlying fix.
Expand Down
4 changes: 2 additions & 2 deletions src/backend/services/cornelius_agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from typing import Optional

from database import db
from utils.admin_identity import admin_username
from models import AgentConfig, User
from services.docker_service import docker_client
from redis_breaker_util import get_breaker_redis, SingleFlightLock
Expand All @@ -59,7 +60,6 @@

CORNELIUS_AGENT_NAME = "cornelius"
CORNELIUS_TEMPLATE = "github:Abilityai/cornelius"
CORNELIUS_OWNER = "admin" # seeded under the admin account, like the system agent

# Durable "already seeded" marker (system_settings KV — no migration).
_SEEDED_FLAG = "cornelius_seeded"
Expand Down Expand Up @@ -130,7 +130,7 @@ async def ensure_seeded(self, fresh: Optional[bool] = None) -> dict:
# 3. Owner must exist. On a truly-fresh pre-setup boot the admin row is not
# created until first-time setup completes; skip WITHOUT setting the flag
# so the setup-completion trigger (or a later boot) retries.
admin_row = db.get_user_by_username(CORNELIUS_OWNER)
admin_row = db.get_user_by_username(admin_username())
if not admin_row:
return self._skip(
result, "deferred",
Expand Down
3 changes: 2 additions & 1 deletion src/backend/services/event_dispatch_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ def _get_internal_token(
from config import SECRET_KEY, ALGORITHM
from datetime import datetime, timedelta
from dependencies import EVENT_LOOPBACK_SCOPE
from utils.admin_identity import admin_username

payload = {
"sub": "admin",
"sub": admin_username(),
"scope": EVENT_LOOPBACK_SCOPE,
"exp": datetime.utcnow() + timedelta(minutes=5),
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/services/onboarding_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
with the bundled system's agents plus Cornelius already running, so the
first-run surface it was gating never appeared again.

The seed is deployed **as the admin user** (``system_seed_service.SEED_OWNER``),
The seed is deployed **as the admin user** (``utils.admin_identity.admin_username()``),
so it is indistinguishable from a human admin's own work in ``audit_log`` — an
actor-based predicate cannot answer this. What CAN answer it is the seed's own
naming contract, which is deterministic: the manifest deploys ``{system}-{short}``
Expand Down
21 changes: 12 additions & 9 deletions src/backend/services/system_agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@
)
from services.agent_service.capabilities import normalize_cpu, normalize_memory
from utils.credential_sanitizer import sanitize_text
from utils.admin_identity import admin_username
from utils.helpers import utc_now_iso
from utils.safe_yaml import load_template_yaml # ent#314

logger = logging.getLogger(__name__)

# Constants
SYSTEM_AGENT_TEMPLATE = "local:trinity-system"
SYSTEM_AGENT_OWNER = "admin" # System agent is owned by admin
SYSTEM_AGENT_NETWORK = "trinity-agent-network"

# #1816: the operator-facing vocabulary for `check_base_image_state`. The
Expand Down Expand Up @@ -163,7 +163,7 @@ async def ensure_deployed(self) -> dict:
await container_reload(container)

# Ensure database record has is_system=True (fixes regression if record exists without flag)
db.register_agent_owner(SYSTEM_AGENT_NAME, SYSTEM_AGENT_OWNER, is_system=True)
db.register_agent_owner(SYSTEM_AGENT_NAME, admin_username(), is_system=True)

# #1816 — RUNNING: report, never act.
#
Expand Down Expand Up @@ -328,11 +328,14 @@ async def _create_system_agent(self) -> dict:
import yaml
import json

# Ensure admin user exists for ownership
admin_user = db.get_user_by_username(SYSTEM_AGENT_OWNER)
# Ensure admin user exists for ownership. The platform's admin account,
# whatever ADMIN_USERNAME names it (#3262) — a literal "admin" never
# found the owner on an `ADMIN_USERNAME=root` install.
owner = admin_username()
admin_user = db.get_user_by_username(owner)
if not admin_user:
logger.error(f"Admin user '{SYSTEM_AGENT_OWNER}' not found. Cannot create system agent.")
raise ValueError(f"Admin user '{SYSTEM_AGENT_OWNER}' not found")
logger.error(f"Admin user '{owner}' not found. Cannot create system agent.")
raise ValueError(f"Admin user '{owner}' not found")

# Load template configuration
templates_dir = Path("/agent-configs/templates")
Expand Down Expand Up @@ -364,7 +367,7 @@ async def _create_system_agent(self) -> dict:
try:
agent_mcp_key = db.create_agent_mcp_api_key(
agent_name=SYSTEM_AGENT_NAME,
owner_username=SYSTEM_AGENT_OWNER,
owner_username=owner,
description="Auto-generated system agent MCP key"
)
if agent_mcp_key:
Expand Down Expand Up @@ -520,10 +523,10 @@ async def _create_system_agent(self) -> dict:
)

# Register ownership with is_system=True
db.register_agent_owner(SYSTEM_AGENT_NAME, SYSTEM_AGENT_OWNER, is_system=True)
db.register_agent_owner(SYSTEM_AGENT_NAME, owner, is_system=True)

# Grant default permissions (system agent can talk to everyone)
db.grant_default_permissions(SYSTEM_AGENT_NAME, SYSTEM_AGENT_OWNER)
db.grant_default_permissions(SYSTEM_AGENT_NAME, owner)

return {
"container_id": container.short_id,
Expand Down
39 changes: 35 additions & 4 deletions src/backend/services/system_seed_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from typing import Optional

from database import db
from utils.admin_identity import admin_username
from models import User
from services.docker_service import docker_client
from services.cornelius_agent_service import cornelius_agent_service
Expand All @@ -62,7 +63,6 @@

logger = logging.getLogger(__name__)

SEED_OWNER = "admin" # seeded under the admin account, like Cornelius

# Durable markers (system_settings KV — no migration).
_SEEDED_FLAG = "default_system_seeded"
Expand Down Expand Up @@ -110,13 +110,44 @@ def _resolve_first_run_verdict() -> Optional[bool]:
"""Resolve the durable first-run verdict, computing + persisting it once.

Returns True (fresh install), False (established install), or None
(undetermined — a DB error; nothing is persisted so a later pass retries
the computation). See the module docstring for why this is persisted.
(undetermined — a DB error, or the platform owner row does not exist yet;
nothing is persisted so a later pass retries the computation). See the
module docstring for why this is persisted.

#3262: before the owner was resolved through ``ADMIN_USERNAME``, an
``ADMIN_USERNAME=root`` install stored ``true`` here at first boot and both
seeders then deferred forever at their owner lookup, so neither seed flag
was ever set. Two rules keep that stale verdict from seeding a mature,
hand-built fleet once the owner is found:

* no verdict is persisted while the owner row is absent — the only state in
which a seeder can defer *after* the verdict is stored;
* a stored ``true`` with neither seed flag set while non-system agents
already exist is reconciled to ``false``. A real first run that got as far
as creating agents sets at least one of the two flags (Cornelius on
success, the system seed on ``deployed``/``partial``), so this state is
only reachable through the deferral above.
"""
try:
stored = db.get_setting_value(_FRESH_VERDICT_KEY, None)
if stored is not None:
if (
stored == "true"
and db.get_setting_value("cornelius_seeded", "false") != "true"
and db.get_setting_value(_SEEDED_FLAG, "false") != "true"
and db.count_non_system_agents() > 0
):
logger.warning(
"First-run verdict: stored 'fresh' but no seed ever ran and "
"agents already exist — reconciling to an established install"
)
db.set_setting(_FRESH_VERDICT_KEY, "false")
return False
return stored == "true"
if not db.get_user_by_username(admin_username()):
# Pre-setup (or a misnamed owner): a verdict stored now would
# outlive the deferral it causes. Decide once the owner exists.
return None
if db.get_setting_value("cornelius_seeded", "false") == "true":
# Established ent#107-era install (possibly with every agent since
# deleted) — never read it as fresh.
Expand Down Expand Up @@ -313,7 +344,7 @@ async def ensure_seeded(self, fresh: Optional[bool]) -> dict:
# not created until first-time setup completes; skip WITHOUT setting
# the flag so the setup-completion trigger (or a later boot) retries.
try:
admin_row = db.get_user_by_username(SEED_OWNER)
admin_row = db.get_user_by_username(admin_username())
except Exception as e: # e.g. SQLite BUSY during boot — retry next pass
return self._skip(result, "skipped_error", f"admin lookup failed: {e}")
if not admin_row:
Expand Down
11 changes: 11 additions & 0 deletions tests/registry.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
{
"test_files": [
{
"file": "unit/test_3262_admin_username_owner.py",
"feature": "#3262",
"added": "2026-10-06",
"categories": [
"backend",
"system-agent",
"setup"
],
"description": "With ADMIN_USERNAME=root and only a root admin: the system agent's create path looks up and names the configured admin, a running system agent is registered to it, the Cornelius seed provisions under it, and the default system seed no longer defers waiting for an 'admin' user."
},
{
"file": "unit/test_ent730_bound_widget_dims.py",
"feature": "abilityai/trinity-enterprise#730",
Expand Down
Loading
Loading