Skip to content

Latest commit

 

History

History
393 lines (313 loc) · 19.7 KB

File metadata and controls

393 lines (313 loc) · 19.7 KB

Workplace security

This fork is designed to run against sensitive internal source code. The local CostasCode harness remains intact, but application-owned networking is deny-by-default and hosted CostasCode/Anomaly services are not part of the shipped product.

Product architecture

Surface Technology Security role
CLI and local server Bun/TypeScript, Effect HTTP, Node HTTP listener packages/opencode; owns CLI commands, sessions, tools, local MCP, PTY, provider composition, and the legacy HTTP API
Session core Effect services and SQLite packages/core; owns durable sessions, filesystem/location boundaries, credentials, projects, worktrees, and native LLM routing
Protocol/server/client Effect HttpApi and generated clients packages/protocol, packages/server, packages/client; client generation runs from packages/client
LLM protocol layer Effect HTTP/WebSocket transports packages/llm; provider-independent protocol and stream handling
Web application SolidJS/Vite packages/app; connects only to a loopback backend
Desktop Electron 42 with sandboxed renderer packages/desktop; loads oc://renderer, starts an authenticated loopback utility-process sidecar, and exposes a narrow preload API

The cloud console, hosted web site, enterprise service, stats site, Slack app, and serverless function sources are intentionally excluded from the root Bun workspace. They are not installed, typechecked, built, packaged, or released by the workplace product.

Network policy

The shared contract is @opencode-ai/core/outbound:

  • Outbound.classify(url) parses and classifies a destination.
  • Outbound.authorize(url, transport, audit?) fails closed and emits a metadata-only audit event.
  • Outbound.guardedFetch(...) disables automatic redirects, validates every redirect, permits same-origin redirects only, and caps redirect depth.
  • Outbound.installGlobalPolicy(audit?) wraps process/browser fetch and the global WebSocket constructor.

The Node composition installs the policy from packages/core/src/effect/app-node-platform.ts. The CLI installs it at process startup in packages/opencode/src/index.ts. Legacy and V2 model resolution also validate the configured provider endpoint before invoking SDKs, covering clients that use their own HTTP/WebSocket implementation. Electron enforces the same policy through session.webRequest in packages/desktop/src/main/windows.ts.

The OpenAI-compatible ws transport performs its own authorization before constructing the third-party WebSocket client. Ambient HTTP proxy variables are not forwarded to that transport, so an approved destination cannot be silently rerouted through an undocumented proxy.

Allowed destination matrix

Destination Protocol Purpose
Embedded data: resources Fetch only In-memory image/media conversion without network access
localhost, 127.0.0.0/8, [::1] HTTP(S), WS(S) Local server, PTY, callback, local provider, and explicitly enabled local MCP traffic. External browser opens are restricted to HTTP(S).
github.com HTTPS/WSS GitHub device OAuth and user-opened repository/support links
api.github.com HTTPS/WSS GitHub API and GitHub Copilot token operations
docs.github.com HTTPS/WSS User-opened GitHub documentation
api.githubcopilot.com HTTPS/WSS GitHub Copilot models and inference
api.individual.githubcopilot.com HTTPS/WSS GitHub Copilot individual routing
api.business.githubcopilot.com HTTPS/WSS GitHub Copilot business routing
api.enterprise.githubcopilot.com HTTPS/WSS GitHub Copilot enterprise-plan routing on GitHub.com
copilot-proxy.githubusercontent.com HTTPS/WSS GitHub Copilot proxy routing

Public destinations require TLS and the default port. Host matching is exact: userinfo, trailing dots, suffix confusion, non-default ports, unsupported schemes, and alternate non-loopback IP forms are rejected. The URL parser canonicalizes integer, hexadecimal, octal, and shortened IPv4 forms before the loopback decision.

Redirects cannot cross origins. This prevents credentials from following a redirect to another allowlisted host. New transport names fail closed; tests in packages/core/test/outbound.test.ts are the protocol-conformance gate for new callback/transport types.

The policy trusts the operating system resolver and TLS validation for the fixed GitHub hostnames. Arbitrary DNS names are never accepted merely because they resolve to loopback, which removes the application-level DNS-rebinding case. localhost and literal loopback addresses are the only local names.

Local audit records

Node processes append to:

$XDG_STATE_HOME/opencode/security/outbound.jsonl

The directory is 0700; the file is 0600. Records contain only:

time, decision, destination category, transport, reason

They never contain a URL, URL path, prompt, code, headers, token, account, session/project identifier, or request/response body. Blocked hosts are recorded only as destination: "blocked".

Removed network and hosted surfaces

The workplace product removes these production graph edges:

  • Sentry runtime capture and source-map upload plugins
  • OpenTelemetry log/trace exporters and AI SDK telemetry configuration
  • Chromium net-log capture and Electron crash-dump collection
  • hosted UI proxying to app.opencode.ai
  • session share/upload/sync workers and automatic share behavior
  • CostasCode Console account/provider integration and remote managed config
  • CostasCode Go upsell/account links
  • desktop and CLI update checks, downloads, and updater IPC/menu actions
  • mDNS/Bonjour discovery and non-loopback server options
  • WSL remote sidecars and desktop custom-protocol deep links
  • models.dev background refresh
  • models.dev build-time fetches and provider-icon downloads; builds use the checked-in, hosted-provider-free packages/schema/models-api.json snapshot
  • runtime ripgrep and language-server downloads
  • npm-on-demand plugins and automatic project plugin execution
  • the Anomaly-hosted ghostty-web Git dependency; the terminal uses the integrity-locked ghostty-web npm release from the upstream Coder project
  • hosted deployment, analytics/stats, Discord notification, and upstream publishing workflows

The security static gate rejects reintroduction of the removed packages, endpoints, wildcard listener/CSP settings, and unguarded Electron external-link calls.

Local server boundary

Server.listen rejects every hostname except 127.0.0.1, localhost, and ::1. CLI configuration cannot add CORS origins or enable discovery. The web/desktop server picker also canonicalizes stored and newly entered URLs through the same outbound policy, discards stale remote entries, and visibly states that workplace builds accept loopback servers only.

Before routing, the server:

  1. parses and validates the Host header as loopback;
  2. parses Origin exactly and permits only loopback web origins or the exact Electron origin oc://renderer;
  3. rejects hostile browser origins before a mutation executes;
  4. applies HTTP Basic authentication when configured;
  5. requires a scoped, random, single-use, 60-second ticket for browser PTY WebSocket connections; and
  6. validates WebSocket Origin and Host through the same exact-origin helpers.

The Electron sidecar always uses a random in-memory password and binds to 127.0.0.1. No password is persisted in renderer storage. CLI listeners are loopback-only; callers that need same-user process isolation should also set OPENCODE_SERVER_PASSWORD.

Desktop boundary

The desktop renderer uses:

  • contextIsolation: true
  • nodeIntegration: false
  • sandbox: true
  • webSecurity: true
  • no <webview> tag
  • no insecure mixed content
  • a local oc://renderer protocol with canonical path containment
  • a restrictive CSP, permissions policy, referrer policy, COOP/CORP, and nosniff

Top-level navigation is limited to the exact renderer origin. New windows are always denied; approved GitHub links are opened externally after outbound validation. Every IPC handler verifies the sender frame is the exact packaged renderer origin (or the exact local development origin in an unpackaged build). ELECTRON_RENDERER_URL is ignored in packaged applications and must resolve to an HTTP(S) loopback origin in development. Store filenames use a strict basename grammar, picked-file reads are scoped to the renderer and token, and attachment file identity is rechecked after the picker to prevent symlink/replace races.

Production builds do not enable remote debugging, source-map upload, crash upload, update polling, or custom protocol deep links. Developer tools are not exposed in the product menu.

macOS packages disable arbitrary network loads, retain only explicit loopback HTTP exceptions for the local sidecar, remove inherited camera, microphone, and Bluetooth usage declarations, and grant only Electron's required JIT entitlement. Beta and production packages fail before packing unless Developer ID signing and complete notarization inputs are present. Unsigned local macOS packages require the explicit package:mac:dev command.

Repository, filesystem, and process boundary

Relative filesystem reads resolve both the lexical path and real path beneath the active Location. Mutations use LocationMutation to detect relative escape, symlink escape, and non-directory ancestors before permission evaluation.

Worktree removal/reset:

  • canonicalizes the requested directory;
  • requires it to be beneath the per-project managed worktree root;
  • requires an exact entry from git worktree list;
  • never recursively deletes an unknown directory;
  • passes Git arguments as arrays rather than shell strings;
  • does not run repository-provided startup scripts; and
  • resets only from locally available refs and submodule objects (--no-fetch).

Project-copy names accept one safe path segment only. Desktop store cleanup and custom protocol file loading reject traversal. Runtime archive download and extraction paths were removed with the updater/LSP/ripgrep downloaders.

An untrusted repository may still provide instructions, agents, commands, and skills because these are core harness capabilities. They are prompt input, not a sandbox boundary. Tool permissions remain mandatory user-attention controls.

Plugins and MCP

Project-local plugins never execute. Registry plugin installation is disabled. Only an already-installed file plugin declared by trusted global configuration may load.

Project-local JavaScript/TypeScript tools are also executable plugins in practice, so they are disabled by default. A user who has reviewed a repository may opt in for that process with:

OPENCODE_ALLOW_PROJECT_TOOLS=1

Formatter configuration is also executable: formatter commands run automatically after edits. Repository formatter settings are therefore ignored. Built-in and custom formatters remain available from user-global, explicitly process-selected, or managed configuration; there is no repository trust opt-in until the product has a concrete persisted trust decision.

Repository reference settings are ignored for the same open-time side-effect reason. Trusted global references remain available. Git references additionally accept only GitHub HTTPS URLs and GitHub shorthand; arbitrary hosts, SSH/SCP forms, userinfo, and non-default ports are rejected before Git cache work starts.

Language servers can execute repository-local binaries and are likewise disabled by default. Reviewed repositories may opt in with:

OPENCODE_ALLOW_LOCAL_LSP=1

Opted-in language servers receive an environment with credential, proxy, dynamic-loader, and process-injection variables removed. Language-server downloads remain permanently disabled.

Remote MCP URLs must be loopback. Local stdio MCP is disabled by default and requires the user-level environment opt-in:

OPENCODE_ALLOW_LOCAL_MCP=1

Repository config cannot set that process-level decision. An opted-in MCP child must stay inside the project working directory after realpath resolution and receives a minimal environment; loader and proxy injection variables are stripped.

MCP OAuth callbacks bind to 127.0.0.1, require the exact callback path and Host header, accept GET only, reject occupied callback ports, expire after five minutes, and consume state once.

Secrets, credentials, and logs

CostasCode applies a 0077 process umask. Data/config/state/log directories are 0700. SQLite, auth JSON, audit logs, exported debug archives, and related state files are 0600.

Legacy provider credentials are replaced atomically in auth.json; V2 credentials live in the permission-restricted SQLite store. Desktop renderer storage never receives provider credentials. GitHub Copilot authentication uses the CostasCode OAuth application's device flow with the read:user scope. MCP token expiry metadata is enforced before reuse.

The cross-platform CLI does not silently fall back from an unavailable OS keychain to plaintext renderer storage. This fork uses its explicit permission-restricted local credential store. Deployments requiring hardware-backed storage should inject short-lived credentials through the process environment and clear them after use.

Local logs recursively redact authorization, cookies, credentials, secrets, passwords, tokens, API keys, prompts, content/body/header fields, URL/path fields, account/session identifiers, email, URL text, bearer/basic values, and common absolute filesystem paths. Sidecar stdout/stderr is represented by byte count only. Debug-log export is always a user action.

GitHub Copilot provider contract

CostasCode uses OpenCode's direct GitHub Copilot adapter for model discovery and inference. The experimental Copilot SDK bridge remains in source for isolated testing, but is not registered as the default provider because the SDK owns an agent/session loop rather than exposing a transparent inference transport.

The parallel inference integration must:

  1. provide an explicit loopback or exact GitHub Copilot baseURL;
  2. use the application fetch/Effect HTTP layer or Outbound.guardedFetch;
  3. declare any new GitHub hostname in the matrix and add policy tests before use;
  4. never synthesize enterprise/customer-controlled hostnames;
  5. never follow cross-origin redirects with credentials; and
  6. keep prompts, code, headers, and identifiers out of audit records.

Generic provider/model configuration, model settings, protocol hooks, and web search tool hooks remain present. A configured endpoint outside the matrix fails explicitly before provider execution.

Build, SBOM, and release gates

Tests must run from package directories.

bun run lint
bun run security:check
bun run security:attest --app packages/desktop/dist/mac-arm64/CostasCode.app
bun run security:sbom
bun run script/turbo.ts typecheck
(cd packages/core && bun test)
(cd packages/core && bun typecheck)
(cd packages/opencode && bun test)
(cd packages/opencode && bun run test:httpapi)
(cd packages/opencode && bun typecheck && bun run build)
(cd packages/app && bun test && bun typecheck && bun run build)
(cd packages/desktop && bun test && bun typecheck && bun run build)

script/security/sbom.ts emits deterministic CycloneDX 1.6 JSON from the locked workspace and package graph. CI generates it twice and requires byte identity.

script/security/attest-package.ts inspects the production dependency closure and packaged app.asar, rejects telemetry/updater/hosted-share and raw network-client signatures, checks effective macOS entitlements, ATS, bundle identity, and URL schemes, and emits SHA-256 hashes for every packaged file plus a canonical artifact-tree hash. It writes the report before failing.

script/turbo.ts is the only repository Turbo entrypoint. It sets TURBO_TELEMETRY_DISABLED=1 and DO_NOT_TRACK=1 before starting Turbo.

For reproducibility checks:

export SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)"
bun run security:manifest --output /tmp/build-a.json
# rebuild from a clean worktree with the same lockfile and environment
bun run security:manifest --output /tmp/build-b.json
cmp /tmp/build-a.json /tmp/build-b.json

The repository has no automatic product publishing or updater feed. A release is a manual human decision after security, generated-client, package, integration, desktop, SBOM, and reproducibility gates pass. Generated bundles and the lockfile must be scanned for removed endpoint/dependency names and credential patterns before distribution.

Safe upstream sync

Upstream is anomalyco/opencode, normally branch dev. Product changes belong only on copilot-workplace.

  1. Fetch upstream into a temporary sync branch; never merge product changes into local dev/main.
  2. Inventory upstream changes to networking, providers, auth, config, plugins, MCP, filesystem, desktop, build scripts, and dependencies.
  3. Port upstream commits into copilot-workplace without restoring cloud, telemetry, updater, remote-listener, or auto-install behavior.
  4. If Protocol or Server HttpApi changed, run bun run generate from packages/client; never edit generated clients directly.
  5. Run the static workplace gate before tests so a newly introduced endpoint or exporter fails early.
  6. Regenerate the SBOM and compare dependency deltas.
  7. Run the full gates above and review the final diff specifically for raw network clients and spawned subprocesses.

Intentional exclusions

  • The permission system is not an OS sandbox. Run hostile agent work in a VM or container.
  • User-approved shell/terminal commands, Git commands, and explicitly opted-in local MCP, project-tool, and language-server processes are user-controlled. They can perform their own networking outside the application-owned policy.
  • LLM data handling on the allowed GitHub Copilot service is governed by the organization’s GitHub/Copilot policy.
  • Dormant hosted-service source directories excluded from the Bun workspace are retained only to reduce upstream-sync conflict. They are not a product or release input.
  • A same-user malicious local process can read another same-user process’s memory and interact with loopback services unless OS isolation or a server password is used.

Reporting

Report vulnerabilities privately through the SlowGreek/opencode GitHub Security Advisory interface. Do not include credentials, prompts, proprietary code, or unredacted logs.