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
18 changes: 9 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Optional shell environment overrides. Source this file before running chat2shell.
CHAT2SHELL_HOST=127.0.0.1
CHAT2SHELL_PORT=18788
CHAT2SHELL_DATA_ROOT="${HOME}/.chat2shell"
CHAT2SHELL_STATE_DIR="${HOME}/.chat2shell/state"
CHAT2SHELL_WORKSPACE_ROOT="${HOME}/.chat2shell/workspaces"
CHAT2SHELL_ALLOWED_HOST_ROOTS="${HOME}/repositories"
CHAT2SHELL_SECRET_DIR="${HOME}/.secrets/tunnel-client"
CHAT2SHELL_TUNNEL_CLIENT="${HOME}/.local/bin/tunnel-client"
CHAT2SHELL_ENABLE_TUNNEL=1
export CHAT2SHELL_HOST=127.0.0.1
export CHAT2SHELL_PORT=18788
export CHAT2SHELL_DATA_ROOT="${HOME}/.chat2shell"
export CHAT2SHELL_STATE_DIR="${HOME}/.chat2shell/state"
export CHAT2SHELL_WORKSPACE_ROOT="${HOME}/.chat2shell/workspaces"
export CHAT2SHELL_ALLOWED_HOST_ROOTS="${HOME}/repositories"
export CHAT2SHELL_SECRET_DIR="${HOME}/.secrets/tunnel-client"
export CHAT2SHELL_TUNNEL_CLIENT="${HOME}/.local/bin/tunnel-client"
export CHAT2SHELL_ENABLE_TUNNEL=1
48 changes: 48 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Release

on:
release:
types:
- published

permissions:
contents: read
id-token: write

jobs:
publish:
name: Publish to npm
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Check out repository
uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [MEDIUM] Release actions are no longer pinned to immutable revisions

The release job has id-token: write and ultimately publishes to npm. Replacing the exact commit pins for checkout, pnpm setup, and Node setup with mutable major-version tags allows code introduced by a moved or compromised tag to execute before publication. Such code can alter the checked-out or built package, which the final trusted npm publish step would then release under this project's identity.

Evidence: Offline zizmor 1.30.0 reported high-confidence unpinned-uses findings at lines 21, 24, and 30. Auditing the review-base workflow reported no unpinned-uses findings, and the unchanged CI workflow still pins these same actions to full commit SHAs.

Suggested action: Restore full commit-SHA pins for all three actions, retaining version comments for readability, and update the pins through reviewed dependency-update PRs.

Confidence: high

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this suggestion. This repository intentionally uses upstream major-version tags for GitHub Actions rather than immutable commit SHAs. The associated supply-chain tradeoff is accepted as a project maintenance policy, so these references are deliberate rather than accidental unpinning.


- name: Set up pnpm
uses: pnpm/action-setup@v6
with:
version: 11.23.0
run_install: false

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
registry-url: https://registry.npmjs.org

- name: Verify release version
run: |
package_version="$(node -p "require('./package.json').version")"
test "v${package_version}" = "${GITHUB_REF_NAME}"

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

- name: Check package
run: pnpm check

- name: Publish package
run: npm publish --access public --provenance
77 changes: 49 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The static contract deliberately excludes CodexPro's generic supertool, self-tes

## Current policy

This section is the complete product policy. A behavior that contradicts it is a bug; a new behavior must be added here when it is introduced.
This section describes the user-visible authority, workspace, network, credential, and lifecycle guarantees enforced by chat2shell.

### Authority

Expand All @@ -32,7 +32,7 @@ This section is the complete product policy. A behavior that contradicts it is a
- A sandbox receives full shell and sudo-equivalent freedom only inside its microVM, including its own Docker Engine.
- Bash is unrestricted inside the sandbox. Commands can modify sandbox files, install packages, access the network, and control the sandbox's private Docker Engine.
- chat2shell does not ask for local approval for ordinary sandbox work. Creating a new host-backed workspace is the only local approval boundary.
- The server has no OAuth and treats every request as `local-owner`. The tunnel and ChatGPT app must remain private to the owner.
- The server has no authentication and treats every request as `local-owner`. Secure MCP Tunnel is the recommended transport. Any other exposure must provide its own authentication and access control; never expose the MCP endpoint directly to an untrusted network.

### Workspaces

Expand All @@ -47,15 +47,15 @@ This section is the complete product policy. A behavior that contradicts it is a

### Network and credentials

- Outbound network access follows the Docker Sandboxes policy installed on this machine. The current prototype intentionally permits general network access.
- General outbound network access follows Docker Sandboxes behavior.
- chat2shell does not add, remove, or override Docker Sandboxes network policy.
- `sandbox_expose` publishes one sandbox TCP port on an automatically assigned port on every host IPv4 interface. It is never called automatically, adds no authentication or expiration, and relies on the sandboxed service listening on `0.0.0.0`.
- Repeating `sandbox_expose` for the same sandbox port returns the existing mapping. The mapping disappears when the sandbox is removed.
- Traffic through an exposed port does not count as a tool call and does not renew the sandbox inactivity deadline.
- chat2shell denies `openrouter.ai` for its sandboxes so Docker's unrelated global `opencodex` credential cannot be used by ChatGPT.
- Docker's built-in MCP gateway may exist inside a shell sandbox, but chat2shell and CodexPro do not connect to it.
- CodexPro endpoints use random bearer tokens and dynamically allocated loopback ports.
- The internal bearer token is stored in the owner-only SQLite state file and is never returned through MCP.
- Tunnel credentials remain outside this repository and are never read by the TypeScript application.
- Tunnel credentials remain outside the npm package. chat2shell reads the tunnel ID and gives tunnel-client the key file path without exposing either value through MCP.

### Lifecycle and failure

Expand Down Expand Up @@ -88,58 +88,77 @@ Restarting chat2shell invalidates existing sandboxes because their foreground se

Destroying an active sandbox follows the same workspace policy, so a managed workspace can be attached to a new sandbox with the same `workspace_id` during its 30-day retention period.

## Setup
## Install and run

Requirements are Node.js 24 or newer, pnpm, Docker Sandboxes (`sbx`), and the previously installed Secure MCP Tunnel client.
Requirements are Node.js 24 or newer and [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/) (`sbx`). Tunnel mode additionally requires Secure MCP Tunnel access, its client, a tunnel ID, and a key file.

Install the CLI and prepare its pinned CodexPro sandbox template:

```bash
pnpm install
./scripts/setup-template.sh
pnpm check
pnpm test:e2e
npm install --global chat2shell
chat2shell setup
```

`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` checks the required local tunnel files and Docker Sandboxes installation. It creates `chat2shell-codexpro:0.30.0` only when that template does not already exist. The template contains CodexPro and its npm dependencies, but no workspace, application source, credentials, or tunnel secret.

`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.
Run chat2shell in the foreground:

```bash
chat2shell serve
```

The `serve` process owns both the loopback MCP gateway and tunnel-client. It first reconciles sandbox state, then opens the gateway and starts the tunnel. It has no startup timeout, daemon mode, automatic restart, or service installation. Use `Ctrl+C` to stop an interactive process or let an external service manager supervise the same foreground command.

Inspect a running instance from another terminal:

```bash
chat2shell status
```

Run locally without opening the tunnel:

```bash
CHAT2SHELL_ENABLE_TUNNEL=0 ./scripts/run.sh
CHAT2SHELL_ENABLE_TUNNEL=0 chat2shell setup
CHAT2SHELL_ENABLE_TUNNEL=0 chat2shell serve
```

Run with the configured Secure MCP Tunnel:
The MCP endpoint binds to loopback by default. If you publish it through a reverse proxy, another tunnel, or a non-loopback bind, you are responsible for authenticating and restricting that route.

Update an npm installation through the package manager that owns it:

```bash
./scripts/run.sh
npm update --global chat2shell
```

Inspect or stop the runtime:
chat2shell intentionally has no self-update command.

## Development

```bash
./scripts/status.sh
./scripts/stop.sh
pnpm install
pnpm check
pnpm test:e2e
```

`pnpm check` is the normal development and CI quality gate: formatting, linting, typechecking, unit and integration tests, and the production bundle. 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.

## Host workspace approval

When ChatGPT requests a new host path, `sandbox_create` returns an `approval_id` instead of creating a sandbox.
Review and decide it locally:

```bash
pnpm cli approval list
pnpm cli approval approve approval_...
pnpm cli approval reject approval_...
chat2shell approval list
chat2shell approval approve approval_...
chat2shell approval reject approval_...
```

A host operator can also register a path directly:

```bash
pnpm cli workspace add /path/to/repository --mode clone
pnpm cli workspace add /path/to/repository --mode direct
pnpm cli workspace list
chat2shell workspace add /path/to/repository --mode clone
chat2shell workspace add /path/to/repository --mode direct
chat2shell workspace list
```

After approval, call `sandbox_create` with the returned `workspace_id`.
Expand Down Expand Up @@ -197,8 +216,10 @@ Current locations are:

The pinned template, Bash session behavior, and retention values are listed in Current policy above. CPU, memory, and disk use Docker Sandboxes defaults rather than chat2shell policy.

OAuth, reboot persistence, the monitoring dashboard, and a browser approval UI do not exist. They will be considered only after the current prototype proves useful.

## License

chat2shell is available under the MIT License. See `THIRD_PARTY_NOTICES.md` for bundled third-party notices.

## Roadmap

See [`ROADMAP.md`](./ROADMAP.md) for the intended product direction.
10 changes: 10 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Roadmap

chat2shell currently focuses on giving ChatGPT a capable shell inside disposable Docker Sandbox microVMs while keeping the host outside the execution boundary.

The intended next product directions are:

- OAuth authentication for deployments that need an authenticated MCP endpoint.
- A web interface for viewing and managing sandboxes and workspaces.

These are product directions, not release commitments. The README and architecture document describe the behavior available today.
20 changes: 8 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ The Secure MCP Tunnel transports MCP messages to one loopback endpoint and does
The chat2shell process owns identity, path approval, lifecycle, expiration, and routing.
The `SbxDriver` is the only component allowed to invoke `sbx`, and it accepts structured values rather than raw arguments.

## Runtime ownership

The npm package exposes one `chat2shell` executable. `chat2shell serve` is the only server entry point and stays in the foreground. It validates local dependencies, reconciles persisted sandbox state, opens the loopback MCP gateway, starts tunnel-client as its child, and closes both on SIGINT or SIGTERM.

There is no shell-script supervisor, fixed startup timeout, daemon mode, automatic restart, or service installation. A process manager may supervise `chat2shell serve`, but those policies remain outside the product. `chat2shell status` reads the runtime PID and probes both the MCP gateway and tunnel readiness endpoints.

## Port exposure

`sandbox_expose` asks `SbxDriver` to publish one TCP/IPv4 sandbox port on `0.0.0.0` using an automatically assigned host port. The service inside the sandbox must listen on `0.0.0.0`; chat2shell does not start it or check its protocol or health.
Expand All @@ -39,8 +45,7 @@ The mapping is owned by Docker Sandboxes and disappears with the sandbox. chat2s
`workspace_id` identifies persistent files and can be attached to a replacement sandbox after the previous runtime expires.
Neither identity depends on a ChatGPT conversation or MCP session, so another conversation under the same authenticated principal can discover it with `sandbox_list` or `workspace_list`.

The current authentication provider maps every accepted tunnel request to `local-owner`.
A future OAuth provider will change principal establishment without changing sandbox or workspace identities.
The current authentication provider maps every accepted MCP request to `local-owner`. Secure MCP Tunnel is the recommended transport, but transport and access control remain outside this provider.

## Workspace modes

Expand Down Expand Up @@ -112,13 +117,4 @@ The trash directory is not emptied automatically. Host workspaces are never move
At controller startup, persisted active records are reconciled with `sbx ls`.
Any microVM left by the previous controller is removed and its sandbox record becomes `failed` because the foreground CodexPro session belonged to that controller.
The user must destroy the failed sandbox before creating a replacement; chat2shell does not restart CodexPro or recover the old runtime automatically.

## Deferred boundaries

- OAuth principal identity and per-user authorization
- A human approval and monitoring dashboard
- Reboot service installation
- Export automation for private clone changes
- Central Docker Sandbox governance profiles

These additions do not require changing the tunnel endpoint, stable IDs, workspace modes, or public tool names.
Reconciliation completes before the MCP gateway begins listening and has no chat2shell-imposed time limit.
31 changes: 26 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
{
"name": "chat2shell",
"version": "0.2.0",
"private": true,
"description": "A private MCP control plane for isolated shell and sandbox access from ChatGPT.",
"homepage": "https://github.com/nbsp1221/chat2shell#readme",
"bugs": "https://github.com/nbsp1221/chat2shell/issues",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/nbsp1221/chat2shell.git"
},
"bin": {
"chat2shell": "dist/cli.mjs"
},
"files": [
"dist",
"LICENSE",
"README.md",
"ROADMAP.md",
"THIRD_PARTY_NOTICES.md"
],
"type": "module",
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": {
"build": "pnpm clean && tsc -p tsconfig.build.json",
"clean": "node scripts/clean-dist.mjs",
"cli": "node dist/src/cli/main.js",
"build": "tsdown",
"cli": "node dist/cli.mjs",
"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",
"start": "node dist/cli.mjs serve",
"test": "vitest run --project unit --project integration",
"test:coverage": "vitest run --coverage --project unit --project integration",
"test:e2e": "vitest run --project e2e",
Expand All @@ -23,6 +42,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"cac": "7.0.0",
"zod": "3.25.76"
},
"devDependencies": {
Expand All @@ -36,6 +56,7 @@
"oxfmt": "0.59.0",
"oxlint": "1.73.0",
"oxlint-tsgolint": "0.25.0",
"tsdown": "0.22.14",
"typescript": "^5.9.0",
"vitest": "4.1.11"
},
Expand Down
Loading