Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
name: Quality checks
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 11.23.0
run_install: false

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check formatting
run: pnpm format:check

- name: Lint
run: pnpm lint

- name: Typecheck
run: pnpm typecheck

- name: Test
run: pnpm test

- name: Build
run: pnpm build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dist/
coverage/
node_modules/
.env
.env.*
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Every tool call that reaches a running sandbox counts as activity, whether it su

Cleanup checks run once per minute. Sandbox resources use Docker Sandboxes defaults. The outer MCP server accepts request bodies up to 20 MiB.

Bash has no execution timeout unless `timeout_ms` is explicitly provided. `bash` always returns a `session_id` and waits up to `yield_time_ms`, which defaults to 10 seconds and accepts at most 60 seconds. `bash_poll` waits for new output, process exit, or its own `yield_time_ms` expiry; that wait also defaults to 10 seconds and accepts at most 60 seconds. It returns only new combined stdout/stderr. Poll again while `status` is `running` or `has_more_output` is true. `bash_stop` sends SIGTERM followed by SIGKILL after 1.5 seconds if necessary. chat2shell does not redact Bash output: everything printed inside the sandbox is visible to the MCP client. Sensitive data must be controlled by the files and credentials explicitly made available to the sandbox. Bash sessions exist only in their sandbox and disappear when that sandbox is removed. They are not recovered after a chat2shell restart, because restart reconciliation removes the old sandbox.
Bash has no execution timeout unless `timeout_ms` is explicitly provided. `bash` always returns a `session_id` and waits up to `yield_time_ms`, which defaults to 10 seconds and accepts at most 60 seconds. If command launch succeeds but the initial status/output snapshot cannot be read, `bash` preserves the session and conservatively returns `status: running` with no output so the caller can recover with `bash_poll`. `bash_poll` waits for new output, process exit, or its own `yield_time_ms` expiry; that wait also defaults to 10 seconds and accepts at most 60 seconds. It returns only new combined stdout/stderr. Poll again while `status` is `running` or `has_more_output` is true. `bash_stop` sends SIGTERM followed by SIGKILL after 1.5 seconds if necessary. chat2shell does not redact Bash output: everything printed inside the sandbox is visible to the MCP client. Sensitive data must be controlled by the files and credentials explicitly made available to the sandbox. Bash sessions exist only in their sandbox and disappear when that sandbox is removed. They are not recovered after a chat2shell restart, because restart reconciliation removes the old sandbox.

If CodexPro becomes unavailable, the sandbox changes to `failed`. `sandbox_list` shows it, and the user must destroy it before creating a replacement. chat2shell does not guess how to recover it.

Expand All @@ -90,15 +90,17 @@ Destroying an active sandbox follows the same workspace policy, so a managed wor

## Setup

Requirements are Node.js 22 or newer, pnpm, Docker Sandboxes (`sbx`), and the previously installed Secure MCP Tunnel client.
Requirements are Node.js 24 or newer, pnpm, Docker Sandboxes (`sbx`), and the previously installed Secure MCP Tunnel client.

```bash
pnpm install
./scripts/setup-template.sh
pnpm check
pnpm test:integration
pnpm test:e2e
```

`pnpm check` is the normal development and CI quality gate: formatting, linting, typechecking, unit and integration tests, and the production build. It deliberately excludes real Docker Sandbox E2E tests. Run `pnpm test:e2e` on a trusted host with `sbx` and the local CodexPro template installed. See [`test/README.md`](./test/README.md) for the test boundaries and individual commands.

`setup-template.sh` creates the local `chat2shell-codexpro:0.30.0` sandbox template once.
The template contains CodexPro and its npm dependencies, but no workspace, application source, credentials, or tunnel secret.

Expand Down Expand Up @@ -148,13 +150,13 @@ Unexported changes in a private clone disappear when its sandbox is destroyed, s
## MCP workflow

```json
{"workspace_mode":"managed"}
{ "workspace_mode": "managed" }
```

Pass the returned sandbox ID to every CodexPro tool:

```json
{"sandbox_id":"sbx_...","command":"pnpm test"}
{ "sandbox_id": "sbx_...", "command": "pnpm test" }
```

Long commands use the same `bash` tool. A running result includes a session ID for later output or termination:
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ CodexPro assigns its own path-derived workspace ID inside the microVM. That inte

CodexPro remains the only Bash executor. chat2shell starts each command through CodexPro as a detached process group inside the selected microVM, with combined stdout/stderr written to that microVM's `/tmp` directory.

`bash` always returns a random `session_id` and waits for completion for 10 seconds by default. `yield_time_ms` can explicitly change that wait from 0 to 60 seconds. If the command exits, the call returns `status: exited`, its output, and its exit code; otherwise it returns the output so far and `status: running`. This wait controls only when MCP yields a response and never kills the command.
`bash` always returns a random `session_id` and waits for completion for 10 seconds by default. `yield_time_ms` can explicitly change that wait from 0 to 60 seconds. If the command exits, the call returns `status: exited`, its output, and its exit code; otherwise it returns the output so far and `status: running`. Once the detached launch has succeeded, chat2shell keeps the session handle even if the first status/output snapshot fails. In that case `bash` conservatively returns `status: running`, empty output, and the same `session_id`; a later `bash_poll` recovers the actual state and unread output. This wait controls only when MCP yields a response and never kills the command.

There is no command lifetime limit unless `timeout_ms` is explicitly supplied. `bash_poll` waits until new output appears, the process exits, or its `yield_time_ms` expires. Its wait defaults to 10 seconds and accepts at most 60 seconds. It returns only output not returned by earlier calls and reports the current status and exit code. Polls for one session are serialized. Each response reads at most 60,000 new bytes, preserves complete UTF-8 characters across reads, and reports `has_more_output` when already-buffered output remains. Call it again while `status` is `running` or `has_more_output` is true. `bash_stop` terminates the process group with SIGTERM and escalates to SIGKILL after 1.5 seconds.

Expand Down
9 changes: 9 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import retn0 from '@retn0/eslint-config';
import eslintConfigOxlint from '@retn0/eslint-config-oxlint';

export default retn0(
{
environments: ['node'],
},
eslintConfigOxlint,
);
7 changes: 7 additions & 0 deletions oxfmt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import config from '@retn0/oxfmt-config';
import { defineConfig } from 'oxfmt';

export default defineConfig({
...config,
ignorePatterns: [...(config.ignorePatterns ?? []), 'src/codexpro/standard-tools.json'],
});
3 changes: 3 additions & 0 deletions oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import retn0 from '@retn0/oxlint-config';

export default retn0();
37 changes: 27 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,43 @@
"private": true,
"description": "A private MCP control plane for isolated shell and sandbox access from ChatGPT.",
"type": "module",
"packageManager": "pnpm@11.23.0",
"engines": {
"node": ">=22"
},
"scripts": {
"build": "pnpm clean && tsc -p tsconfig.json",
"build": "pnpm clean && tsc -p tsconfig.build.json",
"clean": "node scripts/clean-dist.mjs",
"cli": "node dist/src/cli/main.js",
"check": "pnpm typecheck && pnpm test",
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test && pnpm build",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint . && eslint .",
"start": "node dist/src/mcp/main.js",
"test": "pnpm build && node --test dist/test/**/*.test.js",
"test:integration": "pnpm build && CHAT2SHELL_RUN_SBX_INTEGRATION=1 node --test dist/test/sbx-integration.test.js",
"test": "vitest run --project unit --project integration",
"test:coverage": "vitest run --coverage --project unit --project integration",
"test:e2e": "vitest run --project e2e",
"test:integration": "vitest run --project integration",
"test:unit": "vitest run --project unit",
"test:watch": "vitest --project unit --project integration",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"zod": "3.25.76"
},
"devDependencies": {
"@retn0/eslint-config": "2.1.0",
"@retn0/eslint-config-oxlint": "0.1.2",
"@retn0/oxfmt-config": "0.2.0",
"@retn0/oxlint-config": "2.2.0",
"@types/node": "^24.0.0",
"typescript": "^5.9.0"
}
"@vitest/coverage-v8": "4.1.11",
"eslint": "10.7.0",
"oxfmt": "0.59.0",
"oxlint": "1.73.0",
"oxlint-tsgolint": "0.25.0",
"typescript": "^5.9.0",
"vitest": "4.1.11"
},
"engines": {
"node": ">=24.0.0"
},
"packageManager": "pnpm@11.23.0"
}
Loading