* refactor(repo): move OpenWork apps into apps and ee layout Rebase the monorepo layout migration onto the latest dev changes so the moved app, desktop, share, and cloud surfaces keep working from their new paths. Carry the latest deeplink, token persistence, build, Vercel, and docs updates forward to avoid stale references and broken deploy tooling. * chore(repo): drop generated desktop artifacts Ignore the moved Tauri target and sidecar paths so local cargo checks do not pollute the branch. Remove the accidentally committed outputs from the repo while keeping the layout migration intact. * fix(release): drop built server cli artifact Stop tracking the locally built apps/server/cli binary so generated server outputs do not leak into commits. Also update the release workflow to check the published scoped package name for @openwork/server before deciding whether npm publish is needed. * fix(workspace): add stable CLI bin wrappers Point the server and router package bins at committed wrapper scripts so workspace installs can create shims before dist outputs exist. Keep the wrappers compatible with built binaries and source checkouts to avoid Vercel install warnings without changing runtime behavior.
33 KiB
OpenWork Server
Bridge missing capabilities between OpenWork and OpenCode
Summarize
Introduce an OpenWork server layer that fills gaps in OpenCode APIs, enabling remote clients to manage workspace config, skills, plugins, and MCPs without direct filesystem access.
Define problem
Remote clients cannot read or write workspace config because critical state lives in the filesystem. OpenWork needs a safe, minimal surface to access and mutate config, skills, plugins, and MCPs when connected to a host.
Set goals
- Bridge OpenWork needs that OpenCode does not expose today
- Enable remote clients to view and update workspace config safely
- Keep the surface minimal, auditable, and aligned to OpenCode primitives
Mark non-goals
- Replacing OpenCode's server or duplicating its APIs
- Arbitrary filesystem access outside approved workspace roots
- Hosting multi-tenant or cloud-managed instances
Describe personas
- Remote client user: needs visibility into installed skills/plugins while connected
- Host operator: wants safe, explicit control over config changes
List requirements
- Functional: expose workspace config read/write APIs for
.opencodeandopencode.json - Functional: list installed skills, plugins, MCPs for a workspace without direct FS access
- Functional: allow saving new skills, plugins, and MCP entries from a remote client
- Functional: host mode auto-starts the OpenWork server alongside the OpenCode engine
- UX: surface pairing URL + tokens in Settings for host mode
- UX: show remote-config origin, last updated time, and change attribution
Define API (initial)
All endpoints are scoped to an approved workspace root and require host approval for writes.
When OpenCode already exposes a stable API (agents, skills, MCP status), prefer OpenCode directly and avoid duplicating it here. This server only covers filesystem-backed gaps.
API conventions
- Base URL: provided by the host during pairing (e.g.,
http://host:PORT/openwork) - Content-Type:
application/json - Auth: bearer token issued during pairing (
Authorization: Bearer <token>) - Errors: JSON body with
{ code, message, details? } - All writes require explicit host approval and return
403when denied
Workspaces (discovery)
GET /workspaces-> list known workspaces on the host- Response fields align with
WorkspaceInfo(id, name, path, workspaceType, baseUrl?, directory?) - Used by remote clients to select a workspace without filesystem access
- Response fields align with
Health
GET /health-> { ok, version, uptimeMs }
Capabilities
GET /capabilities-> { skills: { read, write, source }, plugins: { read, write }, mcp: { read, write }, commands: { read, write }, config: { read, write } }sourceindicates whether OpenCode or OpenWork server is the authoritative API
Workspace config
GET /workspace/:id/config-> returns parsedopencode.json+.opencode/openwork.jsonPATCH /workspace/:id/config-> merges and writes config (write approval required)- Request body:
{ opencode?: object, openwork?: object } - Merge strategy: shallow merge at top-level keys; arrays replaced (aligns with OpenCode config behavior)
- Response:
{ opencode, openwork, updatedAt } - Only project config is writable by default; global config requires explicit host-only scope
- Request body:
Skills
- Prefer OpenCode skills API when available; OpenWork server only fills local FS gaps.
GET /workspace/:id/skills-> list skill metadata from.opencode/skills(fallback only)POST /workspace/:id/skills-> add/update skill file(s) (write approval required, fallback only)- Request body:
{ name, content, description? } - Writes to
.opencode/skills/<name>/SKILL.md - Response:
{ name, path, description, scope }
- Request body:
Plugins
GET /workspace/:id/plugins-> list configured plugins fromopencode.jsonPOST /workspace/:id/plugins-> add plugin entry to config (write approval required)DELETE /workspace/:id/plugins/:name-> remove plugin entry (write approval required)- Request body (POST):
{ spec }wherespecis a plugin string (npm or file URL) - Response:
{ items: [{ spec, source, scope, path? }], loadOrder: string[] } - Server de-dupes by normalized name (strip version where possible)
- Request body (POST):
MCPs
- Prefer OpenCode MCP APIs for status/runtime; OpenWork server only reads/writes config.
GET /workspace/:id/mcp-> list configured MCP serversPOST /workspace/:id/mcp-> add MCP config entry (write approval required)DELETE /workspace/:id/mcp/:name-> remove MCP config entry (write approval required)- Request body (POST):
{ name, config }whereconfigmatchesMcpServerConfig - Response:
{ items: [{ name, config, source }] }
- Request body (POST):
Agents
- Prefer OpenCode agents API; no OpenWork server endpoints needed unless OpenCode lacks coverage for remote clients.
Commands
GET /workspace/:id/commands-> list commands from.opencode/commands(fallback only)- Optional query:
scope=workspace|global(global requires host-only approval)
- Optional query:
POST /workspace/:id/commands-> create/update command (write approval required)DELETE /workspace/:id/commands/:name-> delete command (write approval required)- Request body (POST):
{ name, description?, template, agent?, model?, subtask? } - Server writes
.opencode/commands/<name>.mdwith YAML frontmatter
- Request body (POST):
OpenCode behavior reference (from docs)
This section captures the exact OpenCode semantics the OpenWork server must respect.
Plugins
- Config list:
opencode.json->pluginfield. Can be string or string[]. - Local plugin folders:
- Project:
.opencode/plugins/ - Global:
~/.config/opencode/plugins/
- Project:
- Load order (all hooks run in sequence):
- Global config
~/.config/opencode/opencode.json - Project config
opencode.json - Global plugin dir
~/.config/opencode/plugins/ - Project plugin dir
.opencode/plugins/
- Global config
- NPM plugins are installed automatically using Bun at startup.
- Cached node_modules live in
~/.cache/opencode/node_modules/. - Local plugins can use dependencies listed in
.opencode/package.json. - Duplicate npm packages with the same name+version load once; local and npm with similar names both load.
Skills
- Skill discovery paths (project):
.opencode/skills/<name>/SKILL.md.claude/skills/<name>/SKILL.md
- Skill discovery paths (global):
~/.config/opencode/skills/<name>/SKILL.md~/.claude/skills/<name>/SKILL.md
- Discovery walks up from current working directory to git worktree root.
SKILL.mdmust include YAML frontmatter withnameanddescription.- Name constraints:
- 1-64 chars
- lowercase alphanumeric with single hyphen separators
- must match directory name
- regex:
^[a-z0-9]+(-[a-z0-9]+)*$
- Description length: 1-1024 chars.
- Permissions:
opencode.jsonpermission.skillsupportsallow,deny,askpatterns.
MCP servers
- Config lives in
opencode.jsonundermcpobject. - Local MCP fields:
type: "local",command[], optionalenvironment,enabled,timeout. - Remote MCP fields:
type: "remote",url, optionalheaders,oauth,enabled,timeout. oauth: falsedisables automatic OAuth detection.- Remote defaults can be provided via
.well-known/opencode; local config overrides. - MCP tools can be disabled globally in
toolsvia glob patterns. - Per-agent tool enabling overrides global settings.
OpenCode server APIs
- OpenCode already exposes:
/config,/mcp(runtime),/agent,/command(list),/session. - OpenWork server should prefer OpenCode APIs for runtime status and only handle FS-backed config gaps.
- When reading config, prefer OpenCode
/configto capture precedence (remote defaults + global + project). - When writing, only modify project
opencode.jsonto avoid clobbering upstream defaults. /commandis list-only; OpenWork server adds create/delete via filesystem for remote clients.
Config precedence (reference)
- Remote defaults from
.well-known/opencode - Global config
~/.config/opencode/opencode.json - Project config
opencode.json .opencode/directories and inline env overrides- OpenWork server should preserve this ordering when presenting config sources
OpenCode server alignment (from docs)
- OpenCode runs an HTTP server via
opencode serve(default127.0.0.1:4096). --corscan be used to allow browser origins; OpenWork should align with that for web clients.- Basic auth can be enabled via
OPENCODE_SERVER_PASSWORD(username defaults toopencode). - The OpenWork server should not bypass OpenCode auth; if OpenCode is password-protected, the host UI must collect credentials and pass them to the client.
- OpenCode publishes its OpenAPI spec at
/doc; the OpenWork server should track upstream changes and avoid duplicating stable APIs.
Host auto-start + pairing UX
- When OpenWork runs in Host mode, it starts the OpenWork server automatically after the OpenCode engine comes online.
- The host UI exposes a pairing card in Settings with:
- OpenWork Server URL (prefers
.localhostname, falls back to LAN IP) - Client token (for remote devices)
- Host token (for approvals)
- OpenWork Server URL (prefers
- Tokens are generated per run unless supplied by host config.
- Pairing info should be copyable (tap-to-copy) and masked by default.
Endpoint examples (requests and responses)
List workspaces
Request:
GET /workspaces
Authorization: Bearer <token>
Response:
{
"items": [
{ "id": "ws_1", "name": "Finance", "path": "/Users/susan/Finance", "workspaceType": "local" },
{ "id": "ws_2", "name": "Remote Ops", "path": "/Users/bob/Shared", "workspaceType": "remote", "baseUrl": "http://10.0.0.8:4096" }
]
}
Get config
Request:
GET /workspace/ws_1/config
Response:
{
"opencode": { "plugin": ["opencode-github"], "mcp": { "chrome": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] } } },
"openwork": { "version": 1, "authorizedRoots": ["/Users/susan/Finance"] }
}
Patch config
Request:
PATCH /workspace/ws_1/config
{ "opencode": { "plugin": ["opencode-github", "opencode-notion"] } }
Response:
{ "updatedAt": 1730000000000 }
Add plugin
Request:
POST /workspace/ws_1/plugins
{ "spec": "opencode-notion" }
Response:
{
"items": [
{ "spec": "opencode-github", "source": "config", "scope": "project" },
{ "spec": "opencode-notion", "source": "config", "scope": "project" }
],
"loadOrder": ["config.global", "config.project", "dir.global", "dir.project"]
}
Add skill
Request:
POST /workspace/ws_1/skills
{ "name": "expense-audit", "content": "# Expense Audit\n..." }
Response:
{ "name": "expense-audit", "path": ".opencode/skills/expense-audit", "description": "Audit expenses...", "scope": "project" }
Add MCP server
Request:
POST /workspace/ws_1/mcp
{ "name": "chrome", "config": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] } }
Response:
{ "items": [{ "name": "chrome", "config": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] }, "source": "config.project" }] }
Add command
Request:
POST /workspace/ws_1/commands
{ "name": "daily-report", "description": "Daily report", "template": "summarize yesterday", "agent": "default" }
Response:
{ "items": [{ "name": "daily-report", "description": "Daily report", "template": "summarize yesterday", "agent": "default", "scope": "workspace" }] }
Outline integration
- Host side: OpenWork server exposes a narrow API layer on top of OpenCode
- Implementation: Bun-based server initially, shipped as a sidecar inside the OpenWork desktop app
- Client side: OpenWork UI uses this layer when remote, falls back to FS when local
- Storage: persists changes to
.opencodeandopencode.jsonwithin workspace root
Server runtime and sidecar lifecycle
- The desktop app launches the OpenWork server as a sidecar process.
- The server binds to localhost on an ephemeral port and reports its URL back to the host UI.
- The host UI includes the OpenWork server URL in the pairing payload for remote clients.
- On crash, the host restarts the server and re-issues capabilities.
- The server must never expose filesystem paths outside approved workspace roots.
Workspace identity and scoping
- Workspaces are referenced by
workspace.id(stable hash of the workspace path). - Remote clients fetch the list via
GET /workspacesand select a target id. - Each request includes
workspaceIdin the path; server resolves to a validated root. - If the workspace is missing or not authorized, return
404or403.
Authentication and pairing
- The host generates a short-lived pairing token and includes the OpenWork server base URL in the pairing payload.
- Remote clients store the token in memory (not on disk) and send it as
Authorization: Bearer <token>. - Tokens are scoped to the host and expire on disconnect or after a short TTL (e.g., 24h).
- The OpenWork server rejects requests without a valid token (
401). - Future: rotate tokens on reconnect and support revocation from host settings.
Capability schema (example)
{
"skills": { "read": true, "write": true, "source": "openwork" },
"plugins": { "read": true, "write": true },
"mcp": { "read": true, "write": true },
"commands": { "read": true, "write": true },
"config": { "read": true, "write": true }
}
Audit log format (example)
{
"id": "audit_123",
"workspaceId": "ws_abc",
"actor": { "type": "remote", "clientId": "client_1" },
"action": "plugins.add",
"target": "opencode.json",
"summary": "Added opencode-github",
"timestamp": 1730000000000
}
Web app integration
- Remote clients connect to both OpenCode (engine) and OpenWork server (config layer)
- Capability check gates UI actions: if OpenWork server is missing, config actions are read-only
- Writes require host approval and are surfaced in the audit log
- Future: this evolves into a sync layer across clients (out of scope here)
Client caching and consistency
- Cache the last successful config snapshot per workspace in local state.
- On write success, refresh via
GET /workspace/:id/configbefore updating UI. - Use optimistic UI only for read-only lists; for writes, wait for approval + server response.
- If OpenWork server is unreachable, show read-only data and a reconnect CTA.
Sequence flows (examples)
Add plugin (remote)
- User clicks “Add plugin” in Plugins page.
context/extensions.tscallsPOST /workspace/:id/pluginswith{ spec }.- Server requests host approval; host approves.
- Server writes
opencode.json, returns updated list. - Client refreshes plugins list and shows success toast.
Add skill (remote)
- User uploads a skill in Skills page.
- Client sends
POST /workspace/:id/skillswith{ name, content }. - Server writes
.opencode/skills/<name>/SKILL.mdafter approval. - Client refreshes skills list.
Add MCP (remote)
- User fills MCP config form.
- Client sends
POST /workspace/:id/mcpwith{ name, config }. - Server merges into
opencode.jsonand returns updated list. - UI shows “Reload engine” banner if required.
OpenWork UI wiring (specific)
These are the concrete integration points inside packages/app.
Data layer
src/app/lib/opencode.ts: keep as-is for OpenCode engine callssrc/app/lib/tauri.ts: host-only FS actions (local) stay here- New
src/app/lib/openwork-server.ts: remote config API client (HTTP) + capability check
Example client surface (TypeScript):
type Capabilities = {
skills: { read: boolean; write: boolean; source: "opencode" | "openwork" };
plugins: { read: boolean; write: boolean };
mcp: { read: boolean; write: boolean };
commands: { read: boolean; write: boolean };
config: { read: boolean; write: boolean };
};
export const openworkServer = {
health(): Promise<{ ok: boolean; version: string; uptimeMs: number }>;
capabilities(): Promise<Capabilities>;
listWorkspaces(): Promise<WorkspaceInfo[]>;
getConfig(id: string): Promise<{ opencode: object; openwork: object }>;
patchConfig(id: string, body: { opencode?: object; openwork?: object }): Promise<void>;
listPlugins(id: string): Promise<string[]>;
addPlugin(id: string, spec: string): Promise<string[]>;
removePlugin(id: string, name: string): Promise<string[]>;
listSkills(id: string): Promise<SkillCard[]>;
upsertSkill(id: string, payload: { name: string; content: string }): Promise<SkillCard>;
listMcp(id: string): Promise<McpServerEntry[]>;
addMcp(id: string, payload: { name: string; config: McpServerConfig }): Promise<McpServerEntry[]>;
removeMcp(id: string, name: string): Promise<McpServerEntry[]>;
listCommands(id: string): Promise<WorkspaceCommand[]>;
upsertCommand(id: string, payload: WorkspaceCommand): Promise<WorkspaceCommand[]>;
deleteCommand(id: string, name: string): Promise<void>;
};
State stores
src/app/context/workspace.ts: route remote config reads/writes to OpenWork server when workspaceType isremotesrc/app/context/extensions.ts: use OpenWork server to list/add skills/plugins/MCPs in remote modesrc/app/context/session.ts: no changes; stays on OpenCode engine
UI surfaces
src/app/pages/dashboard.tsx: display workspace config status + enable “Share config” only when supportedsrc/app/pages/skills.tsx: list and import skills via OpenWork server in remote modesrc/app/pages/plugins.tsx: list/add/remove plugins via OpenWork server in remote modesrc/app/pages/mcp.tsx: list/connect MCPs via OpenWork server in remote modesrc/app/pages/commands.tsx: list/add/remove commands via OpenWork server in remote modesrc/app/pages/session.tsx: surface agent list/selection via OpenWork server when remote if OpenCode lacks agent APIssrc/app/components/workspace-chip.tsx+workspace-picker.tsx: show capability badges (read-only if server missing)
Capability checks
- On connect, call
GET /healthon OpenWork server - Store a
serverCapabilitiesflag in app state and guard remote config actions
Detailed wiring notes
This section explains exactly how requests flow and where the UI switches between local FS and the OpenWork server.
Connection flow (remote)
- User connects to a host (OpenCode engine). The client already has a base URL for OpenCode.
- The client derives or receives the OpenWork server base URL from the host pairing payload.
- The client calls
GET /healthandGET /capabilitieson the OpenWork server. - UI stores
openworkServerStatus(ok/error) andopenworkServerCapabilitiesin app state. - All config-mutating UI surfaces check capabilities before enabling write actions.
Local vs remote switching
- Local workspaces: use Tauri FS helpers (existing
src/app/lib/tauri.ts). - Remote workspaces: route all config reads/writes through
src/app/lib/openwork-server.ts. - The decision happens in the stores, not the UI, so pages don’t need to branch on runtime.
Store-level routing (concrete)
context/extensions.tsrefreshSkills()uses OpenWork server when remote, else lists local skills from FS.refreshPlugins()pulls config from OpenWork server in remote mode, else readsopencode.jsonlocally.refreshMcpServers()reads MCP config from OpenWork server in remote mode, else from FS.
context/workspace.ts- Loads
openwork.jsonandopencode.jsonfrom OpenWork server when remote. - On writes, calls OpenWork server endpoints and refreshes local state on success.
- Loads
context/commands.tslist,create, anddeletecommands via OpenWork server when remote.
Action mapping (UI -> endpoint -> file)
- Add plugin (Plugins page) ->
POST /workspace/:id/plugins->opencode.jsonpluginarray - Remove plugin ->
DELETE /workspace/:id/plugins/:name->opencode.jsonpluginarray - Add MCP server ->
POST /workspace/:id/mcp->opencode.jsonmcpmap - Remove MCP ->
DELETE /workspace/:id/mcp/:name->opencode.jsonmcpmap - Add skill ->
POST /workspace/:id/skills->.opencode/skills/<name>/SKILL.md - Add command ->
POST /workspace/:id/commands->.opencode/commands/<name>.md
UI wiring expectations
- Pages call store methods without caring about local vs remote.
- “Read-only” badges are derived from
openworkServerCapabilities(e.g. missingconfig.write). - “Share config” and any write action is disabled when capabilities are absent.
Permissions and approvals
- Any write request from a remote client triggers a host approval prompt.
- The host UI should show: action type, target workspace, and files to be written.
- If approval is denied or times out, the server returns a clear error and the UI shows a non-blocking toast.
Data contracts (expected formats)
These map to existing OpenWork types.
Plugins
- Source of truth:
opencode.json→pluginfield (string or string[]). - Response shape:
{
"items": [
{ "spec": "opencode-github", "source": "config", "scope": "project" },
{ "spec": "file:///path/to/plugin.js", "source": "dir.project", "scope": "project", "path": ".opencode/plugins/custom.js" }
],
"loadOrder": ["config.global", "config.project", "dir.global", "dir.project"]
}
Skills
- Source of truth:
.opencode/skills/<name>/SKILL.md. - Response shape:
{
"items": [
{ "name": "my-skill", "path": ".opencode/skills/my-skill", "description": "...", "scope": "project" },
{ "name": "global-skill", "path": "~/.config/opencode/skills/global-skill", "description": "...", "scope": "global" }
]
}
MCP
- Source of truth:
opencode.json→mcpobject. - Response shape:
{
"items": [
{
"name": "chrome",
"config": { "type": "local", "command": ["npx", "-y", "chrome-devtools-mcp@latest"], "enabled": true },
"source": "config.project"
}
]
}
Commands
- Source of truth:
.opencode/commands/<name>.mdwith frontmatter. - Response shape:
{
"items": [
{ "name": "daily-report", "description": "...", "template": "...", "agent": "default", "model": null, "subtask": false, "scope": "workspace" }
]
}
Agents
- Prefer OpenCode SDK (
listAgents) as the primary source. - Only add OpenWork server agent endpoints if OpenCode doesn’t expose them for remote clients.
Write approval flow (detailed)
- Client sends a write request (POST/PATCH/DELETE).
- OpenWork server emits a permission request to the host UI with:
- action (write type), workspace id, list of file paths, and summary of changes
- Host approves or denies within a timeout window.
- Server executes the write only after approval and records an audit log entry.
- Client receives success or
403with a reason and shows a toast.
Approval response schema (host -> server):
{ "requestId": "...", "reply": "allow" | "deny" }
Config merge rules (detailed)
opencode.jsonis parsed as JSONC to preserve comments where possible.- Writes are shallow merges at top-level keys; arrays replace existing values.
- Unknown keys are preserved.
- On parse errors, the server returns
422with the error location.
Example: adding a plugin
{ "plugin": ["opencode-github"] }
Server behavior:
- Read current
opencode.json - Normalize plugin list, append new spec if missing
- Write updated JSONC back to disk
Validation rules (initial)
- Plugin spec: non-empty string; if duplicate, no-op.
- Skill name: kebab-case; 1-64 chars; must match folder name; regex
^[a-z0-9]+(-[a-z0-9]+)*$. - Skill description: 1-1024 chars, required in frontmatter.
- MCP name:
^[A-Za-z0-9_-]+$, cannot start with-. - MCP config:
typerequired; forlocalrequirecommand[]; forremoterequireurl. - Commands: name sanitized to
[A-Za-z0-9_-]; template required. - Reject any path traversal (
..) or absolute paths in payloads.
Plugin handling (detailed)
pluginlist inopencode.jsonis treated as the source of npm plugins.- Specs may be unscoped or scoped npm packages (e.g.,
opencode-wakatime,@my-org/custom-plugin). - Specs may also be file URLs or absolute paths when supported by OpenCode.
- Local plugin files are discovered in
.opencode/plugins/and~/.config/opencode/plugins/. - Only JavaScript/TypeScript files are treated as plugins (
.js,.ts). - The OpenWork server should return both config plugins and local plugin files, with a
sourcefield:configfor npm specsdir.projectfor.opencode/plugins/dir.globalfor~/.config/opencode/plugins/
- The UI can display these as separate sections while preserving OpenCode load order.
- The server should not run
bun install; OpenCode handles installs on startup. - If
.opencode/package.jsonis present, note it in responses so the UI can link to dependency setup. - Plugin runtime behavior (events, custom tools, logging) remains owned by OpenCode; OpenWork server only manages config.
Skill handling (detailed)
- Discovery must match OpenCode:
- Walk up from the workspace root to the git worktree root.
- Include any
.opencode/skills/*/SKILL.mdand.claude/skills/*/SKILL.md.
- Include global skills from
~/.config/opencode/skills/*/SKILL.mdand~/.claude/skills/*/SKILL.md. - Validate frontmatter fields:
nameanddescriptionrequiredlicense,compatibility,metadataoptional
- Enforce name and description length rules on write.
- The OpenWork server does not parse or interpret skill content beyond frontmatter extraction.
MCP handling (detailed)
- Read
mcpconfig fromopencode.json. - Preserve
enabled,environment,headers,oauth, andtimeoutfields. - If OpenCode provides remote defaults via
.well-known/opencode, treat those assource: "config.remote". - Writes always go to project
opencode.jsonand should not mutate remote defaults. - If MCP tools are disabled via
toolsglob patterns, surface that asdisabledByTools: truein responses. - OAuth tokens are managed by OpenCode and stored in
~/.local/share/opencode/mcp-auth.json; OpenWork server should not manage tokens directly. - Authentication flows should be triggered via OpenCode (
/mcpendpoints or CLI), not via OpenWork server. - Reference CLI flows:
opencode mcp auth <name>,opencode mcp list,opencode mcp logout <name>,opencode mcp debug <name>.
Commands handling (detailed)
- Commands are markdown files with YAML frontmatter.
- The server should sanitize command names to
[A-Za-z0-9_-]and strip leading/. - The command template is the body after frontmatter and is required.
- Workspace scope lives under
.opencode/commands/in the project. - Global scope lives under
~/.config/opencode/commands/and should be disabled by default for remote clients.
Path safety
- All write targets are resolved under the workspace root.
- The server verifies the resolved path begins with the workspace root.
- Any violation returns
400with a safe error message.
Error codes
400invalid request payload401missing/invalid token403write denied or capability missing404workspace not found409conflict (concurrent edit detected)422config parse/validation error500unexpected server error
Implementation checklist
Server runtime (Bun)
- Create
packages/openwork-serverwith HTTP routing + JSON schema validation - Define stable port + discovery mechanism for clients
- Add lifecycle hooks: start/stop/restart + health checks
Auth + handshake
- Pairing token or session key for remote clients
GET /capabilitiesendpoint to drive UI gating- CORS rules and origin allowlist for web clients
Permissions + audit
- Host approval for any write request
- Audit log for config mutations (who/what/when)
- Clear denial/error propagation to clients
Filesystem writes
- Workspace-root scoping + path validation
- Config merge rules aligned to OpenCode
- Serialization helpers for:
.opencode/skills/<name>/SKILL.md.opencode/commands/<name>.md(frontmatter)opencode.jsonplugin + mcp updates
UI wiring
- Add
src/app/lib/openwork-server.tsclient - Route remote mode reads/writes through OpenWork server:
src/app/context/extensions.tssrc/app/context/workspace.tssrc/app/pages/commands.tsx
- UI gating badges for missing capabilities
Resilience
- Retry/backoff for transient network errors
- Conflict handling for concurrent writes
- Friendly errors for missing workspace roots
Packaging
- Bundle Bun server as a desktop sidecar
- Wire sidecar launch + permissions in Tauri config
Testing strategy
Unit tests (server)
- Config merge rules (arrays replace, unknown keys preserved)
- Validation rules for skill/plugin/mcp/command names
- Path safety checks (reject absolute paths and path traversal)
Filesystem tests (local)
- Create a temp workspace with
.opencode/andopencode.json - Verify
GET /workspace/:id/pluginsreflects:- plugin list from
opencode.json - local plugin files in
.opencode/plugins - global plugin files from
~/.config/opencode/plugins(optional, behind a flag)
- plugin list from
- Verify plugin list preserves OpenCode load order metadata
- Verify
GET /workspace/:id/skillsreturns:.opencode/skills/*/SKILL.md.claude/skills/*/SKILL.md- global skills from
~/.config/opencode/skills(optional)
- Verify skill discovery respects git worktree boundary (walk up to
.gitonly) - Verify skill frontmatter parsing and name/description constraints
- Verify
.opencode/package.jsonis detected and reported when present
Integration tests (server + FS)
- Start server against a temp workspace; verify read/write endpoints
- Ensure writes only affect
.opencodeandopencode.json - Verify audit log entries for each write action
- Validate local plugin discovery only includes
.jsand.ts - Validate MCP config writes preserve
enabledandoauthfields
Approval flow tests
- Write request triggers approval prompt
- Deny returns
403and no file is written - Approve writes file and returns success
Client wiring tests (OpenWork web)
- Remote mode uses OpenWork server endpoints instead of Tauri FS
- Missing capability switches UI to read-only
- Reconnect restores write actions after capabilities return
- OpenCode basic auth prompts propagate to client when enabled
Sidecar lifecycle tests
- Server starts on app launch and reports base URL
- Crash triggers restart and new capabilities handshake
Test cases (initial)
Config
GET /workspace/:id/configreturns bothopencodeandopenworkblocksPATCH /workspace/:id/configupdates plugin list and preserves unknown keys- Invalid JSONC returns
422with parse location - Remote defaults from
.well-known/opencodeappear assource: config.remotein responses - Writes only update project
opencode.json, leaving remote defaults unchanged
Plugins
- Add plugin appends to list and de-dupes existing spec
- Remove plugin deletes only matching spec
- Invalid spec returns
400 - Local plugin files are returned with
source: dir.project - Global plugin files are returned with
source: dir.globalwhen enabled - Non-js/ts files in plugin dirs are ignored
Skills
- Add skill writes
SKILL.mdwith kebab-case name validation - List skills returns
name,path, anddescription - Invalid skill name returns
400 - Missing frontmatter fields return
422 - Skill name mismatch with folder returns
400
MCP
- Add MCP writes to
opencode.jsonundermcpmap - Invalid MCP name returns
400 - Remove MCP deletes entry and returns updated list
enabled: falseis preserved in responsesoauth: falseis preserved in responses- Missing
commandfortype: localreturns400 - Missing
urlfortype: remotereturns400 toolsglob disables MCP entries and marks them asdisabledByTools: true
Commands
- Create command writes
.opencode/commands/<name>.md - Delete command removes file and returns success
- Invalid template returns
400 - Name with leading
/is sanitized
Permissions
- Denied approval returns
403and no file changes - Approval timeout returns
403with timeout reason
Security
- Path traversal payload returns
400and is logged - Workspace id mismatch returns
404
UI
- Remote client with OpenWork server: write actions enabled
- Remote client without OpenWork server: write actions disabled + read-only badge
Set permissions
- Explicit approval for any config write originating from a remote client
- Scope-limited to the active workspace root only
Cover data
- Audit log of config changes (who, what, when)
- Optional telemetry for success/failure counts, opt-in only
Map flow
- Connect: remote client connects to host with capability check
- View: UI shows skills/plugins/MCPs from server API
- Update: user adds skill/plugin/MCP, server validates + writes config, UI refreshes
Note risks
- Over-expanding API surface could drift from OpenCode primitives
- Mis-scoped writes could affect unrelated projects
Ask questions
- Which config surfaces should be writable vs read-only initially?
- Should writes be batched or immediate per action?
Measure success
- Remote users can view skills/plugins/MCPs without filesystem access
- Remote users can add a skill/plugin/MCP with a single approval
Plan rollout
- Phase 0: Bun server prototype running alongside OpenWork host
- Phase 1: read-only APIs for skills/plugins/MCPs + config metadata
- Phase 2: write APIs for skills/plugins/MCPs with audit log
- Phase 3: config export/import support for remote clients
- Phase 4: bundle as a first-class sidecar in desktop builds