Skip to content

Security: SamuelAlev/control-center

SECURITY.md

Security policy

Reporting a vulnerability

Do not open a public issue. Use this repository's private vulnerability reporting with impact, reproduction/PoC, affected component and version/commit. Reports receive acknowledgement and a fix or mitigation plan after triage; allow time to remediate before disclosure.

The server, relay, clients, workers, external tools and disposable rigs have different trust levels. This document owns their security contracts; ARCH.md covers implementation flow and AGENTS.md the coding rules.

Authorization and isolation

  • The server owns data, policy, external APIs and execution. A device PSK authenticates a device, not membership in any workspace. Principal distinguishes humans and agents; rate limits are per principal across devices.
  • Workspace operations require an explicit workspace_id. Check the registered, non-deleted workspace before membership lookup or opening its database; even role lookup can otherwise create an empty database for an unknown ID.
  • Enforce WorkspaceRoleResolver membership at every inbound lane: repo/call, sub/subscribe, tools/call, and HTTP media/backup handlers. Workspace sessions are not a substitute for per-call scoping. Phone tools additionally use default-deny RemoteToolPolicy and RemoteRateLimiter, with tighter mutation caps.
  • Scope ID lookups or reject entity.workspaceId != workspaceId at the read/write chokepoint (WorkspaceMismatchException or explicit MCP error). Repo tools also verify isRepoLinkedToWorkspace; per-repo grants must not exceed forge access.
  • GlobalDatabase contains only explicitly shared state; each workspace has its own file. Never cache a workspace DAO. Use CrossWorkspaceQueries plus a CROSS-WORKSPACE BY DESIGN explanation for legitimate fan-out; no ad hoc workspace enumeration. Pre-auth IDs route through workspace_routes, with no scan-on-miss fallback.
  • Filter cross-workspace watchAll results per subscriber (_visibleRows), events in RemoteEventForwarder, and presence by repo grant. WorkspaceMemberRemoved drops that workspace's subscriptions and invalidates cached membership. Device revocation watches paired_devices and terminates its sessions.
  • Request cancellation is scoped to the authenticated connection and targets only reads or subscription setup. Unknown IDs leave no tombstone; other sessions and mutations are unaffected. Cancellation does not free a running handler's concurrency slot early, so cancel/reissue cannot bypass admission limits.
  • Registry operations self-gate: workspace update requires admin, delete requires owner, reorder filters to the caller's workspaces; confirmation.respond requires member. Server settings/MCP/model operations use requireServerAdmin/serverOwnerUserId, not a workspace role.
  • /meeting/audio and /workspace-logo verify membership after PSK authentication. Backup HTTP routes independently enforce workspace admin for export, workspace owner for restore, and install owner for whole-install snapshots; see backup lifecycle.

Regression coverage: cc_persistence/test/workspace_isolation_ratchet_test.dart, cc_host dispatcher/subscription/session tests, cc_server_core/test/fresh_boot_first_workspace_test.dart and remote-event-forwarder tests. New workspace surfaces need behavioral cross-workspace denial coverage, not only structural checks.

Credentials

Provider tokens live on the server, never in client storage or RPC responses. The client keychain (flutter_secure_storage) holds only its device credential. Existing client provider tokens migrate once through credential_migration.dart, then are deleted locally. shared_preferences is for non-sensitive, per-user synced preferences.

Client read snapshots contain previously authorized presentation data, not credentials or authorization decisions. They are isolated by verified server identity and authenticated user, with workspace/query arguments in each entry. Desktop files are owner-private; web snapshots are origin-local IndexedDB data. Fresh membership reads prune revoked workspace data, explicit authorization failures evict affected snapshots, and forgetting/disconnecting a server removes its persisted snapshots. Cached data never bypasses the cold-start authentication gate or authorizes a write.

Server credential lanes under packages/cc_server_core/lib/src/identity/:

Lane Storage and authority
Per-user UserCredentialsStore user_forge_<forge>_<userId> / user_ticket_<provider>_<userId> in 0600 FileSecretsStore; JSON access/refresh/expiry/source/account envelope. credentials.* always targets ctx.userId.
App identity (ProviderAppSettings, GitHubAppClient) Operator-only providerApps.*; non-secrets in server_settings, secrets in the secrets file. Environment seeds each field once; stored values then win.
Environment CI/headless credentials declared in .env.template.

ForgeCredentials resolution is deliberate:

  • With a calling user: that user's credential, otherwise nothing. Never silently borrow the server environment for their authentication.
  • Without a caller: app identity → server owner's credential → environment.
  • Human-driven forge writes use tokenForActor(forge,userId): the person's credential first, then the no-caller chain only if they have not connected the forge. actsAsSelf reports the result. Thread ctx.userId through per-actor factories (forgeHttpClientFactoryForActor, forgeRegistryForActor, prLifecycleRepositoryForActor, ReviewPublisherService.githubPrClientFor). Include acting user in prRepoCache keys so cached authenticated clients cannot misattribute another person's writes.
  • MCP, polling, webhooks and automatic agent review publication pass no user and use background identity.
  • Agent sandboxes hold only a ForgeTokenScope.read forge token (GitHub App installation: contents read, pull requests write), never a member's own token, a write token or a ticketing API key. actingUserId withholds the server PAT from a run acting for a non-owner member who has not connected GitHub. A write token is minted only by the agent run gateway or a human-opened terminal rig and never enters an agent's environment. Without a GitHub App the fallback PAT cannot be narrowed: git push still meets the push rule, but code using the token directly does not — say so, never present it as enforced.

GitHub sign-in is server-run device flow (ProviderOAuthService, oauth.begin), with no callback URL and no required client secret. A secret is needed to refresh expiring tokens. Linear uses /oauth/<provider>/callback with single-use, TTL-bounded state bound server-side to (user,provider). PAT entry remains available; host gh credentials are not an authentication method.

Each GitHub device attempt returns an opaque login_id, not the provider's device_code. oauth.status checks the authenticated caller and the attempt's original workspace and returns only pending, succeeded or failed. Success is reported after the credential is stored. Attempt tracking is bounded, and starting a login prunes expired outcomes. Clients watch this attempt independently of the dialog, refresh connection state on success, and do not treat an existing credential as completion of a new sign-in.

Provider credentials may be configured through supported Settings fields or the environment, never command-line flags. cc_server reads .env from its working directory, beneath real environment values (environmentWithDotenv); clients do not forward it. CcServerConfig.pickCredential may fall back to release-baked non-confidential values only: Google device-code client, Klipy key, GitHub client ID. Never bake a GitHub private key or client secret. The shipped client ID enables sign-in, not installation-token background access; an operator with token expiry enabled supplies their own refresh secret. Connection RPCs return status/account only; secret settings are write-only, with presence flags returned.

Personal credential operations bind to the authenticated user. Global harness/provider credential writes and OAuth state require the server owner, not merely a workspace owner or administrator. The phone can submit supported credentials but cannot read their secret values back or simulate host-only CLI authentication.

SSO, owner identity and other mutable settings belong in Settings. CC_SERVER_*/matching flags are for pre-boot process configuration (paths, bind, port, TLS, logging), not a second settings store. Redact logged command output before display.

Execution and untrusted input

Actions, tools and skills

ActionClass is a closed effect taxonomy. Every mutating tool declares its classes; the ratchet rejects undeclared tools. Policy precedence is space > agent > workspace > mode preset > built-in default; within a scope, longest-prefix then most-restrictive wins. prompt without a connected approver denies. The autonomy dial cannot turn an action-policy prompt or deny into silent approval, including for pinned output tools. Per-space autonomy profiles use this same store; delegation enforces depth, cycles, budget and autonomy ceilings server-side.

Agents carry no permission flags; push, PR, tracker and network permission are action rules. Shell command lines (harness bash, Claude Code's PreToolUse hook) are classified by ShellActionClassifier, including pushes, deletion, wrapped/compound commands and recognizable gh api PR writes. This remains a soft gate: arbitrary scripts, generated code and unrecognized API calls can conceal effects. Git's pushInsteadOf is routing, not authorization: credential-bearing URLs, explicit ports and command-local Git configuration can avoid its prefixes. Shell preflight therefore always checks gitPush; the gateway independently authorizes actual refs, potentially asking again. GitHub App contents-read credentials prevent direct pushes, but their pull-request write scope does not provide an equivalent hard boundary for PR publication. An unnarrowable fallback PAT also leaves hidden pushes advisory.

Unprompted policy denials notify the operator as informational blocks, not approval requests. This includes a push denied during ref discovery. Live delivery and history remain workspace-filtered; declining an already announced approval produces no duplicate block notice. Notification preferences still apply, and a notification never authorizes an action.

SkillBundleService scans between fetch and write. No skill bytes or frontmatter enter disk/prompt without a pass/warn/quarantine verdict. Mandatory static rules and capability manifests execute nothing; LLM review is additive. Trust tiers express provenance, never bypass scanning. Bytes scanned = bytes written = bytes hash-locked. Repo skills use the same gate; context symlinks resolve only into explicitly permitted roots, never by recursively following repo-directory links.

External MCP servers and guest-extracted content are untrusted. Strip ANSI/control characters and cap external tool descriptions/results; external tools default to the cautious approval tier. wrapUntrustedRigContent labels guest data as data, not instructions. Framing is not enforcement: approval, filesystem/network limits and server authorization remain necessary. Deferred harness schemas never widen the admitted tool surface or bypass guards.

Agent-command output reads require full-client capability, workspace membership and the space's repository grants. process.agentShellOutput accepts an opaque, workspace/space-bound output identity, never a pathname or arbitrary PID. The server revalidates the owning process before accepting its first captured bytes; PID reuse cannot adopt another process's output. On macOS, regular stdout/stderr files are opened without following path-component symlinks and checked against the observed device/inode; Linux reads the validated process's regular /proc descriptors. Retained descriptors are close-on-exec, bounded and never reopened by a replaced pathname. Output is redacted before RPC delivery; pipes and PTYs are never read.

Controlled skill evaluation

Campaign admission, subscriptions, cancellation, attempt retrieval and export validate the registered workspace, membership and repository grants. Admission and execution also enforce action policy, active credentials and model support. API-key providers require a known worst-case call reservation, including documented pricing tiers. Claude Code and Codex OAuth subscription campaigns instead use an explicit API-equivalent usage stopping threshold: one in-flight call may exceed it, then no further calls start. This is not an invoice ceiling or subscription-quota measurement. Missing required pricing or unknown observed spend stops further calls; no paid rerun is implicit.

The evaluator re-scans and hash-locks the exact immutable package. Guest commands and file tools use the same enclosure: private fixture/home, no inherited credentials, denied external filesystem access and network, read-only skill assets, bounded output/files/descriptors/CPU and owned-process-group cleanup. Capability probes must succeed before model spend; unsupported hosts fail closed. macOS uses sandbox-exec plus resource limits and does not enforce a hard memory quota. Linux requires bubblewrap and applies an address-space limit; Windows and fleet evaluation are not supported by this controlled protocol.

The native Claude controller uses a pinned, managed Claude account and observed CLI version, never the operator's ambient login. Its host process retains provider credentials; its native tools, ambient skills and user/project settings are disabled. The only exposed tools are the authenticated loopback MCP bridge into the confined guest. Native init metadata must match the controlled tool surface and built-in plugin identities; unexpected skills, plugins or tool authority fail closed. CLI-native reported cost is API-equivalent usage, not billed subscription spend.

Candidate code never runs on the server host or receives the trusted expected answers. Built-in typed oracles compare independently specified outcomes in the controller; candidate Python executes in a separately contained copy. Untrusted setup scripts and arbitrary guest-supplied verifier programs are not admitted. Artifact reads reject symlinks/non-regular files and enforce byte limits.

Persisted attempt evidence is redacted and capped at 256 KiB; exports are capped at 16 MiB. When detailed traces exceed that budget, exports retain compact outcome/cost summaries for all admitted attempts and explicitly count omitted or summarized records. Secrets, private inputs and host paths are not exportable replay material. A safety-artifact pass does not override scanner quarantine, prove real-world attack resistance or grant additional authority.

Agent run gateway

AgentRunGateway serves /agent/git/… (push proxy) and /agent/hooks/pre-tool-use (Claude Code hook) on the MCP listener.

  • Loopback only: any non-loopback peer gets 403, whatever listener the routes are mounted on.
  • Each run is authenticated by a 32-byte random per-run secret sent as X-CC-Agent-Run (git http.<gateway>.extraHeader, hook header from CC_AGENT_RUN_TOKEN); unknown secrets get 401. The secret only lets a process ask: every answer comes from the action policy.
  • Scope (workspace, space, agent, mode, acting user, own repo) comes from the AgentRunGrant registered at dispatch, never from the request, so one run cannot ask with another's identity. Pushes are forwarded only to the worktree's own repo or a GitHub repo linked to the workspace; read-only modes deny them.
  • Only pushes are proxied (git-receive-pack); the ref-update preamble and hook bodies are capped at 1 MiB. Compressed pushes are decoded before policy evaluation; incomplete or uninspectable updates are rejected, never treated as ref-free probes. The write token is minted per lease, used server-side to forward, never returned to the agent, and revoked when the lease closes at teardown.
  • Claude uses a command PreToolUse hook, not a fail-open HTTP hook. Its curl request must receive the explicit allow verdict; denied, empty, malformed, unauthenticated and failed responses exit 2 and block the tool. Missing leases install a blocking Bash hook. The gateway denies unanswered approvals at 55 minutes, curl times out at 56 minutes, and Claude's hook timeout is 60 minutes. Missing curl also blocks rather than skipping policy.
  • Approval is triggered by an attempted action, not a command printed in chat. When publishing is part of the task, Claude is instructed to invoke the guarded push/PR command and wait for the verdict, rather than hand it back to the operator. This guidance is not an enforcement boundary.

Sandboxes and rigs

  • Copy-on-write provisioning must not write branches, registrations or FETCH_HEAD into source checkouts. No silent git-worktree fallback where rift is supported; Windows is the explicit backend exception. Required native failures are fatal, not degraded operation.
  • Server-provisioned disposable overlays/worktrees carry implicit local execution authority; creating a new conversation or entering a subdirectory does not ask again. Only server-derived writable roots qualify, never a tool-selected cwd, read-only mount, original checkout or durable fallback directory. Existing explicit denials survive; a denied descendant prevents an implicit grant of its parent. This does not relax write protection, network rules or push approval.
  • Restricted native macOS egress is denied except for the session's HTTP/SOCKS proxies, registered loopback service ports and DNS IPC. Local IP permission is limited to network-bind: allowing all network* operations on a local IP also admits direct outbound connections. Removing proxy variables or using a client that ignores them must not bypass the domain allowlist. Allowed SOCKS tunnels retain their live socket subscription and any buffered payload across the handshake. Real-process tests cover HTTP/SOCKS policy, direct IPv4/IPv6 denial, network-off and the reachable approval endpoint.
  • Restricted QEMU argv uses restrict=on, loopback-only hostfwd, read-only base plus per-session overlay, and no -virtfs/-fsdev. Egress passes through guestfwd to SandboxHttpProxy/SandboxSocksProxy on host loopback.
  • Restricted smolvm argv uses --outbound-localhost-only, one --allow-host per admitted host, and broker credentials via --secret-file, not persisted environment values. Docker Hub image-maintenance hosts join the same gate.
  • Only an install-owner-confirmed restart sets RigSpec.unrestrictedNetwork. QEMU omits restrict=on, smolvm uses bare --net, proxy policy widens too, and the UI retains a warning. Pin both restricted/unrestricted argv paths in tests.
  • Android/iOS are host-managed with host networking; do not advertise them as enclosed or offer an unenforceable egress switch. Developer commands remain device/simulator argv, never a host shell.
  • Guests receive only per-VM broker secrets for short-lived, scoped, per-operation tokens: allowlisted, rate-limited and revoked at close. No durable provider credential enters an enclosure. RigSpec.forgeTokenScope is fixed at open: a human-opened terminal rig gets write (bounded by that member's forge access); an agent-opened rig gets read, because a guest credential helper cannot tell a push from a fetch.
  • RigService.act blocks agent mutations during human control. Input is attributed and sequenced transactionally. Guest capture is unprivileged; QEMU input is injected via the hypervisor. Clipboard crossing requires per-user/per-direction permission before reading/transferring.
  • enclosureControl is denied in read-only modes. SandboxBackend.microvm is probe-gated and cannot downgrade to a host shell. Bound lifetimes/resources, recover only owned devices, and tear down owned machines at shutdown. Boot artifacts are checksum/digest-pinned; control sockets are private and reject symlinked namespaces. See enclosure operational constraints.

Audit integrity

Guard-decision hashes canonicalize timestamps to the UTC Unix-second precision persisted by SQLite. Verification still detects changed, reordered or removed decisions. Older hashes computed from fractional timestamps that were not persisted can remain unverifiable; the server reports those failures rather than rewriting historical hashes or accepting them as intact.

Network boundaries

  • Non-loopback server binds require TLS (--tls-cert/--tls-key), or explicit --insecure behind a trusted TLS-terminating proxy.
  • First-party client sessions require Noise_NNpsk0_25519_ChaChaPoly_SHA256 on both direct WebSockets and the broker relay, including loopback. A device's 32-byte PSK authenticates admission; a transcript-bound Ed25519 signature authenticates the server and its saved fingerprint pin. Missing identity, changed pins, unsupported versions, tampering, replay and out-of-order records fail closed.
  • Every connection uses fresh X25519 ephemeral keys and separate directional traffic keys. Ordered counters, periodic rekeying and record limits bound key use. Reconnection starts a new handshake, never resumes old counters or replays an interrupted transfer. Key destruction is best-effort in Dart/browser runtimes, not a guarantee of physical memory erasure.
  • RPC, terminal input/output and sensitive resource bodies share this secure session. Media/fonts, meeting audio, workspace logos, blobs, backup transfers, soundscapes, rig streams/files/clipboard and embedded editor HTTP/WebSockets use the bounded resource tunnel. Their public HTTP routes return 426; signed URLs alone cannot open a readable network lane. Existing signatures, registered-workspace checks, membership and operation-specific roles still apply. /proxy/media also checks every upstream target and redirect against SSRF blocks: loopback, link-local, cloud metadata, private IPv4 and IPv6 ULA.
  • Resource cancellation closes network producers; shared media fetches remain live only while another consumer needs them. Server admission counts actual handlers until they finish, including work unwinding after cancellation. Device revocation/expiry closes attached sessions; membership changes cancel active resource transfers and subsequent requests reauthorize.
  • Native URL consumers use a capability-scoped loopback facade. Browser URL consumers use an in-memory Service Worker/MessagePort facade; decrypted response bodies are not put in CacheStorage. Embedded browser editors share the app origin and use live, connection-owned resource capabilities. The editor frontend and enabled extensions are trusted client code. The server-launched editor extension has a separate capability-protected private loopback report/command bridge; this is trusted endpoint IPC, not a public HTTP exception.
  • Random production invitations redeem through Noise before returning a device PSK. SSO and public-demo handoffs encrypt credentials to a client-held X25519 recipient key using HKDF and AES-256-GCM. Login/bootstrap origins and client-code delivery remain trusted: an active origin or TLS terminator that replaces the initial page/key can compromise that bootstrap. Unsolicited SAML uses a short-lived, single-use browser-bound grant and an explicit sealed claim, not a plaintext credential redirect.
  • The signaling broker relays ciphertext without seeing the PSK. Rooms are invite-gated, capacity-bounded, byte-paced and reaped when idle; room ID alone grants no access. It still sees IP addresses, room/peer identifiers, admission material, record sizes, timing and connection lifetime. It can deny service; encryption does not hide traffic analysis. See broker protocol.
  • The trusted plaintext endpoints are the paired client and the chosen cc_server, including its execution environment. A hosted server operator, compromised endpoint, authorized recipient or configured model/forge provider can still see data it handles. This transport feature does not encrypt databases/backups at rest, erase previously downloaded data, or seal independent MCP, fleet-worker, webhook or provider protocols. TLS remains required for their transport, public metadata, bootstrap and web-code delivery. No independent cryptographic audit is claimed.

Browser push alerts

Web Push is a separate outbound notification channel, not a plaintext exception to the first-party RPC/resource tunnel. Registrations belong to the authenticated active paired device and user; another device cannot read or remove their subscription secrets. VAPID and subscription material stays in owner-private server secrets, with restrictive permissions set before file contents are written.

Only allowlisted push-provider HTTPS endpoints are admitted. Delivery resolves and pins public addresses, rejects private/loopback/link-local targets and redirects, and rechecks device status, workspace membership, recipient targeting and space/repository visibility immediately before the external request. Revocation removes registrations; losing workspace access suppresses delivery even if a browser still holds its local subscription.

Encrypted payloads contain generic alert categories and scoped navigation identifiers, not message previews, approval commands or credentials. The provider still sees endpoint, traffic size and timing. Notification clicks navigate to the first-party origin and do not grant authority; opening content reauthorizes against the server. Quiet/category preferences control delivery, while OS permission and supported PWA installation remain browser requirements.

Dependencies

dependency-audit.yml runs weekly, fails on pub.dev advisories and retracted/discontinued dependencies, and reports outdated packages. Native source/provenance and packaging requirements are documented under cc_natives.

There aren't any published security advisories