sanduary 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/Dockerfile +72 -0
- package/.devcontainer/devcontainer.base.json +15 -0
- package/.devcontainer/devcontainer.json +17 -0
- package/.devcontainer/docker-compose.yml +26 -0
- package/.devcontainer/sample.devcontainer.override.json +31 -0
- package/.devcontainer/sample.docker-compose.override.yml +24 -0
- package/.devcontainer/templates/devcontainer.override.template.json +31 -0
- package/.devcontainer/templates/docker-compose.override.template.yml +24 -0
- package/README.md +289 -0
- package/package.json +40 -0
- package/scripts/postinstall.js +207 -0
- package/scripts/sandbox.sh +93 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Dev Container用Dockerfile
|
|
2
|
+
# Node.js 22 + Docker CLI + Playwright
|
|
3
|
+
|
|
4
|
+
FROM node:22-bookworm
|
|
5
|
+
|
|
6
|
+
# Docker CLI インストール(ホストのDockerを操作するため)
|
|
7
|
+
RUN apt-get update && apt-get install -y \
|
|
8
|
+
ca-certificates \
|
|
9
|
+
curl \
|
|
10
|
+
gnupg \
|
|
11
|
+
lsb-release \
|
|
12
|
+
&& mkdir -p /etc/apt/keyrings \
|
|
13
|
+
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
|
14
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
|
15
|
+
&& apt-get update \
|
|
16
|
+
&& apt-get install -y docker-ce-cli docker-compose-plugin \
|
|
17
|
+
&& apt-get clean \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
|
+
|
|
20
|
+
# 開発に便利なツール
|
|
21
|
+
RUN apt-get update && apt-get install -y \
|
|
22
|
+
git \
|
|
23
|
+
vim \
|
|
24
|
+
jq \
|
|
25
|
+
zip \
|
|
26
|
+
less \
|
|
27
|
+
&& apt-get clean \
|
|
28
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
# ロケール設定(マルチバイト文字対応)
|
|
31
|
+
RUN apt-get update && apt-get install -y \
|
|
32
|
+
locales \
|
|
33
|
+
&& sed -i -e 's/# ja_JP.UTF-8 UTF-8/ja_JP.UTF-8 UTF-8/' /etc/locale.gen \
|
|
34
|
+
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
|
|
35
|
+
&& locale-gen \
|
|
36
|
+
&& apt-get clean \
|
|
37
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
38
|
+
|
|
39
|
+
ENV LANG=ja_JP.UTF-8
|
|
40
|
+
|
|
41
|
+
# Playwright依存ライブラリ
|
|
42
|
+
RUN apt-get update && apt-get install -y \
|
|
43
|
+
libatk1.0-0 \
|
|
44
|
+
libatk-bridge2.0-0 \
|
|
45
|
+
libcups2 \
|
|
46
|
+
libxkbcommon0 \
|
|
47
|
+
libatspi2.0-0 \
|
|
48
|
+
libxcomposite1 \
|
|
49
|
+
libxdamage1 \
|
|
50
|
+
libxfixes3 \
|
|
51
|
+
libxrandr2 \
|
|
52
|
+
libgbm1 \
|
|
53
|
+
libasound2 \
|
|
54
|
+
libnspr4 \
|
|
55
|
+
libnss3 \
|
|
56
|
+
&& apt-get clean \
|
|
57
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
58
|
+
|
|
59
|
+
# グローバルnpmパッケージ
|
|
60
|
+
RUN npm install -g npm@10.9.0
|
|
61
|
+
|
|
62
|
+
# プロジェクト名のビルド引数(デフォルト: project)
|
|
63
|
+
ARG PROJECT_NAME=project
|
|
64
|
+
|
|
65
|
+
# ワークスペースディレクトリをnodeユーザーの所有で作成
|
|
66
|
+
RUN mkdir -p /workspaces/${PROJECT_NAME} && chown node:node /workspaces/${PROJECT_NAME}
|
|
67
|
+
|
|
68
|
+
# 作業ディレクトリ
|
|
69
|
+
WORKDIR /workspaces/${PROJECT_NAME}
|
|
70
|
+
|
|
71
|
+
# nodeユーザーに切り替え
|
|
72
|
+
USER node
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"service": "devcontainer",
|
|
3
|
+
"dockerComposeFile": ["docker-compose.yml"],
|
|
4
|
+
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
|
5
|
+
"workspaceMount": "",
|
|
6
|
+
"updateRemoteUserUID": true,
|
|
7
|
+
"initializeCommand": "echo 'PROJECT_ROOT='$(pwd) > .devcontainer/.env && echo 'PROJECT_NAME='$(basename $(pwd)) >> .devcontainer/.env && echo 'GIT_ORIGIN_URL='$(git remote get-url origin 2>/dev/null || echo '') >> .devcontainer/.env && [ -f ~/.claude-sandbox-credentials.json ] || echo '{}' > ~/.claude-sandbox-credentials.json && [ -f ~/.claude-sandbox.json ] || echo '{}' > ~/.claude-sandbox.json",
|
|
8
|
+
"postCreateCommand": "git init && git remote add origin \"${GIT_ORIGIN_URL:-/host-project}\" && git fetch && git checkout $(git -C /host-project branch --show-current) && echo 'alias claude=\"npx claude --dangerously-skip-permissions\"' >> ~/.bashrc",
|
|
9
|
+
"shutdownAction": "stopCompose",
|
|
10
|
+
"remoteUser": "node",
|
|
11
|
+
"remoteEnv": {
|
|
12
|
+
"LANG": "ja_JP.UTF-8",
|
|
13
|
+
"LC_ALL": "ja_JP.UTF-8"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"service": "devcontainer",
|
|
3
|
+
"dockerComposeFile": [
|
|
4
|
+
"docker-compose.yml"
|
|
5
|
+
],
|
|
6
|
+
"workspaceFolder": "/workspaces/${PROJECT_NAME:-project}",
|
|
7
|
+
"workspaceMount": "",
|
|
8
|
+
"updateRemoteUserUID": true,
|
|
9
|
+
"initializeCommand": "echo 'PROJECT_ROOT='$(pwd) > .devcontainer/.env && echo 'PROJECT_NAME='$(basename $(pwd)) >> .devcontainer/.env && [ -f ~/.claude-sandbox-credentials.json ] || echo '{}' > ~/.claude-sandbox-credentials.json && [ -f ~/.claude-sandbox.json ] || echo '{}' > ~/.claude-sandbox.json",
|
|
10
|
+
"postCreateCommand": "git init && git remote add origin \"${GIT_ORIGIN_URL:-/host-project}\" && git fetch && git checkout $(git -C /host-project branch --show-current) && echo 'alias claude=\"npx claude --dangerously-skip-permissions\"' >> ~/.bashrc",
|
|
11
|
+
"shutdownAction": "stopCompose",
|
|
12
|
+
"remoteUser": "node",
|
|
13
|
+
"remoteEnv": {
|
|
14
|
+
"LANG": "ja_JP.UTF-8",
|
|
15
|
+
"LC_ALL": "ja_JP.UTF-8"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
devcontainer:
|
|
3
|
+
platform: linux/amd64
|
|
4
|
+
build:
|
|
5
|
+
context: .
|
|
6
|
+
dockerfile: Dockerfile
|
|
7
|
+
args:
|
|
8
|
+
PROJECT_NAME: ${PROJECT_NAME:-project}
|
|
9
|
+
group_add:
|
|
10
|
+
- ${DOCKER_GID:-999}
|
|
11
|
+
volumes:
|
|
12
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
13
|
+
- ${PROJECT_ROOT:-..}:/host-project
|
|
14
|
+
- ${HOME}/.claude:/home/node/.claude
|
|
15
|
+
- ${HOME}/.claude-sandbox.json:/home/node/.claude.json
|
|
16
|
+
- ${HOME}/.claude-sandbox-credentials.json:/home/node/.claude/.credentials.json
|
|
17
|
+
- ${HOME}/.gitconfig:/home/node/.gitconfig
|
|
18
|
+
working_dir: /workspaces/${PROJECT_NAME:-project}
|
|
19
|
+
command: sleep infinity
|
|
20
|
+
environment:
|
|
21
|
+
LANG: ja_JP.UTF-8
|
|
22
|
+
LC_ALL: ja_JP.UTF-8
|
|
23
|
+
GIT_CONFIG_COUNT: 1
|
|
24
|
+
GIT_CONFIG_KEY_0: safe.directory
|
|
25
|
+
GIT_CONFIG_VALUE_0: /workspaces/${PROJECT_NAME:-project}
|
|
26
|
+
GIT_ORIGIN_URL: ${GIT_ORIGIN_URL:-/host-project}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sandbox",
|
|
3
|
+
"dockerComposeFile": ["docker-compose.override.yml"],
|
|
4
|
+
"postCreateCommand": "npm ci && npx prisma generate",
|
|
5
|
+
"postStartCommand": "npx prisma migrate deploy",
|
|
6
|
+
"forwardPorts": [3000, 3306],
|
|
7
|
+
"portsAttributes": {
|
|
8
|
+
"3000": {
|
|
9
|
+
"label": "Frontend",
|
|
10
|
+
"onAutoForward": "notify"
|
|
11
|
+
},
|
|
12
|
+
"3306": {
|
|
13
|
+
"label": "Database",
|
|
14
|
+
"onAutoForward": "silent"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"customizations": {
|
|
18
|
+
"vscode": {
|
|
19
|
+
"extensions": ["Prisma.prisma"],
|
|
20
|
+
"settings": {
|
|
21
|
+
"[prisma]": {
|
|
22
|
+
"editor.defaultFormatter": "Prisma.prisma"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"remoteEnv": {
|
|
28
|
+
"DATABASE_URL": "mysql://root:password@db:3306/sandbox",
|
|
29
|
+
"NODE_ENV": "development"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
devcontainer:
|
|
3
|
+
depends_on:
|
|
4
|
+
db:
|
|
5
|
+
condition: service_healthy
|
|
6
|
+
environment:
|
|
7
|
+
DATABASE_URL: mysql://root:password@db:3306/sandbox
|
|
8
|
+
NODE_ENV: development
|
|
9
|
+
|
|
10
|
+
db:
|
|
11
|
+
image: mysql:8.0
|
|
12
|
+
environment:
|
|
13
|
+
MYSQL_ROOT_PASSWORD: password
|
|
14
|
+
MYSQL_DATABASE: "sandbox"
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
17
|
+
interval: 10s
|
|
18
|
+
timeout: 5s
|
|
19
|
+
retries: 5
|
|
20
|
+
volumes:
|
|
21
|
+
- db_data:/var/lib/mysql
|
|
22
|
+
|
|
23
|
+
volumes:
|
|
24
|
+
db_data:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"dockerComposeFile": ["docker-compose.override.yml"],
|
|
4
|
+
"postCreateCommand": "npm ci && npx prisma generate",
|
|
5
|
+
"postStartCommand": "npx prisma migrate deploy",
|
|
6
|
+
"forwardPorts": [3000, 3306],
|
|
7
|
+
"portsAttributes": {
|
|
8
|
+
"3000": {
|
|
9
|
+
"label": "Frontend",
|
|
10
|
+
"onAutoForward": "notify"
|
|
11
|
+
},
|
|
12
|
+
"3306": {
|
|
13
|
+
"label": "Database",
|
|
14
|
+
"onAutoForward": "silent"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"customizations": {
|
|
18
|
+
"vscode": {
|
|
19
|
+
"extensions": ["Prisma.prisma"],
|
|
20
|
+
"settings": {
|
|
21
|
+
"[prisma]": {
|
|
22
|
+
"editor.defaultFormatter": "Prisma.prisma"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"remoteEnv": {
|
|
28
|
+
"DATABASE_URL": "mysql://root:password@db:3306/{{PROJECT_NAME}}",
|
|
29
|
+
"NODE_ENV": "development"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
devcontainer:
|
|
3
|
+
depends_on:
|
|
4
|
+
db:
|
|
5
|
+
condition: service_healthy
|
|
6
|
+
environment:
|
|
7
|
+
DATABASE_URL: mysql://root:password@db:3306/{{PROJECT_NAME}}
|
|
8
|
+
NODE_ENV: development
|
|
9
|
+
|
|
10
|
+
db:
|
|
11
|
+
image: mysql:8.0
|
|
12
|
+
environment:
|
|
13
|
+
MYSQL_ROOT_PASSWORD: password
|
|
14
|
+
MYSQL_DATABASE: "{{PROJECT_NAME}}"
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
|
17
|
+
interval: 10s
|
|
18
|
+
timeout: 5s
|
|
19
|
+
retries: 5
|
|
20
|
+
volumes:
|
|
21
|
+
- db_data:/var/lib/mysql
|
|
22
|
+
|
|
23
|
+
volumes:
|
|
24
|
+
db_data:
|
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# sanduary
|
|
2
|
+
|
|
3
|
+
A secure development sandbox environment for AI agents - providing safe, isolated workspaces for AI coding assistants like Claude Code through Docker DevContainers.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**sanduary** (sanctuary + sandbox) creates disposable, secure development environments where AI agents can safely execute code, install packages, and perform development tasks without affecting your host system. Each session runs in an isolated Docker container that is automatically cleaned up when you exit.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Isolated Environments**: Each session runs in a dedicated Docker DevContainer
|
|
12
|
+
- **Dynamic Configuration**: Supports project-specific DevContainer overrides
|
|
13
|
+
- **Auto-Cleanup**: Containers are automatically removed after session ends
|
|
14
|
+
- **Git Integration**: Seamlessly works with your existing Git repositories
|
|
15
|
+
- **Customizable**: Extend base configuration with override files
|
|
16
|
+
- **Vulnerability Reduction**: Generate DevContainer files on-demand instead of committing to Git
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Global Installation (Recommended)
|
|
21
|
+
|
|
22
|
+
Install globally to use across multiple projects:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g sanduary
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This allows any team member to generate their own DevContainer configuration without tracking potentially vulnerable configuration files in Git.
|
|
29
|
+
|
|
30
|
+
### Project-Local Installation
|
|
31
|
+
|
|
32
|
+
For advanced use cases, install as a project dependency:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install --save-dev sanduary
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then add to your `package.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"scripts": {
|
|
43
|
+
"postinstall": "sdy init"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This automatically sets up the sandbox environment when developers run `npm install`, providing quick DevContainer setup without committing configuration files.
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### 1. Initialize DevContainer Configuration
|
|
53
|
+
|
|
54
|
+
Navigate to your project directory and initialize the DevContainer files:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
cd your-project
|
|
58
|
+
sdy init
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This creates `.devcontainer/` directory with base configuration files.
|
|
62
|
+
|
|
63
|
+
**Important**: Add `.devcontainer/` to your `.gitignore` to keep configuration local:
|
|
64
|
+
|
|
65
|
+
```gitignore
|
|
66
|
+
# DevContainer - generated by sanduary
|
|
67
|
+
.devcontainer/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Start the Sandbox Environment
|
|
71
|
+
|
|
72
|
+
Launch the DevContainer (default command):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
sdy run
|
|
76
|
+
# or simply
|
|
77
|
+
sdy
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The container will:
|
|
81
|
+
- Build and start automatically
|
|
82
|
+
- Execute any `postCreateCommand` and `postStartCommand` from `devcontainer.json`
|
|
83
|
+
- Connect you to an interactive bash session
|
|
84
|
+
- Clean up automatically when you exit
|
|
85
|
+
|
|
86
|
+
## Commands
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---------|-------------|
|
|
90
|
+
| `sdy init` | Initialize DevContainer configuration files in current project |
|
|
91
|
+
| `sdy run` | Start the DevContainer sandbox (default) |
|
|
92
|
+
| `sdy` | Alias for `sdy run` |
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
### Base Configuration
|
|
97
|
+
|
|
98
|
+
After running `sdy init`, you'll have:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
.devcontainer/
|
|
102
|
+
├── devcontainer.base.json # Base DevContainer settings
|
|
103
|
+
├── devcontainer.json # Main configuration file
|
|
104
|
+
├── docker-compose.yml # Docker Compose settings
|
|
105
|
+
├── Dockerfile # Container image definition
|
|
106
|
+
└── templates/
|
|
107
|
+
├── devcontainer.override.template.json
|
|
108
|
+
└── docker-compose.override.template.yml
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Note**: These files should NOT be committed to Git. Each developer generates their own configuration via `sdy init`.
|
|
112
|
+
|
|
113
|
+
### Override Files
|
|
114
|
+
|
|
115
|
+
Customize your sandbox environment by creating override files:
|
|
116
|
+
|
|
117
|
+
#### DevContainer Override
|
|
118
|
+
|
|
119
|
+
Create `.devcontainer/devcontainer.override.json`:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"customizations": {
|
|
124
|
+
"vscode": {
|
|
125
|
+
"extensions": [
|
|
126
|
+
"dbaeumer.vscode-eslint",
|
|
127
|
+
"esbenp.prettier-vscode"
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"postCreateCommand": "npm install"
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Docker Compose Override
|
|
136
|
+
|
|
137
|
+
Create `.devcontainer/docker-compose.override.yml`:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
services:
|
|
141
|
+
devcontainer:
|
|
142
|
+
environment:
|
|
143
|
+
- NODE_ENV=development
|
|
144
|
+
ports:
|
|
145
|
+
- "3000:3000"
|
|
146
|
+
volumes:
|
|
147
|
+
- ./custom-data:/data
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Configuration Files
|
|
151
|
+
|
|
152
|
+
The following files are automatically created in your home directory:
|
|
153
|
+
|
|
154
|
+
- `~/.claude-sandbox.json` - General sandbox settings
|
|
155
|
+
- `~/.claude-sandbox-credentials.json` - Authentication credentials
|
|
156
|
+
|
|
157
|
+
## Git Management Best Practices
|
|
158
|
+
|
|
159
|
+
### What to Commit
|
|
160
|
+
|
|
161
|
+
✅ **DO commit**:
|
|
162
|
+
- `package.json` (with sanduary as dependency)
|
|
163
|
+
- `.gitignore` (with `.devcontainer/` excluded)
|
|
164
|
+
- Project source code and assets
|
|
165
|
+
|
|
166
|
+
### What NOT to Commit
|
|
167
|
+
|
|
168
|
+
❌ **DO NOT commit**:
|
|
169
|
+
- `.devcontainer/` directory and its contents
|
|
170
|
+
- `devcontainer.json`
|
|
171
|
+
- `docker-compose.yml`
|
|
172
|
+
- `Dockerfile`
|
|
173
|
+
|
|
174
|
+
### Why?
|
|
175
|
+
|
|
176
|
+
1. **Security**: DevContainer configurations can contain sensitive settings or expose vulnerabilities
|
|
177
|
+
2. **Flexibility**: Each developer can customize their environment without affecting others
|
|
178
|
+
3. **Version Control**: Configuration generation is handled by sanduary versions, not Git history
|
|
179
|
+
|
|
180
|
+
### Sample `.gitignore`
|
|
181
|
+
|
|
182
|
+
```gitignore
|
|
183
|
+
# DevContainer - generated by sanduary
|
|
184
|
+
.devcontainer/
|
|
185
|
+
|
|
186
|
+
# Dependency directories
|
|
187
|
+
node_modules/
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Workflow Examples
|
|
191
|
+
|
|
192
|
+
### Team Collaboration
|
|
193
|
+
|
|
194
|
+
1. **Project Setup** (once per project):
|
|
195
|
+
```bash
|
|
196
|
+
# Add sanduary to project
|
|
197
|
+
npm install --save-dev sanduary
|
|
198
|
+
|
|
199
|
+
# Update .gitignore
|
|
200
|
+
echo ".devcontainer/" >> .gitignore
|
|
201
|
+
|
|
202
|
+
# Commit
|
|
203
|
+
git add package.json .gitignore
|
|
204
|
+
git commit -m "feat: add sanduary for DevContainer management"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
2. **New Developer Setup**:
|
|
208
|
+
```bash
|
|
209
|
+
# Clone project
|
|
210
|
+
git clone <repo-url>
|
|
211
|
+
cd <project>
|
|
212
|
+
|
|
213
|
+
# Install dependencies (automatically runs sdy init via postinstall)
|
|
214
|
+
npm install
|
|
215
|
+
|
|
216
|
+
# Start sandbox
|
|
217
|
+
sdy
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
3. **Existing Developer**:
|
|
221
|
+
```bash
|
|
222
|
+
# Pull latest changes
|
|
223
|
+
git pull
|
|
224
|
+
|
|
225
|
+
# Update dependencies if needed
|
|
226
|
+
npm install
|
|
227
|
+
|
|
228
|
+
# Start sandbox
|
|
229
|
+
sdy
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Global Installation Workflow
|
|
233
|
+
|
|
234
|
+
1. **One-time Setup**:
|
|
235
|
+
```bash
|
|
236
|
+
# Install globally
|
|
237
|
+
npm install -g sanduary
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
2. **Per-project Usage**:
|
|
241
|
+
```bash
|
|
242
|
+
cd your-project
|
|
243
|
+
|
|
244
|
+
# Initialize (only needed once per project)
|
|
245
|
+
sdy init
|
|
246
|
+
|
|
247
|
+
# Start sandbox (anytime)
|
|
248
|
+
sdy
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## How It Works
|
|
252
|
+
|
|
253
|
+
1. **Dynamic Naming**: Each session gets a unique project name (`sandbox-XXXX`)
|
|
254
|
+
2. **Git-Aware**: Automatically detects project root via Git
|
|
255
|
+
3. **Docker Compose**: Uses Docker Compose for container orchestration
|
|
256
|
+
4. **Lifecycle Hooks**: Executes `postCreateCommand` and `postStartCommand` from DevContainer config
|
|
257
|
+
5. **Auto-Cleanup**: Containers and volumes are removed on exit via cleanup trap
|
|
258
|
+
|
|
259
|
+
## Use Cases
|
|
260
|
+
|
|
261
|
+
- **AI Agent Sandboxing**: Safe environment for Claude Code and similar AI assistants
|
|
262
|
+
- **Dependency Testing**: Test package installations without polluting host
|
|
263
|
+
- **Code Experimentation**: Try risky changes in isolated environment
|
|
264
|
+
- **Multi-Project Development**: Switch between different project configurations easily
|
|
265
|
+
- **Onboarding**: New team members get consistent development environments instantly
|
|
266
|
+
|
|
267
|
+
## Requirements
|
|
268
|
+
|
|
269
|
+
- Docker Engine
|
|
270
|
+
- Node.js and npm (for installation)
|
|
271
|
+
- Git (for project detection)
|
|
272
|
+
- `jq` (for JSON parsing)
|
|
273
|
+
|
|
274
|
+
## Environment Variables
|
|
275
|
+
|
|
276
|
+
The following environment variables are automatically set during execution:
|
|
277
|
+
|
|
278
|
+
- `PROJECT_ROOT`: Git repository root directory
|
|
279
|
+
- `PROJECT_NAME`: Unique sandbox instance name
|
|
280
|
+
- `GIT_ORIGIN_URL`: Set to `/host-project` when launched via `sdy`
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
ISC
|
|
285
|
+
|
|
286
|
+
## Links
|
|
287
|
+
|
|
288
|
+
- [GitHub Repository](https://github.com/yamadamasahiro/sanduary)
|
|
289
|
+
- [Report Issues](https://github.com/yamadamasahiro/sanduary/issues)
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanduary",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Development sandbox environment for AI agents - A secure sanctuary for running AI coding assistants in Docker DevContainers",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"devcontainer",
|
|
7
|
+
"docker",
|
|
8
|
+
"sandbox",
|
|
9
|
+
"ai",
|
|
10
|
+
"agent",
|
|
11
|
+
"claude",
|
|
12
|
+
"development",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/yamadamasahiro/sanduary.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/yamadamasahiro/sanduary/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/yamadamasahiro/sanduary#readme",
|
|
23
|
+
"scripts": {},
|
|
24
|
+
"bin": {
|
|
25
|
+
"sdy": "scripts/sandbox.sh"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"scripts/",
|
|
29
|
+
".devcontainer/"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"js-yaml": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"main": "index.js",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@anthropic-ai/claude-code": "^2.0.72"
|
|
37
|
+
},
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "ISC"
|
|
40
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const yaml = require('js-yaml')
|
|
5
|
+
|
|
6
|
+
const INIT_CWD = process.env.INIT_CWD || process.cwd()
|
|
7
|
+
const SANDBOX_DEVCONTAINER_PATH =
|
|
8
|
+
'../packages/infrastructure/sandbox/.devcontainer'
|
|
9
|
+
|
|
10
|
+
// 連結すべきフィールド(deepMergeではなく && で連結)
|
|
11
|
+
const CONCAT_FIELDS = ['postCreateCommand', 'postStartCommand']
|
|
12
|
+
|
|
13
|
+
// テンプレートディレクトリ
|
|
14
|
+
const templatesDir = path.join(__dirname, '../.devcontainer/templates')
|
|
15
|
+
|
|
16
|
+
// パス
|
|
17
|
+
const devcontainerDir = path.join(INIT_CWD, '.devcontainer')
|
|
18
|
+
const baseJsonPath = path.join(
|
|
19
|
+
__dirname,
|
|
20
|
+
'../.devcontainer/devcontainer.base.json',
|
|
21
|
+
)
|
|
22
|
+
const overrideJsonPath = path.join(
|
|
23
|
+
devcontainerDir,
|
|
24
|
+
'devcontainer.override.json',
|
|
25
|
+
)
|
|
26
|
+
const outputJsonPath = path.join(devcontainerDir, 'devcontainer.json')
|
|
27
|
+
|
|
28
|
+
// Docker Compose パス
|
|
29
|
+
const baseComposePath = path.join(
|
|
30
|
+
__dirname,
|
|
31
|
+
'../.devcontainer/docker-compose.yml',
|
|
32
|
+
)
|
|
33
|
+
const overrideComposePath = path.join(
|
|
34
|
+
devcontainerDir,
|
|
35
|
+
'docker-compose.override.yml',
|
|
36
|
+
)
|
|
37
|
+
const outputComposePath = path.join(devcontainerDir, 'docker-compose.yml')
|
|
38
|
+
|
|
39
|
+
// Dockerfile パス
|
|
40
|
+
const sourceDockerfilePath = path.join(__dirname, '../.devcontainer/Dockerfile')
|
|
41
|
+
const outputDockerfilePath = path.join(devcontainerDir, 'Dockerfile')
|
|
42
|
+
|
|
43
|
+
// ディレクトリ作成
|
|
44
|
+
fs.mkdirSync(devcontainerDir, { recursive: true })
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 深いマージを行う関数
|
|
48
|
+
* - 配列はconcatで結合
|
|
49
|
+
* - オブジェクトは再帰的にマージ
|
|
50
|
+
* - プリミティブ値はoverrideが優先
|
|
51
|
+
*/
|
|
52
|
+
function deepMerge(base, override) {
|
|
53
|
+
if (override === undefined) {
|
|
54
|
+
return base
|
|
55
|
+
}
|
|
56
|
+
if (base === undefined) {
|
|
57
|
+
return override
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 両方が配列の場合はconcat
|
|
61
|
+
if (Array.isArray(base) && Array.isArray(override)) {
|
|
62
|
+
return [...base, ...override]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 両方がオブジェクトの場合は再帰マージ
|
|
66
|
+
if (
|
|
67
|
+
typeof base === 'object' &&
|
|
68
|
+
base !== null &&
|
|
69
|
+
typeof override === 'object' &&
|
|
70
|
+
override !== null &&
|
|
71
|
+
!Array.isArray(base) &&
|
|
72
|
+
!Array.isArray(override)
|
|
73
|
+
) {
|
|
74
|
+
const result = { ...base }
|
|
75
|
+
for (const key of Object.keys(override)) {
|
|
76
|
+
result[key] = deepMerge(base[key], override[key])
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// それ以外はoverrideが優先
|
|
82
|
+
return override
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 特定のフィールドを連結する関数
|
|
87
|
+
* CONCAT_FIELDSに指定されたフィールドは && で連結
|
|
88
|
+
*/
|
|
89
|
+
function applyConcatFields(merged, base, override) {
|
|
90
|
+
for (const field of CONCAT_FIELDS) {
|
|
91
|
+
if (base[field] && override[field]) {
|
|
92
|
+
merged[field] = `${base[field]} && ${override[field]}`
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return merged
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* テンプレートからファイルを作成する関数
|
|
100
|
+
* PROJECT_NAMEプレースホルダーを置換
|
|
101
|
+
*/
|
|
102
|
+
function createFromTemplate(templatePath, outputPath) {
|
|
103
|
+
if (fs.existsSync(templatePath)) {
|
|
104
|
+
const projectName = path.basename(INIT_CWD)
|
|
105
|
+
let templateContent = fs.readFileSync(templatePath, 'utf8')
|
|
106
|
+
templateContent = templateContent.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
|
|
107
|
+
fs.writeFileSync(outputPath, templateContent)
|
|
108
|
+
console.log(`Created from template: ${outputPath}`)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
return false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// =====================================
|
|
115
|
+
// devcontainer.json の生成
|
|
116
|
+
// =====================================
|
|
117
|
+
|
|
118
|
+
// base.jsonを読み込み
|
|
119
|
+
const base = JSON.parse(fs.readFileSync(baseJsonPath, 'utf8'))
|
|
120
|
+
|
|
121
|
+
// overrideファイルが存在しない場合、サンプルテンプレートを作成(sample.接頭辞付き)
|
|
122
|
+
const overrideJsonTemplate = path.join(templatesDir, 'devcontainer.override.template.json')
|
|
123
|
+
const sampleOverrideJsonPath = path.join(devcontainerDir, 'sample.devcontainer.override.json')
|
|
124
|
+
if (!fs.existsSync(overrideJsonPath) && !fs.existsSync(sampleOverrideJsonPath)) {
|
|
125
|
+
createFromTemplate(overrideJsonTemplate, sampleOverrideJsonPath)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// override.jsonを読み込み(存在しない場合は空オブジェクト)
|
|
129
|
+
let override = {}
|
|
130
|
+
if (fs.existsSync(overrideJsonPath)) {
|
|
131
|
+
override = JSON.parse(fs.readFileSync(overrideJsonPath, 'utf8'))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// dockerComposeFileをマージ
|
|
135
|
+
const baseDockerComposeFiles = (base.dockerComposeFile || []).map(
|
|
136
|
+
(file) => `${SANDBOX_DEVCONTAINER_PATH}/${file}`,
|
|
137
|
+
)
|
|
138
|
+
const overrideDockerComposeFiles = override.dockerComposeFile || []
|
|
139
|
+
|
|
140
|
+
// オブジェクトをマージ(override が優先、深いマージで remoteEnv 等も正しくマージ)
|
|
141
|
+
const merged = deepMerge(base, override)
|
|
142
|
+
|
|
143
|
+
// CONCAT_FIELDSは && で連結(deepMergeではoverrideが優先されるため、ここで連結処理)
|
|
144
|
+
applyConcatFields(merged, base, override)
|
|
145
|
+
|
|
146
|
+
// dockerComposeFileは配列を結合(ただしdocker-compose.ymlを生成するのでそれを参照)
|
|
147
|
+
merged.dockerComposeFile = ['docker-compose.yml']
|
|
148
|
+
|
|
149
|
+
// 出力
|
|
150
|
+
fs.writeFileSync(outputJsonPath, JSON.stringify(merged, null, 2) + '\n')
|
|
151
|
+
|
|
152
|
+
console.log(`Generated: ${outputJsonPath}`)
|
|
153
|
+
|
|
154
|
+
// =====================================
|
|
155
|
+
// docker-compose.yml の生成
|
|
156
|
+
// =====================================
|
|
157
|
+
|
|
158
|
+
// base docker-compose.ymlを読み込み
|
|
159
|
+
const baseComposeContent = fs.readFileSync(baseComposePath, 'utf8')
|
|
160
|
+
const baseCompose = yaml.load(baseComposeContent)
|
|
161
|
+
|
|
162
|
+
// overrideファイルが存在しない場合、サンプルテンプレートを作成(sample.接頭辞付き)
|
|
163
|
+
const overrideComposeTemplate = path.join(templatesDir, 'docker-compose.override.template.yml')
|
|
164
|
+
const sampleOverrideComposePath = path.join(devcontainerDir, 'sample.docker-compose.override.yml')
|
|
165
|
+
if (!fs.existsSync(overrideComposePath) && !fs.existsSync(sampleOverrideComposePath)) {
|
|
166
|
+
createFromTemplate(overrideComposeTemplate, sampleOverrideComposePath)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// override docker-compose.ymlを読み込み(存在しない場合は空オブジェクト)
|
|
170
|
+
let overrideCompose = {}
|
|
171
|
+
if (fs.existsSync(overrideComposePath)) {
|
|
172
|
+
const overrideComposeContent = fs.readFileSync(overrideComposePath, 'utf8')
|
|
173
|
+
overrideCompose = yaml.load(overrideComposeContent)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 深いマージを実行
|
|
177
|
+
const mergedCompose = deepMerge(baseCompose, overrideCompose)
|
|
178
|
+
|
|
179
|
+
// build.contextとbuild.dockerfileのパスを調整(ローカルのDockerfileを参照)
|
|
180
|
+
if (
|
|
181
|
+
mergedCompose.services &&
|
|
182
|
+
mergedCompose.services.devcontainer &&
|
|
183
|
+
mergedCompose.services.devcontainer.build
|
|
184
|
+
) {
|
|
185
|
+
mergedCompose.services.devcontainer.build.context = '.'
|
|
186
|
+
mergedCompose.services.devcontainer.build.dockerfile = 'Dockerfile'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// YAMLとして出力(変数展開を維持するため、quotingOptionsを設定)
|
|
190
|
+
const outputYaml = yaml.dump(mergedCompose, {
|
|
191
|
+
lineWidth: -1, // 行の折り返しを無効化
|
|
192
|
+
quotingType: '"',
|
|
193
|
+
forceQuotes: false,
|
|
194
|
+
noRefs: true,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
fs.writeFileSync(outputComposePath, outputYaml)
|
|
198
|
+
|
|
199
|
+
console.log(`Generated: ${outputComposePath}`)
|
|
200
|
+
|
|
201
|
+
// =====================================
|
|
202
|
+
// Dockerfile のコピー
|
|
203
|
+
// =====================================
|
|
204
|
+
|
|
205
|
+
fs.copyFileSync(sourceDockerfilePath, outputDockerfilePath)
|
|
206
|
+
|
|
207
|
+
console.log(`Copied: ${outputDockerfilePath}`)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# スクリプトの実際のパスを取得(シンボリックリンクを解決)
|
|
5
|
+
SCRIPT_PATH="$(realpath "$0")"
|
|
6
|
+
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
|
7
|
+
PACKAGE_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
8
|
+
|
|
9
|
+
COMMAND="${1:-run}"
|
|
10
|
+
|
|
11
|
+
case "$COMMAND" in
|
|
12
|
+
init)
|
|
13
|
+
node "$SCRIPT_DIR/postinstall.js"
|
|
14
|
+
;;
|
|
15
|
+
run)
|
|
16
|
+
# プロジェクトルートをgitから取得
|
|
17
|
+
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
|
18
|
+
export PROJECT_ROOT
|
|
19
|
+
|
|
20
|
+
DEVCONTAINER_DIR="$PROJECT_ROOT/.devcontainer"
|
|
21
|
+
DEVCONTAINER_JSON="$PROJECT_ROOT/.devcontainer/devcontainer.json"
|
|
22
|
+
|
|
23
|
+
# ランダムなプロジェクト名を生成(sandbox-XXXX形式)
|
|
24
|
+
PROJECT_NAME="sandbox-$(head -c 4 /dev/urandom | xxd -p)"
|
|
25
|
+
export PROJECT_NAME
|
|
26
|
+
|
|
27
|
+
cd "$DEVCONTAINER_DIR"
|
|
28
|
+
|
|
29
|
+
# 認証ファイルが存在しない場合は空のJSONを作成
|
|
30
|
+
CREDENTIALS_FILE="$HOME/.claude-sandbox-credentials.json"
|
|
31
|
+
if [ ! -f "$CREDENTIALS_FILE" ]; then
|
|
32
|
+
echo '{}' > "$CREDENTIALS_FILE"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# .claude-sandbox.jsonが存在しない場合は空のJSONを作成
|
|
36
|
+
CLAUDE_JSON="$HOME/.claude-sandbox.json"
|
|
37
|
+
if [ ! -f "$CLAUDE_JSON" ]; then
|
|
38
|
+
echo '{}' > "$CLAUDE_JSON"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# 終了時のcleanup関数
|
|
42
|
+
cleanup() {
|
|
43
|
+
echo ""
|
|
44
|
+
echo "Stopping Dev Container..."
|
|
45
|
+
docker compose -p "$PROJECT_NAME" down -v --remove-orphans 2>/dev/null || true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 終了時に必ずcleanupを実行
|
|
49
|
+
trap cleanup EXIT
|
|
50
|
+
|
|
51
|
+
echo "Starting Dev Container (project: $PROJECT_NAME)..."
|
|
52
|
+
docker compose -p "$PROJECT_NAME" up -d
|
|
53
|
+
|
|
54
|
+
# コンテナが起動するまで待機
|
|
55
|
+
echo "Waiting for container to be ready..."
|
|
56
|
+
sleep 5
|
|
57
|
+
|
|
58
|
+
# コンテナIDを取得
|
|
59
|
+
CONTAINER_ID=$(docker compose -p "$PROJECT_NAME" ps -q devcontainer)
|
|
60
|
+
|
|
61
|
+
# devcontainer.jsonからコマンドを読み取って実行
|
|
62
|
+
POST_CREATE_CMD=$(jq -r '.postCreateCommand // empty' "$DEVCONTAINER_JSON")
|
|
63
|
+
POST_START_CMD=$(jq -r '.postStartCommand // empty' "$DEVCONTAINER_JSON")
|
|
64
|
+
|
|
65
|
+
# ワークディレクトリを設定
|
|
66
|
+
WORKSPACE_DIR="/workspaces/$PROJECT_NAME"
|
|
67
|
+
|
|
68
|
+
if [ -n "$POST_CREATE_CMD" ]; then
|
|
69
|
+
echo "Running postCreateCommand..."
|
|
70
|
+
# sandbox.sh経由の場合はGIT_ORIGIN_URLを/host-projectに設定
|
|
71
|
+
docker exec -w "$WORKSPACE_DIR" -e GIT_ORIGIN_URL=/host-project "$CONTAINER_ID" bash -c "$POST_CREATE_CMD"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if [ -n "$POST_START_CMD" ]; then
|
|
75
|
+
echo "Running postStartCommand..."
|
|
76
|
+
docker exec -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "$POST_START_CMD"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ランダムな色コードを生成(31-36: 赤、緑、黄、青、マゼンタ、シアン)
|
|
80
|
+
COLORS=(31 32 33 34 35 36)
|
|
81
|
+
RANDOM_COLOR=${COLORS[$RANDOM % ${#COLORS[@]}]}
|
|
82
|
+
|
|
83
|
+
# カスタムPS1を設定してbashを起動
|
|
84
|
+
echo "Connecting to Dev Container..."
|
|
85
|
+
docker exec -it -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "export PS1='\[\e[${RANDOM_COLOR}m\]\h\[\e[0m\]:\w# '; exec bash" || true
|
|
86
|
+
;;
|
|
87
|
+
*)
|
|
88
|
+
echo "Usage: sandbox [init|run]"
|
|
89
|
+
echo " init - Initialize devcontainer files"
|
|
90
|
+
echo " run - Start Dev Container (default)"
|
|
91
|
+
exit 1
|
|
92
|
+
;;
|
|
93
|
+
esac
|