Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7505be6
Add Docker Desktop runtime setup
lahuman Aug 22, 2026
b4e5dfb
Persist container SSH credentials
lahuman Aug 24, 2026
f9d4f55
Document MCP listen host setting
lahuman Aug 24, 2026
d9aea75
Harden authentication and OAuth state handling
lahuman Aug 25, 2026
2cd63cc
Add Docker Compose operations guides
lahuman Aug 25, 2026
0216267
feat: add process lifecycle diagnostics
lahuman Aug 31, 2026
734f059
feat: add development task journals
lahuman Aug 31, 2026
9093308
feat: add task event tracing
lahuman Aug 31, 2026
f21ae99
feat: add task timeline dashboard
lahuman Aug 31, 2026
2c6a824
feat: add optional safety approval policy
lahuman Aug 31, 2026
cbb20a0
feat: support configurable safety policies
lahuman Aug 31, 2026
5a13385
feat: manage safety policy from dashboard
lahuman Aug 31, 2026
774a15c
feat: add safety policy audit rollback
lahuman Aug 31, 2026
b00bc3e
feat: verify policy audit chain and show diffs
lahuman Aug 31, 2026
d5017cc
fix: bind safety approvals to exact operations
lahuman Aug 31, 2026
a1f8195
fix: validate all apply patch target paths
lahuman Aug 31, 2026
da1c241
fix: invalidate approvals and unify process journal
lahuman Aug 31, 2026
06bcdce
fix: tighten safety policy validation
lahuman Aug 31, 2026
3dbeeae
fix: serialize safety policy audit writes
lahuman Aug 31, 2026
78dde72
fix: redact common secrets from command metadata
lahuman Aug 31, 2026
f331fb6
test: cover approval denial and policy invalidation
lahuman Aug 31, 2026
7ba4194
fix: report safety mode accurately
lahuman Aug 31, 2026
fe8fa92
feat: add dashboard account login
lahuman Aug 31, 2026
ead19ef
feat: show automatic command sessions in dashboard
lahuman Aug 31, 2026
1264792
fix: use dashboard api absolute paths
lahuman Aug 31, 2026
926e9e9
fix: preserve dashboard inline script syntax
lahuman Aug 31, 2026
fdfa371
feat: simplify dashboard task results
lahuman Aug 31, 2026
3da2cd7
fix: distinguish dashboard task outcomes
lahuman Aug 31, 2026
fe4c6d4
Improve MCP instructions and fix dependency vulnerabilities
lahuman Sep 3, 2026
65077d2
refactor: focus MCP server instructions
lahuman Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Dependencies and generated output
node_modules/
dist/
coverage/
.vitest/
*.tsbuildinfo

# Source-control and local configuration
.git/
.env
.env.*
!.env.example
workspace/
data/
.ssh/
npm-debug.log
*.log

# Local/editor files
.DS_Store
Thumbs.db
.idea/
.vscode/
39 changes: 36 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
# Docker Compose mount source. Keep this limited to a dedicated project directory.
WORKSPACE_PATH=./workspace

# Container-only SSH directory stored beside this project on the same disk.
SSH_PATH=./.ssh

# Network
MCP_HOST=0.0.0.0
MCP_LISTEN_HOST=127.0.0.1
MCP_PORT=3000
MCP_ENDPOINT=/mcp
MCP_PUBLIC_URL=https://mcp.example.com
MCP_PUBLIC_URL=
MCP_ALLOWED_HOSTS=
MCP_TRUST_PROXY_HOPS=0

# Static bearer authentication. OAuth can instead use MCP_OAUTH_APPROVAL_KEY.
# Secrets must be 32+ characters; literal replace-with-* placeholders are rejected.
MCP_AUTH_TOKEN=replace-with-a-long-random-token
MCP_ALLOW_NO_AUTH=false

# Host execution. No sandbox, approval, command allowlist, or path restriction is applied.
MCP_DEFAULT_CWD=/root
# Optional browser login for /dashboard. Change the example password before enabling.
MCP_DASHBOARD_USERNAME=admin
MCP_DASHBOARD_PASSWORD=replace-with-a-strong-dashboard-password
# Optional when MCP_AUTH_TOKEN or MCP_OAUTH_APPROVAL_KEY is already 32+ chars.
MCP_DASHBOARD_SESSION_SECRET=

# Container execution. No sandbox, approval, command allowlist, or path restriction is applied.
MCP_DEFAULT_CWD=/workspace
MCP_DEFAULT_SHELL=/bin/bash

# Operational limits for transport stability, not permission restrictions.
MCP_MAX_REQUEST_BODY=8mb
MCP_MAX_OUTPUT_BYTES=1048576
MCP_MAX_RETAINED_PROCESS_OUTPUT_BYTES=4194304
MCP_PROCESS_RETENTION_MS=3600000
MCP_PROCESS_IDLE_TIMEOUT_MS=1800000
MCP_PROCESS_MAX_RUNTIME_MS=14400000
MCP_TASK_JOURNAL_FILE=/data/task-journal.jsonl
MCP_MAX_PROCESSES=128
MCP_MAX_FILE_CHUNK_BYTES=1048576
MCP_MAX_EDIT_FILE_BYTES=67108864

# Optional built-in OAuth 2.1 server. These values are required when enabled.
MCP_OAUTH_ENABLED=false
MCP_OAUTH_APPROVAL_KEY=
MCP_OAUTH_ISSUER=
MCP_OAUTH_RESOURCE=
MCP_OAUTH_STATE_FILE=/data/oauth-state.json
MCP_OAUTH_ACCESS_TOKEN_TTL_SECONDS=3600
MCP_OAUTH_REFRESH_TOKEN_TTL_SECONDS=2592000
MCP_OAUTH_AUTHORIZATION_CODE_TTL_SECONDS=300

# unrestricted (default) or safe (human approval for risky operations)
MCP_SAFETY_MODE=unrestricted
# Optional JSON policy file. Docker Compose example: /config/safety-policy.json
# MCP_SAFETY_POLICY_FILE=/config/safety-policy.json
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ coverage/
.tmp/
tmp/
temp/
/data/

# Docker bind-mount workspace: retain the directory, ignore its contents.
/workspace/*
!/workspace/.gitkeep

# Credentials and private keys
*.pem
Expand Down
110 changes: 110 additions & 0 deletions DOCKER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Running with Docker Desktop on macOS

This setup runs `cokacremote` inside Docker Desktop. The MCP server can access
only the directory mounted at `/workspace`; it does not receive the Docker
socket, your SSH keys, or your macOS home directory.

## Prerequisites

- macOS
- Docker Desktop
- Git

## Setup

Create your local configuration and set a strong authentication token:

```bash
cp .env.example .env
openssl rand -hex 32
```

Put the generated value in `.env` as `MCP_AUTH_TOKEN=<generated-value>`. Do
not commit `.env`.

The service executes commands as the non-root `node` user inside the
container. The image already includes Git, curl, wget, OpenSSH client,
Python 3, build tools, jq, and ripgrep for work performed in the mounted
workspace.

## Build

```bash
docker compose build
```

## Start

```bash
docker compose up -d
```

## Status

```bash
docker compose ps
```

## Logs

```bash
docker compose logs -f cokacremote
```

## Stop

```bash
docker compose down
```

The named `cokacremote-data` volume is intentionally retained by this command
so OAuth state survives a container replacement. Remove it only when you
intend to invalidate that state:

```bash
docker compose down -v
```

## Rebuild

```bash
docker compose build --no-cache
docker compose up -d
```

## Health check

The HTTP port is bound to localhost only. With the default settings:

```bash
curl -f http://127.0.0.1:3000/health
```

The MCP endpoint is `http://127.0.0.1:3000/mcp`. It requires the Bearer token
configured in `MCP_AUTH_TOKEN` unless built-in OAuth is enabled or the server
is deliberately configured for authenticated upstream access.

## Workspace

By default, the host directory `./workspace` is mounted at `/workspace` in the
container and is the MCP server's default working directory. Only
`workspace/.gitkeep` is tracked by this repository; files created during work
are ignored.

To use an existing macOS project directory, set a specific path in `.env`:

```dotenv
WORKSPACE_PATH=/Users/myname/Projects/ai-workspace
```

Do not mount your entire home directory, `/`, Docker Desktop's socket, system
directories, or SSH key directories. The container is intentionally not
privileged and publishes its HTTP port only on `127.0.0.1`.

## OAuth state

When `MCP_OAUTH_ENABLED=true`, set `MCP_PUBLIC_URL`,
`MCP_OAUTH_APPROVAL_KEY`, `MCP_OAUTH_ISSUER`, and `MCP_OAUTH_RESOURCE` to the
public HTTPS values for the future reverse-proxy or tunnel deployment. OAuth
state defaults to `/data/oauth-state.json`, which is backed by the named
Docker volume rather than the bind-mounted workspace.
46 changes: 46 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
FROM node:22-bookworm-slim AS build

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY tsconfig.json ./
COPY src ./src
RUN npm run build && npm prune --omit=dev

FROM node:22-bookworm-slim

RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
bash \
build-essential \
ca-certificates \
curl \
git \
jq \
openssh-client \
python3 \
python3-pip \
ripgrep \
wget \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=build --chown=node:node /app/package.json /app/package-lock.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist

RUN mkdir /workspace /data \
&& chown node:node /workspace /data

ENV NODE_ENV=production \
MCP_DEFAULT_CWD=/workspace \
MCP_OAUTH_STATE_FILE=/data/oauth-state.json

USER node

EXPOSE 3000

CMD ["npm", "start"]
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ The MCP transport is stateless, but long-running command sessions are kept in me
- Read, write, edit, transfer, and delete host files, including absolute paths
- Built-in static Bearer authentication and OAuth 2.1/DCR/PKCE for ChatGPT
- Stateless JSON transport per request, per-process output retention, and response size limits
- Built-in `npm run doctor` diagnostics for runtime/tooling/workspace readiness
- Optional persistent JSONL process journal plus idle/max-runtime process lifecycle controls
- systemd and Nginx deployment examples for Linux VPS/EC2 environments

## Available tools
Expand Down Expand Up @@ -139,14 +141,84 @@ The server provides 20 tools in total. `remove_path` permanently deletes targets
- Python 3 if Python execution through `run_script` is needed
- A stable, publicly accessible HTTPS domain when connecting directly from ChatGPT

## Safety policy

`MCP_SAFETY_MODE=unrestricted` preserves the original full-access behavior. Set `MCP_SAFETY_MODE=safe` to require a one-time human approval for risky shell commands and writes outside `MCP_DEFAULT_CWD`. Extremely destructive host commands such as filesystem formatting, reboot/shutdown, raw device overwrite, and root recursive deletion are denied. Pending approvals appear in `/dashboard`, expire after 10 minutes, and are consumed once when the tool retries with the returned `approvalId`.

Safe mode can be customized with `MCP_SAFETY_POLICY_FILE`. The file is JSON (`version: 1`) and supports ordered `commands` regex rules, ordered `paths` prefix rules, and default decisions. Rule decisions are `allow`, `approval-required`, or `deny`. User rules run before the built-in approval rules, so an environment can explicitly allow something such as `docker rm`; catastrophic built-in deny rules always run first and cannot be overridden. Path rules support `${workspace}` as an alias for `MCP_DEFAULT_CWD` and can optionally be limited to named MCP tools.

Docker Compose mounts `${CONFIG_PATH:-./config}` at `/config` so the authenticated dashboard can save policy changes. Copy `config/safety-policy.example.json` to `config/safety-policy.json`, set `MCP_SAFETY_MODE=safe` and `MCP_SAFETY_POLICY_FILE=/config/safety-policy.json`, then restart the service once to load the configured file path. After that, `/dashboard` can validate, save, and reload the policy without restarting the server. Each successful save/reload is appended to `<policy-file>.history.jsonl` with a timestamp, revision ID, SHA-256 digest, and validated policy snapshot. The dashboard shows this history and can atomically roll back to any retained revision; rollback itself creates a new audit revision. New revisions are linked with SHA-256 entry hashes so the dashboard can verify the audit chain and flag tampering; pre-chain history remains readable as legacy/unverified. Each revision also has a line diff against the currently active policy. `npm run doctor` validates the configured policy file and fails if the JSON, regex, version, or decision values are invalid.

Example policy:

```json
{
"version": 1,
"commands": [
{ "id": "allow-docker-rm", "pattern": "(?:^|[;&|]\\s*)docker\\s+rm\\b", "decision": "allow" },
{ "id": "approve-sudo", "pattern": "(?:^|[;&|]\\s*)sudo\\b", "decision": "approval-required" }
],
"paths": [
{ "id": "deny-etc", "prefix": "/etc", "decision": "deny" },
{ "id": "allow-generated", "prefix": "${workspace}/generated", "decision": "allow" }
],
"defaults": { "unmatchedCommand": "allow", "outsideWorkspace": "approval-required" }
}
```

## Task dashboard

Open `/dashboard` on the same server to inspect recent development tasks, summaries, and their ordered event timelines. For normal browser access, set `MCP_DASHBOARD_USERNAME` and `MCP_DASHBOARD_PASSWORD` in `.env`; unauthenticated browser visits are redirected to `/dashboard/login`, and a successful login creates a 12-hour HttpOnly, SameSite=Strict session cookie (`Secure` when `MCP_PUBLIC_URL` uses HTTPS). The existing MCP bearer/OAuth authentication remains accepted for dashboard routes and APIs. Login failures are rate-limited per client address. `MCP_DASHBOARD_SESSION_SECRET` is optional when an existing 32+ character MCP auth secret can sign sessions. The dashboard refreshes every three seconds, and its JSON endpoints under `/dashboard/api/tasks` can also be used by external monitoring UIs. Commands run without an explicit `start_task` are automatically grouped into dashboard sessions; a 15-minute inactivity gap starts a new automatic session, so ordinary RMCP command activity remains visible without extra task-management calls.

## Reliability controls

Three operational controls are available for long-running development sessions:

- `MCP_PROCESS_IDLE_TIMEOUT_MS`: terminates a process after no stdout/stderr activity for the configured interval. Default: `1800000` (30 minutes). Set `0` to disable.
- `MCP_PROCESS_MAX_RUNTIME_MS`: fallback hard runtime limit when a tool call does not provide its own `timeoutMs`. Default: `14400000` (4 hours). Set `0` to disable.
- `MCP_TASK_JOURNAL_FILE`: optional JSONL journal containing process start/completion/idle-timeout events. Docker Compose defaults this to `/data/task-journal.jsonl`, which is stored in the persistent `cokacremote-data` volume.

Run a host readiness check with:

```bash
npm run doctor
```

The doctor checks Node.js, Git, the configured shell, npm, Python, workspace read/write access, and the configured task-journal location.

### Development task journal

When `MCP_TASK_JOURNAL_FILE` is configured, related MCP calls can be grouped into a durable development task:

1. Call `start_task` and keep the returned `taskId`.
2. Pass `taskId` to `exec_command`, `run_script`, `write_file`, `replace_in_file`, `apply_patch`, or `upload_file`.
3. Use `get_task` or `list_tasks` to review commands and changed files.
4. Call `complete_task` when the work is finished.

Calls without `taskId` remain fully backward compatible. The journal stores command metadata and changed paths, not stdout/stderr or file contents. Common credential forms in command metadata (token/password/secret/API-key assignments, flags, and Authorization headers) are redacted before they enter process state or the journal.

## Local development

The Quick Start above is enough to run a normal local instance. If you are changing the source code itself, development mode automatically watches the TypeScript entry point:

```bash
MCP_AUTH_TOKEN=development-token npm run dev
MCP_AUTH_TOKEN="$(openssl rand -hex 32)" npm run dev
```

## Docker Compose

For complete installation, security, persistent SSH, OAuth, update, and troubleshooting instructions, see the [Docker Compose Installation and Operations Guide](docs/docker-compose-guide.md). A [Korean version](docs/docker-compose-guide.ko.md) is also available. For a shorter macOS Docker Desktop walkthrough, see [DOCKER.md](DOCKER.md).

The Compose service bind-mounts the project-local `./.ssh` directory at `/home/node/.ssh`. SSH keys, config, and `known_hosts` created inside the container therefore survive container restarts and recreation without exposing the host user's SSH directory. Set `SSH_PATH` in `.env` to use another directory on the same disk.

Then build or recreate the service normally:

```bash
docker compose up -d --build
```

Create the host directory with mode `0700` before starting the service. Because this is a bind mount, `docker compose down --volumes` does not remove its SSH credentials. Back up or delete the configured `SSH_PATH` separately when needed.

## Authentication

When `MCP_AUTH_TOKEN` is set, every MCP request requires the following header:
Expand Down Expand Up @@ -177,6 +249,8 @@ When enabled, the server provides:

OAuth uses a single `mcp:tools` scope. Enter the `MCP_OAUTH_APPROVAL_KEY` value on the approval page shown when authorizing a ChatGPT connection. For OAuth-only deployments, it is recommended to leave `MCP_AUTH_TOKEN` empty so there is no permanent static Bearer bypass path. For backward compatibility, `MCP_AUTH_TOKEN` is used as the approval key when no dedicated approval key is configured, but keeping the two values separate is safer. Treat both values like root credentials. Registered clients, client secrets, and token hashes are stored in `MCP_OAUTH_STATE_FILE` with mode `600`.

Both authentication secrets must contain at least 32 characters, and known example placeholder values are rejected at startup. Generate independent values with `openssl rand -hex 32`; do not copy the literal placeholder values from the example environment files.

OAuth-related HTTP routes:

| Path | Purpose |
Expand Down Expand Up @@ -409,3 +483,6 @@ This includes, but is not limited to:
The user assumes full responsibility for all consequences arising from the use of this software, whether such use was intended, authorized, or foreseeable.

**ALL RISKS ASSOCIATED WITH USE ARE BORNE BY THE USER**


Safety approvals are one-time, expire after 10 minutes, and are SHA-256 bound to the exact requested operation so they cannot be reused for modified commands or file operations. `apply_patch` additionally parses Git/unified-diff headers and evaluates every create/delete/rename/copy target path before applying a patch; patches whose targets cannot be determined are rejected.
33 changes: 33 additions & 0 deletions config/safety-policy.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"version": 1,
"commands": [
{
"id": "allow-docker-rm",
"pattern": "(?:^|[;&|]\\s*)docker\\s+rm\\b",
"decision": "allow",
"reason": "Container removal is allowed in this development environment"
},
{
"id": "approve-sudo",
"pattern": "(?:^|[;&|]\\s*)sudo\\b",
"decision": "approval-required"
}
],
"paths": [
{
"id": "deny-etc",
"prefix": "/etc",
"decision": "deny",
"reason": "System configuration is managed outside cokacremote"
},
{
"id": "allow-workspace-generated",
"prefix": "${workspace}/generated",
"decision": "allow"
}
],
"defaults": {
"unmatchedCommand": "allow",
"outsideWorkspace": "approval-required"
}
}
Loading