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 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
+ }
@@ -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.");
@@ -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.");
@@ -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
+ }