Octonode Playbook

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.

ThreatBoundary and controlRegression evidenceResidual risk / decision
Unauthorized or cross-origin API accessProduction 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.tsA stolen token retains its granted access until revoked.
Path traversal or cross-project writesProject, 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.tsThe trusted local user can still edit their own files directly.
Remote code executionLocal 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.tsWorkspace code can access resources intentionally granted to that workspace.
SSRF and undeclared network accessProduction host execution is disabled; Sandbox deployment policy owns workspace egress.tests/server-security.test.ts, tests/cloud-sandbox.test.tsFine-grained per-node egress is not promised in v1.
Secret exfiltrationAPIs 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.tsTrusted workspace code may read secrets explicitly granted to its workspace.
Corruption, stale writes, or partial stateChecksummed 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.tsRecovery is bounded by the last successful backup.
Resource exhaustionRequest/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.tsWorkspace-level CPU and memory ceilings remain the Sandbox platform's responsibility.
Cross-workspace social access and IDOREvery 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.tsMembers of one workspace intentionally trust each other under the shared-workspace model.
Private assistant-session disclosure or unauthorized source editsAssistant 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.tsExplicitly invited members can read the whole session and request edits within their own current permissions.
GitHub webhook spoofing or replayThe 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.tsGitHub availability and installation rate limits remain external dependencies.
Unauthorized visual baseline approvalCI 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.tsAnyone who obtains an unexpired private Actions review link can approve that run; rotate the signing secret to revoke all links.
Slack OAuth or event forgeryOAuth 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.tsSlack availability and provider rate limits remain external dependencies.
Stale or duplicate GitHub writesReview 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.tsBranch protection may still reject an otherwise valid suggestion commit.
Untrusted Markdown and malicious avatarsStudio 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 testsImage decoders in the user's browser remain part of the browser trust boundary.
Agent-token leakage or privilege escalationAgent 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.tsA stolen live token works until expiry or revocation within its scopes.
Knowledge-index poisoning or leakageOnly 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.tsA malicious repository documentation change can influence assistant guidance and must pass normal code review.
Community content poisoning, XSS, or IDORPublic 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.tsModerators remain responsible for reviewing deceptive but syntactically valid content.
Queue replay, restore abuse, or destructive deletionDurable 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.tsA 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 familyOrg member / team memberOrg viewerTeam lead
billing:read, billing:managenonenoneread
integrations:read, integrations:managereadreadread and manage
design:read, design:reviewread and reviewreadread and review
previews:read, previews:manageread and managereadread and manage
terminal:useusenoneuse
workflow-agents:run, workflow-agents:managerunnonerun and manage
agent-operations:read, agent-operations:managereadnoneread and manage
tasks:read, tasks:write, tasks:manageread and writereadread, 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.

On this page