Skip to content

Security

This document describes the security model Cinatra runs on: how authentication and authorization work, how secrets are protected, how the agent runtime is sandboxed, and what threat patterns the platform is designed against.

For operational secret-handling, see also Configuration.


Cinatra has two kinds of identity:

  • Users — humans who sign in to the app. Managed by Better Auth. A user has a role inside one or more organizations (member, admin, owner) and a platform-level role (regular or platform admin).
  • Actors — the executing entity inside a request. Resolved from the inbound auth (session cookie, Bearer JWT, A2A identity envelope, or worker context) and threaded through every server action, MCP primitive, and background job. Authorization checks happen against the actor, not the user that started the chain.

Actors carry their source — "ui", "route", "worker", "scheduler", "agent", "a2a" — so authorization checks can apply different rules to UI calls vs. agent-driven calls. For example, an agent calling agents_run resolves to an "agent" actor and is bound by the policy of the run that started the chain.


Better Auth handles all interactive authentication. Username/password is the primary path; passkeys and two-factor authentication are supported plugins. Google OAuth is optional and configured via the GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET environment variables.

Session cookies are signed with BETTER_AUTH_SECRET. Rotating the secret invalidates all existing sessions.

External clients — IDE assistants, third-party agent frameworks, custom dashboards — authenticate with Cinatra by exchanging client credentials for a JWT through the Better Auth OAuth-provider plugin. The same JWT authorizes:

  • The MCP server at /api/mcp
  • The A2A endpoint at /api/a2a (when CINATRA_A2A_HTTP_ENABLED is set)
  • Any per-agent A2A sub-route

The JWT carries the actor identity, so all downstream authorization checks behave the same as for a UI session.

Two bypasses exist for development convenience and are explicitly off-by-default:

  • A2A_DEV_BYPASS=true allows unauthenticated loopback requests to /api/a2a for local testing.
  • CINATRA_RUNTIME_MODE=development enables filesystem-driven agent scans and a handful of other dev affordances.

Neither should be set in production. The first opens the A2A endpoint to anyone reaching the box; the second activates dev-only code paths that assume trust boundaries that do not exist in production.


Every Cinatra resource — agents, objects, skills, templates, playbooks, runs, policies, integrations — is owned at exactly one of four levels:

User → Team → Organization → Workspace

These are nesting scopes: a Team is inside an Organization, an Organization is inside a Workspace. A resource owned at “Team” is visible to anyone with that team membership; a resource owned at “Organization” is visible to anyone in the organization; and so on.

A Project is a bounded execution and context space that can sit at any of these four levels. Outputs created inside a project inherit the project’s scope; outputs created outside a project are owned at the triggering level directly. Projects can ratchet upward through the four levels — irreversibly — but never downgrade.

This model is enforced in the kernel and is the basis for every authorization decision.

Every privileged operation goes through an explicit authorization helper. The kernel never trusts that “we already checked this earlier in the call chain.” Common patterns:

  • requireAuthSession() — any signed-in user
  • requireAdminSession() — platform admin only
  • enforceRunAccess(run, actor) — agent-run ownership and policy check, used uniformly across UI / worker / A2A actors
  • withPlatformAdminBypass(actor, action) — explicit, audited bypass for cross-organization administrative actions

Authorization bypass is a convention in Cinatra, not an exception path: every place a check is intentionally relaxed for an admin uses an explicit helper that names the bypass and logs the action. There is no “implicit admin override” anywhere in the code path.

Per-run authorization is governed by an agentAuthPolicy JSON document on each agent template, with optional per-run overrides. The policy controls four dimensions:

  • Who can list the runs
  • Who can read run data and messages
  • Who can execute new runs
  • Whether the run is shareable to specific users beyond the owner

These checks apply uniformly across the UI, the BullMQ worker, the MCP server, and A2A inbound calls.

Beyond per-run authorization, Cinatra ships a generic permissions surface that applies to four extension kinds:

KindWhat it covers
agent_templateInstalled agents (each agent_templates row). Governs who can read the template, run it, and share it.
agent_runIndividual run records, scoped per run. Inherits from the template’s policy with per-run overrides.
skill_packageInstalled skill packages — the bundle as a whole.
skillIndividual SKILL.md files inside a skill package, with per-skill overrides that can be tighter or wider than the parent package.

The model is the same across kinds:

  • Every resource has an owner identified by ownerLevel (one of user / team / organization / workspace) and ownerId.
  • An owner can grant co-owners — a row in the polymorphic extension_co_owners table ({resourceKind, resourceId, userId, grantedBy, grantedAt}). Being a co-owner is a boolean grant: a co-owner can edit and share the resource without being an organization admin.
  • A polymorphic extension_access_policy row ({resourceKind, resourceId, policy, installedByUserId}) holds the resource’s per-operation access rules (read / execute / share). The same AgentAuthPolicy shape is used for every kind, and the policy is the lever that controls who outside the co-owner set can do what.

The polymorphic backend means a single generic UI component (PermissionsForm) drives every permissions screen across all four kinds; cleanup on extension removal is uniform across kinds, so a co-owner row for a deleted skill cannot dangle. Every grant, revoke, and policy change writes an audit_events row.

The permission resolver runs inside every mutation transaction. An unauthorized write cannot commit even if a client tries to bypass the screen.


Per-instance secrets — provider API keys, connector credentials, registry tokens — are encrypted at rest with AES-256-GCM. The key is CINATRA_ENCRYPTION_KEY.

  • In development, the key is generated automatically on first server boot and persisted to .env.local. Do not commit .env.local.
  • In production, the key must be provided explicitly as an environment variable from your secrets manager. Losing the key means losing access to all encrypted data — the platform cannot decrypt provider keys without it.

The general rule:

  • Environment variables are for things the platform needs at process start — the encryption key, database URLs, the bridge token, OAuth provider client secrets.
  • In-app settings are for everything else — LLM provider keys (managed under /administration/llm), connector OAuth tokens (managed in the Connectors area at /connectors), and registry credentials. All are stored encrypted in the database.

OPENAI_API_KEY is the notable exception: the Graphiti object-graph indexer needs it as a static env var at container startup, so it is an env var rather than an in-app setting.

Calls from the WayFlow container back into the Next.js app — the /api/llm-bridge and per-agent A2A routes — are authenticated by a shared secret in the X-Cinatra-Bridge-Token header. The variable is CINATRA_BRIDGE_TOKEN. Strict-token-only auth: when the variable is unset, all bridge calls return 403. There is no loopback or origin-based fallback.

Set the variable on both the Next.js app and the WayFlow container; the two values must match.


Agents can be configured with shell tool access for executing local commands. The shell tool runs inside a dedicated OpenAI-style sandbox container built from packages/connector-openai/runtime — the agent does not get arbitrary shell access on the host.

The sandbox container has its own filesystem snapshot, no host network mounts by default, and limited capabilities. Agents that need filesystem access read from a host directory mounted explicitly into the sandbox.

Every MCP primitive validates its input with Zod before any side effect runs. Invalid input is rejected at the boundary; downstream code can assume validated shapes. Primitives that perform sensitive operations (admin-only writes, cross-organization reads) gate explicitly through the authorization helpers.

A platform admin can apply an agentAuthPolicy to an agent template that restricts who can run it, who can see its outputs, and whether the runs are shareable. The policy is enforced at every authorization seam — there is no path that bypasses it for “convenience.”


  • Stolen session cookie → MFA and passkey support reduce the window; rotating BETTER_AUTH_SECRET invalidates all sessions immediately.
  • Stolen Bearer JWT → tokens are scoped to a single OAuth client and rotated through the OAuth provider plugin; per-actor checks still apply.
  • Compromised provider API key → keys are encrypted at rest; only decryptable with CINATRA_ENCRYPTION_KEY, which is held outside the database.
  • Malicious external agent calling Cinatra over A2A → A2A route is off by default (CINATRA_A2A_HTTP_ENABLED opts in); when on, every call is bound by the same enforceRunAccess checks the UI uses.
  • Cross-organization data leakage → resource scope is enforced in the kernel; cross-org reads require an explicit withPlatformAdminBypass call which is audited.
  • Path traversal in package install → installer rejects archives with non-canonical paths before unpacking.
  • Timing attacks on the bridge tokentimingSafeEqual with a length-mismatch short-circuit.

If you believe you have found a security issue in Cinatra, report it privately — do not open a public issue. Coordinated disclosure protects users running the platform in production.

To report a vulnerability:

  • Preferred: open a private security advisory on GitHub via the repository’s Security tab (SecurityAdvisoriesNew draft security advisory). This opens a private conversation with the maintainers.
  • Alternative: if you cannot use GitHub Security Advisories, contact the maintainers through the contact path listed in package.json (author / repository fields).

Please include:

  • A description of the vulnerability and its impact
  • Reproduction steps or a proof-of-concept
  • Affected versions or commit ranges, if known
  • Your proposed remediation if you have one

We aim to acknowledge security reports within three business days and to issue a fix or mitigation timeline within ten business days for valid HIGH/CRITICAL findings.