Imported from MaraClaw/MaraClaw (
engine/app/core/AGENTS.md). Install upstream withnpx skills add MaraClaw/MaraClaw --skill core. Copyright stays with the author.
app/core
app/core contains cross-cutting primitives only: tracing, logging setup, JWT/auth dependencies, permission policy, Redis Pub/Sub lifecycle, and low-level email transport helpers.
Trace And Logging
TraceIdMiddlewarereads or createsX-Trace-Id, storesrequest.state.trace_id, adds the response header, and logs request/response lines.app.maininstalls trace middleware first. Preserve that order for request-wide tracing.- Starlette wraps middleware in reverse order; verify runtime behavior before relying on trace logs for CORS/error paths.
- Use
from app.core.logging import logger.intercept_standard_logging()redirects stdlib logging into the process logger at startup. - Background jobs that are not inside an HTTP request should call
new_trace_id()before emitting related logs. - WebSocket flows set trace ids manually because they do not follow the normal HTTP request lifecycle.
Security
outbound_policy.pyandoutbound_transport.pyenforce public-only destinations for untrusted MCP, webpage reads, and URL polling. Usepublic_http_clientto pin each dial to validated DNS answers while retaining TLS hostname verification and disabling environment proxies. MCP clients additionally bind requests to their configured origin.security.pyowns JWT creation/decoding, password hashing, and FastAPI auth dependencies.- Shared loader:
load_user_from_access_token(token, require_active=..., enforce_password_change=...)loadsUserRecordwith identity and optionally enforcesmust_change_password. - Use from route modules:
get_current_user- active user; 403 with{must_change_password: true}when force-change is pending (all privileged REST).get_authenticated_user- allows inactive + force-change; only forGET /auth/meand password change (not for tenant join mutations).get_current_admin/require_role(...)- built onget_current_user(inherit force-change gate).raise_if_password_change_required(user)- explicit check when a handler still usesget_authenticated_userbut must block force-change users (POST /tenants/join).
- Non-Depends JWT auth (file download query/Bearer token) must use
load_user_from_access_token(not baredecode_access_token+user_dao.get). Chat WebSockets redeem single-use tickets and useload_user_principal(fresh=True)throughchat_tickets.authorize. Baregetdoes not join identity, somust_change_passwordwould always look false. - Use async password helpers from async paths; bcrypt work is intentionally pushed to a thread pool.
- Token payloads use
subfor user id androlefor role. Force-change is not in the JWT; it is re-read from identity on each request.
Permissions
permissions.pyis the shared source of truth for agent visibility, access levels, and relationship access.- Prefer
list_visible_agents(...)(DAO). There is no SQLAlchemy visibility query. - Use
check_agent_access(...)at HTTP endpoint boundaries. Custom-mode departmentusegrants count vialocal_department_dao.has_agent_access. acl_locks.py:lock_tenants, thenlock_user/lock_agent, areSELECT … FOR UPDATEinside the caller'sconnection_ctx. Take them beforebump_agent_acl_version. Order is tenant, subject, agent.- Access decisions (not agent rows) may be cached in Redis via
access_cache.py. TTLAGENT_ACCESS_CACHE_TTL_SECONDS(0 disables). Policy writes bumpaclver:{agent_id}after the top-levelconnection_ctxcommit.set_cached_levelis skipped ifaclverchanged since compute. Redis errors fail open to DAOs. Do not cache 403/404. - Auth snapshots (
session_cache.py,USER_SESSION_CACHE_TTL_SECONDS) cacheget_with_identitywithoutpassword_hashor quota counters. User/identity writes bumpsessver:*after commit and drop request memo. Sets carryobserved_ver. Password verify/change/transfer must useload_identity_for_password(SQL), never the snapshot hash. Redis read errors fall back to SQL; version INCR/DELETE are not skipped by the fail-open circuit. - Tenant rows may be cached (
tenant_cache.py). Agent PK reads use request-localrow_memoonly (full agent rows includeapi_key_hashand counters). - New fail-open caches go through
redis_cache.py+get_cache_redis()(short socket timeout). Tokens, locks, pubsub, and webhooks keepget_redis(). - Agent tool config mutations that change sandbox egress (
allow_network, proxy fields) are authorized inapp/api/tools.py(admin roles), not here; keep permission policy for agent visibility separate from sandbox egress policy. - Use non-HTTP helpers such as
get_agent_access_level_for_user_id(...)anduser_can_manage_agent_id(...)in services/background jobs. - Keep relationship checks in
evaluate_agent_relationship_status(...)andevaluate_human_relationship_status(...).
Events
- Use
publish_event(channel, data)for Redis Pub/Sub events. - Do not create ad hoc Redis clients for shared app events. Caches use
get_cache_redis(); durable paths useget_redis(). close_redis()closes both pools during app shutdown; event users should not close the process-global clients themselves.
Existing Exceptions
- Bundled skill-creator payloads under
app/services/skill_creator_files/may still importloguru. Do not "fix" those; they run outside the backend process.
