Security profiles and limits
Canonical Octonode repository documentation.
Security profiles and limits
Local octonode serve binds to 127.0.0.1 and trusts callers on that loopback
boundary. Production mode requires a bearer credential for every /api route except
/api/health, /api/health/live, /api/health/ready, /api/docs, and
/api/openapi.json; the cloud gateway validates the user's JWT and workspace permissions before
forwarding to the private server. Production host exposure and allowed browser origins must be
configured explicitly with HOST and OCTONODE_CORS_ORIGINS. The CLI production profile also
requires OCTONODE_AUTH_TOKEN and an explicit persistent OCTONODE_STORE_DIR; it refuses to
start when either is missing. Readiness returns 503 when the canonical store is unreadable.
Only GET on those five exact paths is public; wrong methods and prefix lookalikes remain
default-deny. Gateway readiness checks only mode-required bindings and a servable Studio shell,
never a tenant runtime. Release smoke uses separate API-bearer and browser-session fixtures;
authenticated browser traces are prohibited.
Threat model
Architecture review decision (2026-07-25): the local OS user and local project code are trusted. Cloud workspace members, project code, and installed plugins are trusted within that workspace; the per-workspace Cloudflare Sandbox is the security boundary protecting the host and other workspaces. Production server mode never launches host node commands. Revisit this decision before allowing mutually untrusted users or third-party code inside one workspace.
Actors are the local developer, authenticated workspace member, workspace administrator, plugin author, and unauthenticated internet caller. Protected assets are project source and configuration, canonical store records and revisions, Git credentials, workspace secrets, run data, and host or cross-workspace resources. The browser/API boundary is untrusted input; the private server/store boundary is authenticated; child-process execution is trusted-local only; the cloud gateway/Sandbox boundary is the tenant isolation boundary.
| Threat | Boundary and control | Regression evidence | Residual risk / decision |
|---|---|---|---|
| Unauthorized or cross-origin API access | Production bearer auth is default-deny; CORS permits configured origins only; workspace RBAC is checked at the gateway. | tests/server-security.test.ts, tests/cloud-sandbox.test.ts, tests/permissions.test.ts | A stolen token retains its granted access until revoked. |
| Path traversal or cross-project writes | Project, source, Git, restore, and store paths are resolved beneath their owned roots; writes use revision guards and atomic replacement. | tests/project-workspace.test.ts, tests/native.test.ts, tests/backup-restore.test.ts, tests/topology-store-sync.test.ts | The trusted local user can still edit their own files directly. |
| Remote code execution | Local execution is explicitly trusted; production host execution returns 503; hosted execution stays inside the workspace Sandbox. | tests/server-security.test.ts, tests/cloud-sandbox.test.ts, tests/ipc.test.ts | Workspace code can access resources intentionally granted to that workspace. |
| SSRF and undeclared network access | Production host execution is disabled; Sandbox deployment policy owns workspace egress. | tests/server-security.test.ts, tests/cloud-sandbox.test.ts | Fine-grained per-node egress is not promised in v1. |
| Secret exfiltration | APIs redact values, exports omit dotenv files, records redact declared/provenance secret names, and request credentials are not copied into node environments. | tests/variables.test.ts, tests/phase5.test.ts | Trusted workspace code may read secrets explicitly granted to its workspace. |
| Corruption, stale writes, or partial state | Checksummed restore, compare-and-swap revisions, atomic file writes, and store reconciliation reject damage and concurrent stale drafts. | tests/backup-restore.test.ts, tests/config-store-sync.test.ts, tests/topology-store-sync.test.ts | Recovery is bounded by the last successful backup. |
| Resource exhaustion | Request/output sizes, SSE clients, process duration, run concurrency, and queue length are bounded. | tests/server-security.test.ts, tests/source-watch-limiter.test.ts, tests/workflow-run-queue.test.ts, tests/ipc.test.ts | Workspace-level CPU and memory ceilings remain the Sandbox platform's responsibility. |
| Cross-workspace social access and IDOR | Every chat, profile, review, GitHub, and recovery operation derives one workspace principal and rechecks membership, Architecture ownership, contained-Project scope where required, and action at the Cloudflare boundary; missing access returns 404. Durable writes also assert the workspace key and write epoch. | tests/collaboration-schema.test.ts, tests/mcp-collaboration.test.ts, tests/cloud-gateway-routing.test.ts | Members of one workspace intentionally trust each other under the shared-workspace model. |
| Private assistant-session disclosure or unauthorized source edits | Assistant sessions start with only their creator as owner and are excluded from human Chat. Transcript, realtime, and run reads require explicit session membership. Each run mints a short-lived project-bound projects:read, projects:write, and workflows:run credential from the sending user's current permissions; writes use exact base revisions inside that workspace's Sandbox. | tests/chat-agent-store.test.ts, tests/chat-agent-queue.test.ts, tests/chat-agents.test.ts, tests/mcp-project-authoring.test.ts | Explicitly invited members can read the whole session and request edits within their own current permissions. |
| GitHub webhook spoofing or replay | The raw bounded body is verified with HMAC before parsing; delivery ids are unique, normalized payloads are queued, and completed bodies are scrubbed. Installation tokens are short-lived and never enter D1, queues, logs, or clients. | tests/github-review-safety.test.ts | GitHub availability and installation rate limits remain external dependencies. |
| Unauthorized visual baseline approval | CI uploads use a dedicated bearer secret; seven-day capability links become HttpOnly run-scoped cookies; reports, paths, sizes, repository, and story ids are bounded; approval rejects a moved branch and updates the Git ref without force. | tests/visual-review-cloud.test.ts | Anyone who obtains an unexpired private Actions review link can approve that run; rotate the signing secret to revoke all links. |
| Slack OAuth or event forgery | OAuth state is random, hashed, short-lived, one-use, and bound to the initiating user and workspace. Event signatures cover the untouched bounded body and a fresh timestamp; app and installation identities are exact; unique event ids enter a normalized queue before processing. | tests/slack-oauth.test.ts, tests/slack-events.test.ts, tests/integration-secrets.test.ts | Slack availability and provider rate limits remain external dependencies. |
| Stale or duplicate GitHub writes | Review anchors bind to immutable head/base SHAs. Suggestions compare the current head and selected bytes; outbox jobs have stable idempotency markers and installation-serialized delivery. | tests/github-review-safety.test.ts, tests/workflow-review-selection.test.ts | Branch protection may still reject an otherwise valid suggestion commit. |
| Untrusted Markdown and malicious avatars | Studio Markdown disables raw HTML and applies safe link behavior. Avatar uploads accept only bounded PNG/JPEG/WebP bytes, use non-public R2 keys, and serve with nosniff/private cache headers. | Storybook stories and collaboration schema tests | Image decoders in the user's browser remain part of the browser trust boundary. |
| Agent-token leakage or privilege escalation | Agent secrets are shown once, stored only as SHA-256 hashes, limited to the schema-owned action catalog, project-bound when requested, revocable, and excluded from generated plugin creation tools. The private chat service keeps the caller token outside LangGraph state and passes it only to a per-run MCP process. | tests/mcp-collaboration.test.ts, tests/collaboration-schema.test.ts, tests/permissions.test.ts, tests/cloud-gateway-routing.test.ts, tests/agent-security.test.ts | A stolen live token works until expiry or revocation within its scopes. |
| Knowledge-index poisoning or leakage | Only repository-owned public docs and author skills enter the derived Vectorize index. Sync uses a private token, retrieval rechecks project projects:read, and model output receives bounded chunks without project source, prompts, chat, or secrets. | tests/knowledge.test.ts, tests/mcp-project-authoring.test.ts | A malicious repository documentation change can influence assistant guidance and must pass normal code review. |
| Community content poisoning, XSS, or IDOR | Public reads select only moderated published rows; Markdown raw HTML and private architecture embeds are excluded; author writes require exact Design Doc revisions, personal ownership, idempotency, and optimistic versions; moderation requires a non-personal workspace permission. Community retrieval is separately labelled and never enters the trusted knowledge namespace. | tests/community-content-schema.test.ts, tests/community-publication-storage.test.ts, tests/community-publication-api.test.ts | Moderators remain responsible for reviewing deceptive but syntactically valid content. |
| Queue replay, restore abuse, or destructive deletion | Durable outbox state makes queue delivery idempotent; backup manifests are workspace-bound and checksummed; restore/deletion fence epochs and require dedicated owner permissions; deletion requires exact workspace confirmation. | tests/social-recovery.test.ts, tests/github-review-safety.test.ts | A production restore still requires an operator-controlled D1 recovery step and recorded drill. |
Safe defaults bound untrusted work: API and source bodies are 4 MiB, webhook bodies are 1 MiB,
cloud snapshots allow 2,000 files/25 MiB total/2 MiB per file, source-watch SSE concurrency is
32, run concurrency is 8, and the waiting run queue is bounded. Operators can lower connection
and run caps with OCTONODE_SOURCE_WATCH_CONCURRENCY, OCTONODE_RUN_CONCURRENCY, and
OCTONODE_RUN_QUEUE. Limit failures return 413 or 429 without writing the rejected input.
Secrets
Local variables are stored as mode-0600 plaintext dotenv files. Do not use the local profile
on a shared host. Production deployments must use the workspace secret provider; dotenv files
and secret values are excluded from project exports. Variable APIs return [redacted], run
records redact declared and provenance-named secrets before persistence, and logs must never
receive variable values. Updating an existing variable rotates it atomically without editing or
stopping workflows. Values containing a newline or NUL and invalid variable names are rejected.
The coding assistant keeps OPENAI_API_KEY and OCTONODE_CHAT_SERVICE_TOKEN in backend service
environments only. They are removed from node child-process environments and must never enter Studio,
model messages, MCP arguments, conversation records, logs, traces, errors, exports, or backups.
The service rejects browser Origin requests and authenticates its private run endpoint separately
from the opaque caller token used by the project-bound MCP session.
OCTONODE_CLOUD_INTERNAL_TOKEN also protects knowledge-corpus replacement and remains backend-only.
Workspace administration and scoped credentials
Supabase Auth issues user sessions; it does not decide workspace authorization. The Cloudflare
API verifies every user JWT against the configured Supabase JWKS and expected issuer, then resolves
the subject's current role from D1 for the requested personal, organization, or team workspace.
Missing verifier configuration fails closed. Roles are workspace-local, so one subject may be an
organization owner in one workspace and a viewer in another without either grant leaking across
the boundary. A personal workspace has one immutable user:owner with every action.
The action catalog in @octonode/schema is shared by role policy, scoped API credentials, the
gateway, OpenAPI, and Studio. API credentials store only a SHA-256 hash and prefix; the full secret
is returned once. Every request rechecks revocation, expiration, workspace, optional project
binding, and required action before Durable Object or Sandbox access. Credentials cannot mint more
credentials. Identity-bound operations such as creating organizations or teams, connecting OAuth,
and managing Supabase identities also require a user session. Member removal changes only the
selected workspace membership; it never deletes the shared Supabase identity. Organization owners
and the last team lead are protected from removal or demotion.
Workspace analytics are aggregated from the workspace-keyed Durable Object and contain only that tenant's project, workflow, execution, and data-table totals. Membership, invitation, API-key, and GitHub connection totals come from D1 using the same workspace key. The admin dashboard receives no account-wide Cloudflare telemetry and never logs or persists the one-time API-key secret.
Expansion action defaults
Every R10-R13 feature route must call the shared workspace policy service with one of these actions. An action absent from a role is denied. Personal owners, organization owners, and organization admins receive the complete catalog; organization overrides may change only the configurable roles.
| Action family | Org member / team member | Org viewer | Team lead |
|---|---|---|---|
billing:read, billing:manage | none | none | read |
integrations:read, integrations:manage | read | read | read and manage |
design:read, design:review | read and review | read | read and review |
previews:read, previews:manage | read and manage | read | read and manage |
terminal:use | use | none | use |
workflow-agents:run, workflow-agents:manage | run | none | run and manage |
agent-operations:read, agent-operations:manage | read | none | read and manage |
tasks:read, tasks:write, tasks:manage | read and write | read | read, write, and manage |
A team carries no separate policy or billing account. Its orgId is the parent organization used
for role overrides and, by dependent billing code, entitlements. Scoped agents still intersect this
matrix with their explicit action scopes and exact workspace/project binding on every request.
Billing authority and downgrade policy
Stripe owns customers, products, prices, subscriptions, and active entitlements. Control D1 stores one projection account for a personal user or organization; a team always resolves its parent organization account. Database uniqueness prevents either a billable parent from changing customers silently or one Stripe customer from being attached to two parents.
Paid-plan state is never copied into Supabase user_metadata or trusted from a browser/JWT claim.
The verified JWT subject selects the personal billable account at request time, organizations use
their D1 identity, and Studio refreshes the projected Stripe subscription for the active workspace.
POST /hooks/stripe verifies the Stripe-Signature HMAC against the untouched request body with a
five-minute timestamp tolerance. The unique event id enters stripe_webhook_events before any
projection. Completed inbox payloads are cleared, duplicate ids are ignored, mismatched replays are
rejected, and subscription, catalog, and entitlement snapshots accept only a newer (created, id)
version. The scheduled Worker retries failed inbox rows and reconciles every attached customer against
Stripe's paginated subscriptions and active-entitlements APIs using the pinned API version.
Permissions and entitlements answer different questions and are recorded together in
billing_access_audit. Only cost-bearing mutations use the entitlement gate. Active and trialing
subscriptions may use projected features; past_due keeps them for a 72-hour grace period, then
fails closed. Missing entitlements and incomplete, paused, unpaid, or cancelled subscriptions fail
closed. Project/data reads, export, billing access, and workspace deletion remain permission-only so
downgrade never prevents recovery, payment repair, or deletion. STRIPE_API_KEY and
STRIPE_WEBHOOK_SECRET are Worker secrets and never enter a browser, workspace Sandbox, event log,
or audit row.
Chargeable operations use a caller-owned stable source event id and append an immutable row to
usage_ledger; database triggers reject updates, deletes, duplicate reversals, and operations that
would exceed an active hard quota. The same transaction updates the bounded account/meter/period
projection and records each warning, soft, or hard budget threshold once. A scheduled reconciliation
rebuilds those totals from the ledger, which remains authoritative even when telemetry or Stripe is
unavailable.
Stripe meter deliveries use a deterministic, content-free identifier and idempotency key. Explicit ledger reversals cancel the corresponding Stripe meter event. Retryable delivery failures use capped backoff and stop after eight attempts; permanent failures stop immediately for operator review. Structured usage and budget events contain only the billable-account id, meter, quantity, threshold, and delivery status. Prompts, source payloads, command output, secrets, and user content never enter billing metrics or alerts.
Collaboration deployment controls
SOCIAL_ROLLOUT is the server authority: off, personal, a comma-separated allowlist such as
personal,org:<id>, or all. The workspace permissions response exposes
capabilities.collaboration, and the shared collaboration authorization boundary rejects disabled
workspaces. Deployment defaults to personal; promote one test organization before all.
The GitHub App should request repository metadata/contents and pull-request read/write access only; it does not request organization administration or Actions permissions. GitHub installation access is linked only to a validated registered Architecture. The link records bounded Project IDs and repository-relative roots so webhook file paths can be routed without trusting caller-supplied roots. tokens are minted just in time inside the Cloud worker. Explicit user actions publish inline review comments or submit reviews. General workflow discussion uses a pull-request conversation comment, which supports later update/deletion; submitted review summaries are not used as mutable storage.
Before broad enablement, review cross-workspace denial, two-browser socket revocation, webhook signature/replay fixtures, stale-head suggestions, backup/restore/deletion, DLQ depth, oldest outbox age, oldest backup age, and GitHub App permissions. Abort rollout on authorization leakage, lost acknowledged writes, an unrecoverable restore, or sustained queue age beyond the operating SLO.
Slack connection security
The first Slack product slice supports public-channel history and delivery only. Its bot scopes are
exactly channels:history, channels:read, chat:write, and users:read; private-channel,
workspace-administration, and user-token scopes are not requested. Only a browser user with
integrations:manage may install or revoke. Members with integrations:read may inspect identity,
status, granted scopes, and drift, but no credential fields.
Access and refresh tokens are JSON inside a versioned AES-256-GCM envelope. The current wrapping-key
version selects a base64url-encoded 32-byte key from INTEGRATION_WRAPPING_KEYS; authenticated data
binds the workspace kind/id, provider, and connection id. Keep old versions during rotation until all
live envelopes have been refreshed. Workspace exports and social backups omit the connection registry;
operator D1 recovery snapshots contain ciphertext only. Tokens are never copied to Studio responses,
queues, logs, social records, node environments, or Sandbox configuration.
/hooks/slack verifies Slack's v0 HMAC over the raw body and rejects timestamps outside five
minutes before JSON parsing. The configured Slack app id and the event's team/enterprise installation
mode select one connection exactly. Unbound channels retain only the unique event id, body hash, and
content-free control metadata. An administrator must explicitly bind one public channel and direction
before its bounded message text enters the seven-day retry inbox. Successful processing clears that
inbox payload immediately; expired failed payloads are deleted by scheduled maintenance.
Provider message/thread ids and the durable Slack outbox live with chat in the workspace Durable
Object, so they follow collaboration export, restore, retention, and deletion. External Slack authors
carry a stable slack:<id> attribution, display label, and origin=slack; no membership row is
created. Outbound messages carry an octonode_bridge metadata marker, and bot-user checks plus unique
delivery/mapping/outbox keys stop echoes and retries from becoming local loops. Retry-After controls
rate-limit retries, with a maximum of eight provider attempts.
Disabling a binding or beginning connection revocation stops new ingress and egress before the
provider call completes. Local deletion calls chat.delete only for messages posted by the Octonode
bot; Slack does not permit the bot to delete another author's message. Revocation, workspace deletion,
or a local tombstone therefore must not be reported as erasing pre-existing Slack history remotely.