rabano 0.1.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/LICENSE +21 -0
- package/README.md +181 -0
- package/ai.sh +96 -0
- package/package.json +50 -0
- package/scripts/dev.ts +58 -0
- package/scripts/doctor.ts +120 -0
- package/scripts/menu.ts +47 -0
- package/scripts/onboard.ts +102 -0
- package/scripts/reset.ts +70 -0
- package/scripts/stop.ts +35 -0
- package/templates/Dockerfile +65 -0
- package/templates/devcontainer.json +40 -0
- package/templates/env +19 -0
- package/templates/post-start.mjs +104 -0
- package/tsconfig.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asaf Ratzon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Secure AI Dev Environment — Template
|
|
2
|
+
|
|
3
|
+
A reusable, hardened development container template for Next.js projects with AI coding assistants (Claude, Kilo, OpenCode).
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
Your code and VSCodium stay on your host machine as normal. The AI tools and all development activity run inside an isolated Docker container, connected to your terminal via SSH.
|
|
8
|
+
|
|
9
|
+
**DevPod** is the glue — it reads the `.rabano/` configuration, builds the Docker image, manages the container lifecycle, and sets up the SSH tunnel so your terminal session lands inside the container automatically.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Host machine
|
|
13
|
+
├── VSCodium → edits files normally (bind-mounted from container)
|
|
14
|
+
├── terminal → SSH'd into container via DevPod
|
|
15
|
+
│ ├── claude → AI tools run here, isolated
|
|
16
|
+
│ ├── kilo
|
|
17
|
+
│ └── opencode
|
|
18
|
+
└── git push/pull → only possible from host, blocked inside container
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`./ai.sh` is the single entry point — it lets you start or stop the container.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Repository Structure
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
.
|
|
29
|
+
├── .rabano/
|
|
30
|
+
│ ├── .env # API keys — fill in before first start
|
|
31
|
+
│ ├── Dockerfile # Image: Node 22, AI tools, git protocol block
|
|
32
|
+
│ ├── devcontainer.json # Dev container config: mounts, startup hook
|
|
33
|
+
│ ├── post-start.mjs # Security checks + readiness output on every start
|
|
34
|
+
│ └── README.md # Security model details
|
|
35
|
+
├── scripts/ # rabano logic — not copied to user projects
|
|
36
|
+
│ ├── dev.ts
|
|
37
|
+
│ ├── stop.ts
|
|
38
|
+
│ ├── reset.ts
|
|
39
|
+
│ ├── doctor.ts
|
|
40
|
+
│ └── onboard.ts
|
|
41
|
+
├── templates/ # Copied into user's .rabano/ during onboarding
|
|
42
|
+
│ ├── Dockerfile
|
|
43
|
+
│ ├── devcontainer.json
|
|
44
|
+
│ ├── post-start.mjs
|
|
45
|
+
│ └── env
|
|
46
|
+
├── .gitignore
|
|
47
|
+
├── ai.sh # Entry point — run from your project directory
|
|
48
|
+
└── README.md # This file
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Prerequisites
|
|
54
|
+
|
|
55
|
+
- [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
|
56
|
+
- [VSCodium](https://vscodium.com/)
|
|
57
|
+
- [DevPod CLI](https://devpod.sh/docs/getting-started/install)
|
|
58
|
+
|
|
59
|
+
### One-time DevPod setup
|
|
60
|
+
|
|
61
|
+
After installing the DevPod CLI, register Docker as the backend provider:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
devpod provider add docker
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This only needs to be done once per machine.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### 1. Fill in your API keys
|
|
74
|
+
|
|
75
|
+
Edit `.rabano/.env`:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
79
|
+
KILO_API_KEY=your-kilo-key-here
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Start the container
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
./ai.sh
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Select **Start session**. First run builds the image (a few minutes). Subsequent starts are fast.
|
|
89
|
+
|
|
90
|
+
### 3. Verify startup
|
|
91
|
+
|
|
92
|
+
Security checks run automatically on every start. Re-run anytime from inside the container:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
status
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 4. Stop the container
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
./ai.sh
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Select **Stop all**.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Container Management
|
|
109
|
+
|
|
110
|
+
| Command | Description |
|
|
111
|
+
| ----------------- | -------------------------------------------- |
|
|
112
|
+
| `./ai.sh` | Start, stop, or reset the container |
|
|
113
|
+
| `./ai.sh` → Reset | Wipe all workspaces + images and start fresh |
|
|
114
|
+
| `devpod list` | List active workspaces |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## AI Tools
|
|
119
|
+
|
|
120
|
+
Run inside the container terminal:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
claude # Claude Code (Anthropic)
|
|
124
|
+
kilo # Kilo AI
|
|
125
|
+
opencode # OpenCode
|
|
126
|
+
status # Re-run security + readiness check
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Security Model
|
|
132
|
+
|
|
133
|
+
| Control | Implementation |
|
|
134
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------- |
|
|
135
|
+
| Non-root user | All processes run as `devuser` (uid 1001) |
|
|
136
|
+
| Filesystem isolation | Only the repo is mounted — host is not visible |
|
|
137
|
+
| Git remote block | `protocol.allow never` in `/etc/gitconfig` — enforced at the git layer, requires root to override |
|
|
138
|
+
| No credentials forwarded | `gitCredentialHelperConfigLocation: none` in devcontainer.json |
|
|
139
|
+
| No privilege escalation | `no-new-privileges:true` security opt |
|
|
140
|
+
| Secrets never in image | API keys injected at runtime via `.env` only |
|
|
141
|
+
|
|
142
|
+
Remote git operations are blocked inside the container. Push from your host terminal instead.
|
|
143
|
+
|
|
144
|
+
See `.rabano/README.md` for full details.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Git Workflow
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Inside container — local operations ✅
|
|
152
|
+
git add .
|
|
153
|
+
git commit -m "message"
|
|
154
|
+
git log / diff / branch
|
|
155
|
+
|
|
156
|
+
# Remote operations — host terminal only 🚫 blocked inside container
|
|
157
|
+
git push / pull / fetch
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Using This Template
|
|
163
|
+
|
|
164
|
+
When using rabano in a real project, run `ai.sh` from your project directory — the onboarding flow will create `.rabano/` automatically. You do not need to copy `scripts/` — those stay with the rabano package.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Troubleshooting
|
|
169
|
+
|
|
170
|
+
**Container fails to start** — the startup check prints exactly which check failed and why.
|
|
171
|
+
|
|
172
|
+
**API key warnings** — check `.rabano/.env` has the correct variable names, then run `devpod up . --recreate`.
|
|
173
|
+
|
|
174
|
+
**AI tool not found** — rebuild with `devpod up . --recreate`. Do not install tools manually inside a running container as changes won't persist.
|
|
175
|
+
|
|
176
|
+
## Personal notes to review with claude:
|
|
177
|
+
|
|
178
|
+
- update readme: IDE should not be pre-requiste.
|
|
179
|
+
- ensure container prefix is more unique? boxa
|
|
180
|
+
- add to backlog: fix onboarding - it currently fails (temp-test dir on host machine)
|
|
181
|
+
- update all docs/package.json description to be more related to boxa (Cage for agent.. rather then previous 'box' terminology).
|
package/ai.sh
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# ai.sh — rabano entry point
|
|
4
|
+
# Run this from your project directory (or via npx rabano).
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# ─── Guard: inside container ─────────────────────────────────────────────────
|
|
10
|
+
if [ "$(whoami)" = "devuser" ]; then
|
|
11
|
+
echo ""
|
|
12
|
+
echo " You are running rabano from inside the dev container."
|
|
13
|
+
echo " Open a terminal on your host machine and run:"
|
|
14
|
+
echo ""
|
|
15
|
+
echo " rabano (or ./path/to/ai.sh from your project directory)"
|
|
16
|
+
echo ""
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# ─── Paths ───────────────────────────────────────────────────────────────────
|
|
21
|
+
PACKAGE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
23
|
+
|
|
24
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
25
|
+
echo ""
|
|
26
|
+
echo " No git repository found."
|
|
27
|
+
echo ""
|
|
28
|
+
echo " rabano requires a git repository. Run 'git init' first, then re-run rabano."
|
|
29
|
+
echo ""
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
export RABANO_PACKAGE_DIR="$PACKAGE_DIR"
|
|
34
|
+
export RABANO_REPO_ROOT="$REPO_ROOT"
|
|
35
|
+
|
|
36
|
+
# ─── Node.js check ──────────────────────────────────────────────────────────
|
|
37
|
+
if ! command -v node &>/dev/null; then
|
|
38
|
+
echo ""
|
|
39
|
+
echo " Node.js is required to run rabano."
|
|
40
|
+
echo " Install it from https://nodejs.org/ (v18+)"
|
|
41
|
+
echo ""
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# ─── Auto-install dependencies (dev flow — npx handles this automatically) ──
|
|
46
|
+
if [ ! -d "$PACKAGE_DIR/node_modules" ]; then
|
|
47
|
+
echo " Installing rabano dependencies..."
|
|
48
|
+
(cd "$PACKAGE_DIR" && pnpm install --silent 2>/dev/null)
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ─── Onboarding ──────────────────────────────────────────────────────────────
|
|
52
|
+
if [ ! -f "$REPO_ROOT/.rabano/devcontainer.json" ]; then
|
|
53
|
+
node --import tsx/esm "$PACKAGE_DIR/scripts/onboard.ts"
|
|
54
|
+
if [ ! -f "$REPO_ROOT/.rabano/devcontainer.json" ]; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ─── Doctor (silent pre-check) ───────────────────────────────────────────────
|
|
60
|
+
if ! node --import tsx/esm "$PACKAGE_DIR/scripts/doctor.ts"; then
|
|
61
|
+
echo " Fix the issues above and re-run rabano."
|
|
62
|
+
echo ""
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# ─── Gather state for menu ──────────────────────────────────────────────────
|
|
67
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
68
|
+
WORKSPACE_NAME="rabano-$PROJECT_NAME"
|
|
69
|
+
|
|
70
|
+
ACTIVE_COUNT=$(docker ps --filter "name=rabano-" --format "{{.Names}}" 2>/dev/null | wc -l | tr -d '[:space:]')
|
|
71
|
+
|
|
72
|
+
HAS_KEY=false
|
|
73
|
+
if [ -f "$REPO_ROOT/.rabano/.env" ]; then
|
|
74
|
+
while IFS='=' read -r key value; do
|
|
75
|
+
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
|
76
|
+
[[ -z "$key" ]] && continue
|
|
77
|
+
value="$(echo "$value" | tr -d '[:space:]')"
|
|
78
|
+
if [ -n "$value" ]; then HAS_KEY=true; break; fi
|
|
79
|
+
done < "$REPO_ROOT/.rabano/.env"
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# ─── Interactive menu (clack) ────────────────────────────────────────────────
|
|
83
|
+
# stdout → /dev/tty (clack UI displayed on terminal)
|
|
84
|
+
# stderr → captured (selected action string)
|
|
85
|
+
set +e
|
|
86
|
+
action=$(node --import tsx/esm "$PACKAGE_DIR/scripts/menu.ts" "$PROJECT_NAME" "$ACTIVE_COUNT" "$HAS_KEY" 2>&1 >/dev/tty)
|
|
87
|
+
set -e
|
|
88
|
+
|
|
89
|
+
# ─── Execute selection ───────────────────────────────────────────────────────
|
|
90
|
+
case "$action" in
|
|
91
|
+
dev) node --import tsx/esm "$PACKAGE_DIR/scripts/dev.ts" ;;
|
|
92
|
+
stop) node --import tsx/esm "$PACKAGE_DIR/scripts/stop.ts" ;;
|
|
93
|
+
reset) node --import tsx/esm "$PACKAGE_DIR/scripts/reset.ts" ;;
|
|
94
|
+
doctor) node --import tsx/esm "$PACKAGE_DIR/scripts/doctor.ts" --verbose ;;
|
|
95
|
+
quit|*) exit 0 ;;
|
|
96
|
+
esac
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rabano",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Secure AI Box — isolated dev environments for AI coding assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rabano": "./ai.sh"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@clack/prompts": "^1.1.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@biomejs/biome": "^2.4.6",
|
|
14
|
+
"@types/node": "^25.4.0",
|
|
15
|
+
"tsx": "^4.21.0",
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Asaf Ratzon",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/asafratzon/rabano"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/asafratzon/rabano#readme",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"ai",
|
|
30
|
+
"claude",
|
|
31
|
+
"docker",
|
|
32
|
+
"devpod",
|
|
33
|
+
"isolation",
|
|
34
|
+
"security",
|
|
35
|
+
"devcontainer"
|
|
36
|
+
],
|
|
37
|
+
"files": [
|
|
38
|
+
"ai.sh",
|
|
39
|
+
"scripts/",
|
|
40
|
+
"templates/",
|
|
41
|
+
"tsconfig.json",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "biome check .",
|
|
47
|
+
"format": "biome format --write .",
|
|
48
|
+
"fix:all": "biome check --write ."
|
|
49
|
+
}
|
|
50
|
+
}
|
package/scripts/dev.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/dev.ts — Start the dev container and SSH in
|
|
4
|
+
// Called by ai.sh — do not run directly.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
import { log, outro } from "@clack/prompts";
|
|
10
|
+
|
|
11
|
+
const workspaceDir = process.env.RABANO_REPO_ROOT;
|
|
12
|
+
if (!workspaceDir) {
|
|
13
|
+
log.error("RABANO_REPO_ROOT not set — run via ai.sh");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const workspaceName = `rabano-${basename(workspaceDir)}`;
|
|
18
|
+
|
|
19
|
+
// Check if workspace already exists
|
|
20
|
+
const listResult = spawnSync("devpod", ["list", "--output", "json"], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
const workspaceExists = listResult.stdout?.includes(`"id":"${workspaceName}"`);
|
|
24
|
+
|
|
25
|
+
if (workspaceExists) {
|
|
26
|
+
log.step(`Connecting to workspace '${workspaceName}'...`);
|
|
27
|
+
} else {
|
|
28
|
+
log.step("Starting dev container...");
|
|
29
|
+
const up = spawnSync(
|
|
30
|
+
"devpod",
|
|
31
|
+
[
|
|
32
|
+
"up",
|
|
33
|
+
workspaceDir,
|
|
34
|
+
"--devcontainer-path",
|
|
35
|
+
".rabano/devcontainer.json",
|
|
36
|
+
"--ide",
|
|
37
|
+
"none",
|
|
38
|
+
"--id",
|
|
39
|
+
workspaceName,
|
|
40
|
+
],
|
|
41
|
+
{ stdio: "inherit" },
|
|
42
|
+
);
|
|
43
|
+
if (up.status !== 0) {
|
|
44
|
+
outro("Failed to start dev container.");
|
|
45
|
+
process.exit(up.status ?? 1);
|
|
46
|
+
}
|
|
47
|
+
log.step("Connecting via SSH...");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ssh = spawnSync(
|
|
51
|
+
"devpod",
|
|
52
|
+
["ssh", workspaceName, "--workdir", "/workspace"],
|
|
53
|
+
{
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
process.exit(ssh.status ?? 0);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/doctor.ts — Host readiness check for rabano
|
|
4
|
+
// Runs silently on success; exits non-zero on failure.
|
|
5
|
+
// Pass --verbose for a full report.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { log, outro } from "@clack/prompts";
|
|
11
|
+
|
|
12
|
+
const verbose = process.argv.includes("--verbose");
|
|
13
|
+
const repoRoot = process.env.RABANO_REPO_ROOT;
|
|
14
|
+
if (!repoRoot) {
|
|
15
|
+
log.error("RABANO_REPO_ROOT not set — run via ai.sh");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const errors: string[] = [];
|
|
20
|
+
|
|
21
|
+
function check(label: string, ok: boolean, detail?: string): void {
|
|
22
|
+
if (ok) {
|
|
23
|
+
if (verbose)
|
|
24
|
+
log.success(`${label}${detail ? ` \x1b[2m${detail}\x1b[0m` : ""}`);
|
|
25
|
+
} else {
|
|
26
|
+
errors.push(`${label}${detail ? `: ${detail}` : ""}`);
|
|
27
|
+
if (verbose) log.error(`${label}${detail ? ` ${detail}` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandExists(cmd: string): boolean {
|
|
32
|
+
const r = spawnSync("command", ["-v", cmd], {
|
|
33
|
+
shell: true,
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
});
|
|
36
|
+
return r.status === 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (verbose) console.log("");
|
|
40
|
+
|
|
41
|
+
// --- Docker installed ---
|
|
42
|
+
check(
|
|
43
|
+
"Docker installed",
|
|
44
|
+
commandExists("docker"),
|
|
45
|
+
commandExists("docker") ? undefined : "'docker' not found in PATH",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// --- Docker running ---
|
|
49
|
+
const dockerInfo = spawnSync("docker", ["info"], {
|
|
50
|
+
encoding: "utf8",
|
|
51
|
+
stdio: "pipe",
|
|
52
|
+
});
|
|
53
|
+
check(
|
|
54
|
+
"Docker running",
|
|
55
|
+
dockerInfo.status === 0,
|
|
56
|
+
dockerInfo.status === 0 ? undefined : "Docker daemon not responding",
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// --- DevPod installed ---
|
|
60
|
+
const devpodInstalled = commandExists("devpod");
|
|
61
|
+
check(
|
|
62
|
+
"DevPod installed",
|
|
63
|
+
devpodInstalled,
|
|
64
|
+
devpodInstalled ? undefined : "'devpod' not found in PATH",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// --- DevPod provider configured ---
|
|
68
|
+
if (devpodInstalled) {
|
|
69
|
+
let providerOk = false;
|
|
70
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
71
|
+
const r = spawnSync("devpod", ["provider", "list"], {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
stdio: "pipe",
|
|
74
|
+
});
|
|
75
|
+
if (r.stdout?.toLowerCase().includes("docker")) {
|
|
76
|
+
providerOk = true;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (attempt < 5) {
|
|
80
|
+
spawnSync("sleep", ["1"]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
check(
|
|
84
|
+
"DevPod provider configured",
|
|
85
|
+
providerOk,
|
|
86
|
+
providerOk
|
|
87
|
+
? undefined
|
|
88
|
+
: "no provider found — run: devpod provider add docker",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- .rabano/ config present ---
|
|
93
|
+
const configOk =
|
|
94
|
+
existsSync(`${repoRoot}/.rabano/devcontainer.json`) &&
|
|
95
|
+
existsSync(`${repoRoot}/.rabano/Dockerfile`);
|
|
96
|
+
check(
|
|
97
|
+
".rabano/ config present",
|
|
98
|
+
configOk,
|
|
99
|
+
configOk
|
|
100
|
+
? undefined
|
|
101
|
+
: "missing .rabano/devcontainer.json or .rabano/Dockerfile",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// --- Report ---
|
|
105
|
+
if (errors.length > 0) {
|
|
106
|
+
if (verbose) {
|
|
107
|
+
console.log("");
|
|
108
|
+
log.error("rabano doctor found problems:");
|
|
109
|
+
for (const err of errors) {
|
|
110
|
+
console.log(` \x1b[2m•\x1b[0m ${err}`);
|
|
111
|
+
}
|
|
112
|
+
console.log("");
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (verbose) {
|
|
118
|
+
console.log("");
|
|
119
|
+
outro("All checks passed.");
|
|
120
|
+
}
|
package/scripts/menu.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/menu.ts — rabano interactive menu (powered by @clack/prompts)
|
|
4
|
+
// Called by ai.sh — outputs selected action to stderr.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import { box, cancel, isCancel, select } from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
const [projectName = "unknown", activeCountStr, hasKeyStr] =
|
|
10
|
+
process.argv.slice(2);
|
|
11
|
+
const activeCount = Number.parseInt(activeCountStr ?? "0", 10);
|
|
12
|
+
const hasKey = hasKeyStr === "true";
|
|
13
|
+
|
|
14
|
+
// ─── Status box ──────────────────────────────────────────────────────────────
|
|
15
|
+
const sessionLabel =
|
|
16
|
+
activeCount === 1
|
|
17
|
+
? "1 active dev container"
|
|
18
|
+
: `${activeCount} active dev containers`;
|
|
19
|
+
const lines = [];
|
|
20
|
+
lines.push(`status: ${sessionLabel}`);
|
|
21
|
+
lines.push(`api keys: ${hasKey ? "configured" : "none"} (.rabano/.env)`);
|
|
22
|
+
box(lines.join("\n"), ` rabano · ${projectName} `, {
|
|
23
|
+
contentAlign: "center",
|
|
24
|
+
titleAlign: "center",
|
|
25
|
+
width: "auto",
|
|
26
|
+
rounded: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ─── Menu ───────────────────────────────────────────────────────────────────
|
|
30
|
+
const action = await select({
|
|
31
|
+
message: "Menu:",
|
|
32
|
+
options: [
|
|
33
|
+
{ value: "dev", label: "Start session" },
|
|
34
|
+
{ value: "stop", label: "Stop all" },
|
|
35
|
+
{ value: "reset", label: "Reset (wipe workspaces + images)" },
|
|
36
|
+
{ value: "doctor", label: "Doctor" },
|
|
37
|
+
{ value: "quit", label: "Quit" },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (isCancel(action)) {
|
|
42
|
+
cancel();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Output action to stderr — ai.sh captures it via redirection
|
|
47
|
+
process.stderr.write(action as string);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/onboard.ts — First-time setup for a project using rabano
|
|
4
|
+
// Called by ai.sh when no .rabano/ config is found in the project.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
cpSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { basename, join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
box,
|
|
17
|
+
cancel,
|
|
18
|
+
confirm,
|
|
19
|
+
intro,
|
|
20
|
+
isCancel,
|
|
21
|
+
log,
|
|
22
|
+
outro,
|
|
23
|
+
} from "@clack/prompts";
|
|
24
|
+
|
|
25
|
+
const packageDir = process.env.RABANO_PACKAGE_DIR;
|
|
26
|
+
const repoRoot = process.env.RABANO_REPO_ROOT;
|
|
27
|
+
|
|
28
|
+
if (!packageDir || !repoRoot) {
|
|
29
|
+
log.error("RABANO_PACKAGE_DIR / RABANO_REPO_ROOT not set — run via ai.sh");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const templatesDir = join(packageDir, "templates");
|
|
34
|
+
const rabanoDir = join(repoRoot, ".rabano");
|
|
35
|
+
const projectName = basename(repoRoot);
|
|
36
|
+
|
|
37
|
+
// ─── Intro ────────────────────────────────────────────────────────────────────
|
|
38
|
+
intro("rabano — First-time setup");
|
|
39
|
+
|
|
40
|
+
box(
|
|
41
|
+
`project : ${projectName}\nlocation : ${rabanoDir}`,
|
|
42
|
+
"No .rabano/ config found — rabano will create it now.",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const ok = await confirm({ message: "Continue?" });
|
|
46
|
+
|
|
47
|
+
if (isCancel(ok) || !ok) {
|
|
48
|
+
cancel("Setup cancelled.");
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Copy templates ───────────────────────────────────────────────────────────
|
|
53
|
+
mkdirSync(rabanoDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
cpSync(join(templatesDir, "Dockerfile"), join(rabanoDir, "Dockerfile"));
|
|
56
|
+
cpSync(join(templatesDir, "post-start.mjs"), join(rabanoDir, "post-start.mjs"));
|
|
57
|
+
|
|
58
|
+
// Substitute project name in devcontainer.json using JSON parse/stringify
|
|
59
|
+
const dcTemplate = readFileSync(
|
|
60
|
+
join(templatesDir, "devcontainer.json"),
|
|
61
|
+
"utf8",
|
|
62
|
+
);
|
|
63
|
+
const dcJson: unknown = JSON.parse(
|
|
64
|
+
dcTemplate.replace(/RABANO_PROJECT_NAME/g, projectName),
|
|
65
|
+
);
|
|
66
|
+
writeFileSync(
|
|
67
|
+
join(rabanoDir, "devcontainer.json"),
|
|
68
|
+
`${JSON.stringify(dcJson, null, 2)}\n`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
log.success("Copied config templates to .rabano/");
|
|
72
|
+
|
|
73
|
+
// ─── Create .env ──────────────────────────────────────────────────────────────
|
|
74
|
+
const envPath = join(rabanoDir, ".env");
|
|
75
|
+
if (existsSync(envPath)) {
|
|
76
|
+
log.info(".rabano/.env already exists — leaving it untouched");
|
|
77
|
+
} else {
|
|
78
|
+
cpSync(join(templatesDir, "env"), envPath);
|
|
79
|
+
log.success("Created .rabano/.env");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Ensure .rabano/.env is gitignored ─────────────────────────────────────────
|
|
83
|
+
const gitignorePath = join(repoRoot, ".gitignore");
|
|
84
|
+
const gitignoreEntry = ".rabano/.env";
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
existsSync(gitignorePath) &&
|
|
88
|
+
readFileSync(gitignorePath, "utf8").includes(gitignoreEntry)
|
|
89
|
+
) {
|
|
90
|
+
log.info(".rabano/.env already in .gitignore");
|
|
91
|
+
} else {
|
|
92
|
+
const addition =
|
|
93
|
+
"\n# rabano — API keys must never be committed\n.rabano/.env\n";
|
|
94
|
+
const existing = existsSync(gitignorePath)
|
|
95
|
+
? readFileSync(gitignorePath, "utf8")
|
|
96
|
+
: "";
|
|
97
|
+
writeFileSync(gitignorePath, existing + addition);
|
|
98
|
+
log.success("Added .rabano/.env to .gitignore");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log.warn("Add your API keys to .rabano/.env before starting the container.");
|
|
102
|
+
outro("Setup complete. Run rabano again to start your session.");
|
package/scripts/reset.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/reset.ts — Full reset: delete all rabano workspaces and Docker images
|
|
4
|
+
// Called by ai.sh — do not run directly.
|
|
5
|
+
// Run 'ai.sh' → Start session after this to get a fresh build.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { log, outro } from "@clack/prompts";
|
|
10
|
+
|
|
11
|
+
// ─── Step 1: Find all rabano-* DevPod workspaces ───────────────────────────────
|
|
12
|
+
const listResult = spawnSync("devpod", ["list", "--output", "json"], {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const workspaces: string[] = [];
|
|
17
|
+
if (listResult.stdout) {
|
|
18
|
+
const matches = listResult.stdout.matchAll(/"id":"(rabano-[^"]+)"/g);
|
|
19
|
+
for (const match of matches) {
|
|
20
|
+
if (match[1]) workspaces.push(match[1]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Step 2: Stop and delete all rabano workspaces ─────────────────────────────
|
|
25
|
+
if (workspaces.length === 0) {
|
|
26
|
+
log.info("No rabano workspaces found.");
|
|
27
|
+
} else {
|
|
28
|
+
log.step(`Stopping and deleting ${workspaces.length} workspace(s)...`);
|
|
29
|
+
for (const ws of workspaces) {
|
|
30
|
+
log.step(` Removing ${ws}...`);
|
|
31
|
+
spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
|
|
32
|
+
spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Step 3: Remove cached Docker images ─────────────────────────────────────
|
|
37
|
+
// DevPod images are named vsc-<project>-<hash> (based on the folder name, not
|
|
38
|
+
// the workspace --id). For each rabano-<project> workspace, strip the "rabano-"
|
|
39
|
+
// prefix to get the image reference filter.
|
|
40
|
+
log.step("Removing cached Docker images...");
|
|
41
|
+
|
|
42
|
+
const allImageIds = new Set<string>();
|
|
43
|
+
for (const ws of workspaces) {
|
|
44
|
+
const projectName = ws.replace(/^rabano-/, "");
|
|
45
|
+
const findImages = spawnSync(
|
|
46
|
+
"docker",
|
|
47
|
+
[
|
|
48
|
+
"images",
|
|
49
|
+
"--filter",
|
|
50
|
+
`reference=vsc-${projectName}-*`,
|
|
51
|
+
"--format",
|
|
52
|
+
"{{.ID}}",
|
|
53
|
+
],
|
|
54
|
+
{ encoding: "utf8" },
|
|
55
|
+
);
|
|
56
|
+
const ids = (findImages.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
57
|
+
for (const id of ids) allImageIds.add(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (allImageIds.size > 0) {
|
|
61
|
+
log.info(` Found ${allImageIds.size} image(s) — removing...`);
|
|
62
|
+
spawnSync("docker", ["rmi", "--force", ...allImageIds], { stdio: "inherit" });
|
|
63
|
+
} else {
|
|
64
|
+
log.info(" No cached images found.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
spawnSync("docker", ["image", "prune", "--force"], { stdio: "inherit" });
|
|
68
|
+
|
|
69
|
+
// ─── Done ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
outro("Reset complete. Run 'ai.sh' and select 'Start session' to start fresh.");
|
package/scripts/stop.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// scripts/stop.ts — Stop and remove all rabano dev container workspaces
|
|
4
|
+
// Called by ai.sh — do not run directly.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { log, outro } from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
const listResult = spawnSync("devpod", ["list", "--output", "json"], {
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const workspaces: string[] = [];
|
|
15
|
+
if (listResult.stdout) {
|
|
16
|
+
const matches = listResult.stdout.matchAll(/"id":"(rabano-[^"]+)"/g);
|
|
17
|
+
for (const match of matches) {
|
|
18
|
+
if (match[1]) workspaces.push(match[1]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (workspaces.length === 0) {
|
|
23
|
+
log.info("No rabano workspaces found.");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log.step("Stopping all rabano workspaces...");
|
|
28
|
+
|
|
29
|
+
for (const ws of workspaces) {
|
|
30
|
+
log.step(`Stopping ${ws}...`);
|
|
31
|
+
spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
|
|
32
|
+
spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
outro("All rabano workspaces stopped and removed.");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Secure AI Dev Container — Next.js / Node.js
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Non-root user, no git remote access, AI tools: claude, kilo, opencode
|
|
5
|
+
# =============================================================================
|
|
6
|
+
|
|
7
|
+
FROM node:22-bookworm-slim
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# System packages
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
13
|
+
git \
|
|
14
|
+
curl \
|
|
15
|
+
wget \
|
|
16
|
+
bash \
|
|
17
|
+
jq \
|
|
18
|
+
unzip \
|
|
19
|
+
ca-certificates \
|
|
20
|
+
procps \
|
|
21
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Create non-root user
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
RUN groupadd --gid 1001 devuser \
|
|
27
|
+
&& useradd --uid 1001 --gid devuser --shell /bin/bash --create-home devuser
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Git remote block — enforced via git's own system-level config.
|
|
31
|
+
# protocol.allow never -> blocks https, ssh, git:// and ALL other transports
|
|
32
|
+
# protocol.file.allow -> keeps local operations (commit, branch, log) working
|
|
33
|
+
#
|
|
34
|
+
# This is set in /etc/gitconfig (system scope) so it applies to every user
|
|
35
|
+
# and every process — including direct calls to /usr/bin/git. It cannot be
|
|
36
|
+
# bypassed without overwriting /etc/gitconfig, which requires root.
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
RUN git config --system protocol.allow never && \
|
|
39
|
+
git config --system protocol.file.allow always
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Global npm tools — installed as root so they land in /usr/local/bin
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
RUN npm install -g \
|
|
45
|
+
@anthropic-ai/claude-code \
|
|
46
|
+
@kilocode/cli \
|
|
47
|
+
opencode-ai \
|
|
48
|
+
&& npm cache clean --force
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Switch to non-root user for remainder
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
USER devuser
|
|
54
|
+
WORKDIR /workspace
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Shell experience
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
RUN echo 'export PS1="\[\033[01;32m\][devcontainer]\[\033[00m\] \[\033[01;34m\]\w\[\033[00m\] \$ "' \
|
|
60
|
+
>> /home/devuser/.bashrc && \
|
|
61
|
+
echo 'echo ""' >> /home/devuser/.bashrc && \
|
|
62
|
+
echo "echo \" Type 'status' to re-run the readiness check.\"" >> /home/devuser/.bashrc && \
|
|
63
|
+
echo 'alias status="node /workspace/.rabano/post-start.mjs"' >> /home/devuser/.bashrc
|
|
64
|
+
|
|
65
|
+
CMD ["/bin/bash"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "RABANO_PROJECT_NAME",
|
|
3
|
+
|
|
4
|
+
// Build from local Dockerfile
|
|
5
|
+
"build": {
|
|
6
|
+
"dockerfile": "Dockerfile",
|
|
7
|
+
"context": ".."
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
// Mount the repo as the workspace — nothing else is visible
|
|
11
|
+
"workspaceFolder": "/workspace",
|
|
12
|
+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
|
|
13
|
+
|
|
14
|
+
// Pass API keys from a local .env file — never baked into the image
|
|
15
|
+
// Create .rabano/.env on your host (see README.md)
|
|
16
|
+
"runArgs": [
|
|
17
|
+
"--name",
|
|
18
|
+
"rabano-${localWorkspaceFolderBasename}",
|
|
19
|
+
"--env-file",
|
|
20
|
+
"${localWorkspaceFolder}/.rabano/.env"
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
// Run security + readiness checks every time container starts
|
|
24
|
+
"postStartCommand": "node /workspace/.rabano/post-start.mjs",
|
|
25
|
+
|
|
26
|
+
// Terminal-only — no extensions installed
|
|
27
|
+
"customizations": {
|
|
28
|
+
"vscode": {
|
|
29
|
+
"extensions": [],
|
|
30
|
+
"settings": {
|
|
31
|
+
// Do not forward host git credentials into the container
|
|
32
|
+
"remote.containers.gitCredentialHelperConfigLocation": "none"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Drop capabilities — container cannot gain new privileges
|
|
38
|
+
"capAdd": [],
|
|
39
|
+
"securityOpt": ["no-new-privileges:true"]
|
|
40
|
+
}
|
package/templates/env
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# rabano — API Keys
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Fill in your keys before starting the container.
|
|
5
|
+
# Add .rabano/.env to your project's .gitignore — keys must never be committed.
|
|
6
|
+
# =============================================================================
|
|
7
|
+
|
|
8
|
+
# Anthropic (Claude Code — optional if using Pro subscription via browser auth)
|
|
9
|
+
ANTHROPIC_API_KEY=
|
|
10
|
+
|
|
11
|
+
# Kilo AI
|
|
12
|
+
KILO_API_KEY=
|
|
13
|
+
|
|
14
|
+
# OpenCode supports multiple providers — add whichever you use
|
|
15
|
+
OPENAI_API_KEY=
|
|
16
|
+
GEMINI_API_KEY=
|
|
17
|
+
|
|
18
|
+
# Optional: OpenRouter (gives access to many models via one key)
|
|
19
|
+
OPENROUTER_API_KEY=
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// post-start.mjs — Security validation & readiness check
|
|
4
|
+
// Runs automatically on every container start via postStartCommand.
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
import { intro, log, outro } from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
const run = (cmd) => {
|
|
11
|
+
try {
|
|
12
|
+
return execSync(cmd, {
|
|
13
|
+
encoding: "utf8",
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
}).trim();
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
22
|
+
|
|
23
|
+
let errors = 0;
|
|
24
|
+
|
|
25
|
+
const ok = (label, detail) =>
|
|
26
|
+
log.success(`${label.padEnd(24)}${detail ? dim(detail) : ""}`);
|
|
27
|
+
const warn = (label, detail) =>
|
|
28
|
+
log.warn(`${label.padEnd(24)}${detail ? dim(detail) : ""}`);
|
|
29
|
+
const fail = (label, detail) => {
|
|
30
|
+
log.error(`${label.padEnd(24)}${detail || ""}`);
|
|
31
|
+
errors++;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ─── Header ──────────────────────────────────────────────────────────────────
|
|
35
|
+
intro("rabano — Secure AI Box");
|
|
36
|
+
|
|
37
|
+
// ─── Security ────────────────────────────────────────────────────────────────
|
|
38
|
+
log.step("Security");
|
|
39
|
+
|
|
40
|
+
const whoami = run("whoami");
|
|
41
|
+
if (whoami !== "root") {
|
|
42
|
+
ok("non-root user", whoami ?? "unknown");
|
|
43
|
+
} else {
|
|
44
|
+
fail("non-root user", "running as root — container is misconfigured");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const gitProtocol = run("git config --system protocol.allow");
|
|
48
|
+
if (gitProtocol === "never") {
|
|
49
|
+
ok("git remote block", "protocol.allow = never");
|
|
50
|
+
} else {
|
|
51
|
+
fail("git remote block", "not set — rebuild the container");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
execSync("/usr/bin/git -C /workspace push", { stdio: "pipe" });
|
|
56
|
+
fail("push blocked", "git push succeeded — remote access is NOT blocked");
|
|
57
|
+
} catch {
|
|
58
|
+
ok("push blocked", "remote push not possible");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── AI tools ────────────────────────────────────────────────────────────────
|
|
62
|
+
log.step("AI tools");
|
|
63
|
+
|
|
64
|
+
const checkTool = (cmd) => {
|
|
65
|
+
const out = run(`${cmd} --version`);
|
|
66
|
+
if (out !== null) {
|
|
67
|
+
ok(cmd, out.split("\n")[0]);
|
|
68
|
+
} else {
|
|
69
|
+
fail(cmd, "not found — rebuild container");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
checkTool("claude");
|
|
74
|
+
checkTool("kilo");
|
|
75
|
+
checkTool("opencode");
|
|
76
|
+
|
|
77
|
+
// ─── Runtimes ────────────────────────────────────────────────────────────────
|
|
78
|
+
log.step("Runtimes");
|
|
79
|
+
|
|
80
|
+
ok("node", run("node --version") ?? "not found");
|
|
81
|
+
ok("npm", `v${run("npm --version") ?? "not found"}`);
|
|
82
|
+
|
|
83
|
+
// ─── API keys ────────────────────────────────────────────────────────────────
|
|
84
|
+
log.step("API keys");
|
|
85
|
+
|
|
86
|
+
const checkKey = (varName) => {
|
|
87
|
+
const val = process.env[varName];
|
|
88
|
+
if (val) {
|
|
89
|
+
ok(varName, `${val.substring(0, 12)}...`);
|
|
90
|
+
} else {
|
|
91
|
+
warn(varName, "not set — add to .rabano/.env");
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
checkKey("ANTHROPIC_API_KEY");
|
|
96
|
+
checkKey("KILO_API_KEY");
|
|
97
|
+
|
|
98
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
99
|
+
if (errors === 0) {
|
|
100
|
+
outro("Ready.");
|
|
101
|
+
} else {
|
|
102
|
+
outro(`${errors} error(s) — see above. Rebuild the container to fix.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Visit https://aka.ms/tsconfig to read more about this file
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
// Environment Settings
|
|
5
|
+
// See also https://aka.ms/tsconfig/module
|
|
6
|
+
"module": "nodenext",
|
|
7
|
+
"target": "esnext",
|
|
8
|
+
"lib": ["esnext"],
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
|
|
11
|
+
// Other Outputs
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
|
|
14
|
+
// Stricter Typechecking Options
|
|
15
|
+
"noUncheckedIndexedAccess": true,
|
|
16
|
+
"exactOptionalPropertyTypes": true,
|
|
17
|
+
|
|
18
|
+
// Recommended Options
|
|
19
|
+
"strict": true,
|
|
20
|
+
"verbatimModuleSyntax": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true,
|
|
23
|
+
"moduleDetection": "force",
|
|
24
|
+
"skipLibCheck": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["scripts/**/*.ts"]
|
|
27
|
+
}
|