instabox 0.4.1 → 0.4.2
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/README.md +6 -2
- package/bin/instabox.mjs +71 -19
- package/lib/cli.mjs +72 -7
- package/package.json +1 -1
- package/skills/instabox/SKILL.md +38 -6
- package/skills/instabox-migrate/SKILL.md +3 -3
- package/skills/instabox-snapshots/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
npx instabox@latest
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
Signs you in to instabox in your browser (first run only; Google by default, `--github` for GitHub), lets you pick one of your boxes or start a new one (no boxes yet: a new one, no question; `--box NAME` skips the question), installs the `instabox` CLI already signed in (macOS/Linux; skip with `--no-cli`), installs the agent skills from [`../skills`](../skills)
|
|
7
|
+
Signs you in to instabox in your browser (first run only; Google by default, `--github` for GitHub), lets you pick one of your boxes or start a new one (no boxes yet: a new one, no question; `--box NAME` skips the question), installs the `instabox` CLI already signed in or updates an out-of-date one (macOS/Linux; skip with `--no-cli`), installs or updates the agent skills from [`../skills`](../skills) in `~/.claude/skills` and `~/.agents/skills` (skip with `--no-skills`), and drops you into the box over SSH. `npx instabox@latest sh` does the same.
|
|
8
|
+
|
|
9
|
+
Google and GitHub sign-ins are separate instabox accounts, each with its own boxes. The CLI's own `instabox login` signs in with GitHub by default (`--provider google` for Google), so when the CLI needs a new sign-in, `instabox login --provider google` keeps you in the account you used here with Google.
|
|
8
10
|
|
|
9
11
|
Any other command line is the instabox CLI's ([`cmd/instabox`](../cmd/instabox)): `npx instabox@latest ls` runs `instabox ls`, installing the CLI first if it is missing.
|
|
10
12
|
|
|
@@ -12,6 +14,8 @@ Any other command line is the instabox CLI's ([`cmd/instabox`](../cmd/instabox))
|
|
|
12
14
|
|
|
13
15
|
- `bin/instabox.mjs` runs instabox's loopback sign-in (RFC 8252 + PKCE) and saves the tokens where the CLI keeps its own, `~/.config/instabox/credentials.json`, so both are signed in at once. There is one copy on purpose: the server rotates refresh tokens and ends the session when a rotated one comes back.
|
|
14
16
|
- The box list is `instabox ls`: running boxes first, then newest. A suspended or stopped box is resumed or started; its settings (timeout, auto-suspend) are left alone. Without a terminal to ask (an agent running it), it lists the boxes, installs the CLI and skills, and stops; `--box NAME` or `instabox create` takes it from there.
|
|
17
|
+
- An installed CLI is checked against what the server's `/install.sh` would install: the binary's sha256 against the server's `/dl/SHA256SUMS` (release builds are named after a commit, so their versions do not sort). One the server no longer serves is replaced in place through the same installer; a development build (`instabox dev`, or `-dirty`) is left alone with a one-line notice of the install command.
|
|
18
|
+
- Skills are written only over copies marked `installed-by: instabox`; the output says which were installed, updated, already up to date, or kept because they are the user's own.
|
|
15
19
|
- The bin is also called `instabox`, so `lib/cli.mjs` finds the real CLI the way the insta CLI does: PATH without `node_modules` dirs and without links back to this script, then the installer's own targets.
|
|
16
20
|
- `prepack` copies `../skills` into the package; every skill there ships on the next publish.
|
|
17
21
|
|
|
@@ -22,7 +26,7 @@ npm ci
|
|
|
22
26
|
npm test
|
|
23
27
|
npm run format:check
|
|
24
28
|
npm pack # copies ../skills, then builds instabox-<version>.tgz
|
|
25
|
-
npx --package=./instabox-0.4.
|
|
29
|
+
npx --package=./instabox-0.4.2.tgz instabox --help
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
`INSTABOX_API_URL` points it at another instabox server.
|
package/bin/instabox.mjs
CHANGED
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
// npx instabox@latest: box + CLI + skills + SSH in; any other command goes to the instabox CLI.
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { createHash, randomBytes } from 'node:crypto';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
accessSync,
|
|
7
|
+
constants as fsConstants,
|
|
8
|
+
mkdtempSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
} from 'node:fs';
|
|
6
16
|
import http from 'node:http';
|
|
7
17
|
import { tmpdir, homedir } from 'node:os';
|
|
8
18
|
import { join, dirname } from 'node:path';
|
|
@@ -10,7 +20,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
10
20
|
import { fileURLToPath } from 'node:url';
|
|
11
21
|
import { createClient } from '../lib/api.mjs';
|
|
12
22
|
import { orderBoxes, parseChoice } from '../lib/boxes.mjs';
|
|
13
|
-
import { boxFlag, findCli, installCli, isOnboarding, runCli } from '../lib/cli.mjs';
|
|
23
|
+
import { boxFlag, checkCli, findCli, installCli, installCommand, isOnboarding, real, runCli } from '../lib/cli.mjs';
|
|
14
24
|
import { callbackHandler } from '../lib/login.mjs';
|
|
15
25
|
import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
|
|
16
26
|
|
|
@@ -43,8 +53,9 @@ function usage() {
|
|
|
43
53
|
npx instabox@latest sign in (first time), pick one of your boxes or start a new
|
|
44
54
|
one, SSH into it, and install the instabox CLI and agent skills
|
|
45
55
|
--box NAME go straight to that box instead of asking
|
|
46
|
-
--github sign in with GitHub instead of Google
|
|
47
|
-
|
|
56
|
+
--github sign in with GitHub instead of Google (a separate account;
|
|
57
|
+
instabox login uses GitHub, --provider google for Google)
|
|
58
|
+
--no-cli do not install or update the instabox CLI
|
|
48
59
|
--no-skills do not install the instabox agent skills
|
|
49
60
|
npx instabox@latest <command> run an instabox CLI command (instabox help lists them)
|
|
50
61
|
`);
|
|
@@ -184,8 +195,8 @@ async function waitOp(op) {
|
|
|
184
195
|
return op;
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
// The box to enter, null for a new one, or { listed:
|
|
188
|
-
// which one
|
|
198
|
+
// The box to enter, null for a new one, or { listed: true } when there is no terminal to ask
|
|
199
|
+
// which one (an agent running it): the boxes are listed and nothing is picked for it.
|
|
189
200
|
async function chooseBox() {
|
|
190
201
|
const { machines = [] } = await client
|
|
191
202
|
.call('GET', '/v1/machines?limit=100')
|
|
@@ -202,7 +213,7 @@ async function chooseBox() {
|
|
|
202
213
|
` ${ask ? `${i + 1}) ` : ''}${b.name.padEnd(w)} ${b.state.padEnd(9)} ${dim(`${b.template?.id || ''} created ${new Date(b.createdAt).toLocaleString()}`)}`,
|
|
203
214
|
),
|
|
204
215
|
);
|
|
205
|
-
if (!ask) return { listed:
|
|
216
|
+
if (!ask) return { listed: true };
|
|
206
217
|
console.log(` ${boxes.length + 1}) ${green('+ new box')}`);
|
|
207
218
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
208
219
|
try {
|
|
@@ -248,7 +259,31 @@ async function newBox() {
|
|
|
248
259
|
// Runs while the box starts; never fatal. Returns a line to print, or null.
|
|
249
260
|
async function ensureCli() {
|
|
250
261
|
if (process.argv.includes('--no-cli')) return null;
|
|
251
|
-
|
|
262
|
+
const found = findCli();
|
|
263
|
+
// The installer is a sh script: on Windows an existing CLI is only reported, never checked.
|
|
264
|
+
if (found && process.platform === 'win32') return `instabox CLI already installed ${dim('· try')} instabox ls`;
|
|
265
|
+
if (found) {
|
|
266
|
+
const { state, version } = await checkCli(found, API);
|
|
267
|
+
const v = version ? ` ${version}` : '';
|
|
268
|
+
if (state === 'current') return `instabox CLI${v} is up to date ${dim('· try')} instabox ls`;
|
|
269
|
+
if (state === 'dev')
|
|
270
|
+
return `instabox CLI at ${found} is a development build (${version}), left as is ${dim('· for the release:')} ${installCommand(API)}`;
|
|
271
|
+
if (state === 'unknown')
|
|
272
|
+
return `instabox CLI${v} already installed, could not check for an update ${dim('· to update:')} ${installCommand(API)}`;
|
|
273
|
+
// stale: replace it where it is, the way a first install does. Through a symlink, replace
|
|
274
|
+
// the file it points to, so the link (and whatever manages it) stays as it was.
|
|
275
|
+
const dir = dirname(real(found));
|
|
276
|
+
try {
|
|
277
|
+
accessSync(dir, fsConstants.W_OK);
|
|
278
|
+
} catch {
|
|
279
|
+
// Installing anywhere else would leave the old copy first on PATH.
|
|
280
|
+
return `instabox CLI${v} at ${found} is out of date and ${dir} is not writable ${dim('· to update it there:')} ${installCommand(API, dir)}`;
|
|
281
|
+
}
|
|
282
|
+
const r = await installCli(API, { dir });
|
|
283
|
+
if (r.error)
|
|
284
|
+
return `instabox CLI${v} is out of date and could not be updated ${dim(`(${r.error}) · to update:`)} ${installCommand(API)}`;
|
|
285
|
+
return `instabox CLI updated${v ? ` from${v}` : ''}${r.version ? ` to ${r.version}` : ''} at ${found} ${dim('· try')} instabox ls`;
|
|
286
|
+
}
|
|
252
287
|
if (process.platform === 'win32') return null;
|
|
253
288
|
const r = await installCli(API);
|
|
254
289
|
if (r.error) return `instabox CLI not installed ${dim(`(${r.error})`)}`;
|
|
@@ -270,21 +305,39 @@ function installSkills() {
|
|
|
270
305
|
return null;
|
|
271
306
|
}
|
|
272
307
|
const roots = [join(homedir(), '.claude', 'skills'), join(homedir(), '.agents', 'skills')];
|
|
273
|
-
|
|
308
|
+
// A skill counts as updated if any of its copies changed, else installed if one was new.
|
|
309
|
+
const outcome = new Map();
|
|
310
|
+
const rank = { unchanged: 0, installed: 1, updated: 2 };
|
|
311
|
+
const kept = new Set();
|
|
274
312
|
for (const root of roots) {
|
|
275
313
|
for (const n of names) {
|
|
276
314
|
const dest = join(root, n, 'SKILL.md');
|
|
277
315
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
316
|
+
const body = readFileSync(join(src, n, 'SKILL.md'), 'utf8');
|
|
317
|
+
let what = 'installed';
|
|
318
|
+
if (existsSync(dest)) {
|
|
319
|
+
const old = readFileSync(dest, 'utf8');
|
|
320
|
+
if (!old.includes('installed-by: instabox')) {
|
|
321
|
+
kept.add(n);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
what = old === body ? 'unchanged' : 'updated';
|
|
325
|
+
}
|
|
326
|
+
if (what !== 'unchanged') {
|
|
327
|
+
mkdirSync(join(root, n), { recursive: true });
|
|
328
|
+
writeFileSync(dest, body);
|
|
329
|
+
}
|
|
330
|
+
if (rank[what] > (rank[outcome.get(n)] ?? -1)) outcome.set(n, what);
|
|
282
331
|
} catch {}
|
|
283
332
|
}
|
|
284
333
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
334
|
+
const of = (what) => names.filter((n) => outcome.get(n) === what);
|
|
335
|
+
const parts = [];
|
|
336
|
+
if (of('updated').length) parts.push(`updated ${of('updated').join(', ')}`);
|
|
337
|
+
if (of('installed').length) parts.push(`installed ${of('installed').join(', ')}`);
|
|
338
|
+
if (of('unchanged').length) parts.push(`${of('unchanged').join(', ')} already up to date`);
|
|
339
|
+
if (kept.size) parts.push(`kept your own ${[...kept].join(', ')}`);
|
|
340
|
+
return parts.length ? `agent skills: ${parts.join('; ')} ${dim('(~/.claude/skills, ~/.agents/skills)')}` : null;
|
|
288
341
|
}
|
|
289
342
|
|
|
290
343
|
// Waits for the CLI install started earlier, installs the skills, and prints a line for each.
|
|
@@ -308,9 +361,8 @@ async function sh() {
|
|
|
308
361
|
const picked = await chooseBox();
|
|
309
362
|
if (picked?.listed) {
|
|
310
363
|
await reportInstalls(cli);
|
|
311
|
-
console.log(
|
|
312
|
-
|
|
313
|
-
);
|
|
364
|
+
console.log(`\n New box: ${bold('instabox create --name NAME')}`);
|
|
365
|
+
console.log(` Or enter one listed above: ${bold('npx instabox@latest --box NAME')}\n`);
|
|
314
366
|
return;
|
|
315
367
|
}
|
|
316
368
|
// Only entering a box needs OpenSSH; the list-and-stop path above runs without it.
|
package/lib/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The Go instabox CLI: find it, install it, hand it the command line.
|
|
2
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
2
|
+
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { createReadStream, realpathSync, statSync } from 'node:fs';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
import { delimiter, join, sep } from 'node:path';
|
|
6
7
|
|
|
@@ -24,7 +25,7 @@ export function boxFlag(args) {
|
|
|
24
25
|
return eq ? eq.slice('--box='.length) : null;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const real = (p) => {
|
|
28
|
+
export const real = (p) => {
|
|
28
29
|
try {
|
|
29
30
|
return realpathSync(p);
|
|
30
31
|
} catch {
|
|
@@ -65,11 +66,73 @@ export function findCli({ env = process.env, self = process.argv[1] } = {}) {
|
|
|
65
66
|
// The URL is "$1", never script text; a failed download stops before sh runs anything.
|
|
66
67
|
const INSTALL = 's=$(curl -fsSL "$1/install.sh") && printf \'%s\\n\' "$s" | sh';
|
|
67
68
|
|
|
68
|
-
//
|
|
69
|
-
|
|
69
|
+
// The command a person runs to install or update the CLI themselves; with dir, one that puts it
|
|
70
|
+
// in that directory (for a directory they cannot write to without sudo).
|
|
71
|
+
export const installCommand = (api, dir) =>
|
|
72
|
+
dir ? `curl -fsSL ${api}/install.sh | sudo env INSTABOX_INSTALL_DIR=${dir} sh` : `curl -fsSL ${api}/install.sh | sh`;
|
|
73
|
+
|
|
74
|
+
// What `instabox --version` prints after "instabox ", or null when it does not run. Async, so a
|
|
75
|
+
// slow or hung binary does not hold up the rest of onboarding.
|
|
76
|
+
export function cliVersion(path) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
execFile(path, ['--version'], { encoding: 'utf8', timeout: 10_000 }, (err, stdout) => {
|
|
79
|
+
if (err) return resolve(null);
|
|
80
|
+
const m = String(stdout)
|
|
81
|
+
.trim()
|
|
82
|
+
.match(/^instabox (\S+)$/);
|
|
83
|
+
resolve(m ? m[1] : null);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The file's sha256, streamed: the CLI is tens of MB and this runs while the box starts.
|
|
89
|
+
function sha256File(path) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const h = createHash('sha256');
|
|
92
|
+
createReadStream(path)
|
|
93
|
+
.on('error', reject)
|
|
94
|
+
.on('data', (d) => h.update(d))
|
|
95
|
+
.on('end', () => resolve(h.digest('hex')));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Whether the CLI at path is the build the server's /install.sh would install. Release builds
|
|
100
|
+
// are named after a commit (scripts/build-release.sh), which says nothing about which is newer,
|
|
101
|
+
// so the check is by content: the binary's sha256 against the server's SHA256SUMS.
|
|
102
|
+
// current: one of the server's builds
|
|
103
|
+
// dev: a local build (`go build` leaves "dev"; build-cli.sh marks a dirty tree "-dirty"),
|
|
104
|
+
// which is not ours to replace
|
|
105
|
+
// stale: a release build the server no longer serves
|
|
106
|
+
// unknown: the CLI does not run, or SHA256SUMS could not be read
|
|
107
|
+
export async function checkCli(path, api, { fetch = globalThis.fetch } = {}) {
|
|
108
|
+
const version = await cliVersion(path);
|
|
109
|
+
if (!version) return { state: 'unknown', version };
|
|
110
|
+
if (version === 'dev' || version.endsWith('-dirty')) return { state: 'dev', version };
|
|
111
|
+
let sums;
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(`${api}/dl/SHA256SUMS`, { signal: AbortSignal.timeout(10_000) });
|
|
114
|
+
if (!res.ok) return { state: 'unknown', version };
|
|
115
|
+
sums = new Set((await res.text()).match(/\b[0-9a-f]{64}\b/g) || []);
|
|
116
|
+
} catch {
|
|
117
|
+
return { state: 'unknown', version };
|
|
118
|
+
}
|
|
119
|
+
if (!sums.size) return { state: 'unknown', version };
|
|
120
|
+
let mine;
|
|
121
|
+
try {
|
|
122
|
+
mine = await sha256File(path);
|
|
123
|
+
} catch {
|
|
124
|
+
return { state: 'unknown', version };
|
|
125
|
+
}
|
|
126
|
+
return { state: sums.has(mine) ? 'current' : 'stale', version };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// path is null, never a bare `instabox` that could resolve back to this script. dir replaces the
|
|
130
|
+
// CLI in that directory (INSTABOX_INSTALL_DIR), so an update lands where the old one is found.
|
|
131
|
+
export function installCli(api, { dir } = {}) {
|
|
70
132
|
return new Promise((resolve) => {
|
|
71
133
|
let out = '';
|
|
72
|
-
const
|
|
134
|
+
const env = dir ? { ...process.env, INSTABOX_INSTALL_DIR: dir } : process.env;
|
|
135
|
+
const p = spawn('sh', ['-c', INSTALL, 'sh', api], { stdio: ['ignore', 'pipe', 'pipe'], env });
|
|
73
136
|
p.stdout.on('data', (d) => {
|
|
74
137
|
out += d;
|
|
75
138
|
});
|
|
@@ -79,8 +142,10 @@ export function installCli(api) {
|
|
|
79
142
|
p.on('error', (e) => resolve({ error: e.message }));
|
|
80
143
|
p.on('close', (code) => {
|
|
81
144
|
if (code !== 0) return resolve({ error: out.trim().split('\n').pop() || 'installer failed' });
|
|
145
|
+
const installed = out.match(/^Installed (.+) \(([^()]*), sha256 [0-9a-f]+\)$/m) || [];
|
|
82
146
|
resolve({
|
|
83
|
-
path:
|
|
147
|
+
path: installed[1] || findCli(),
|
|
148
|
+
version: installed[2]?.replace(/^instabox /, '') || null,
|
|
84
149
|
pathHint: (out.match(/export PATH="[^"]+"/) || [])[0] || null,
|
|
85
150
|
});
|
|
86
151
|
});
|
package/package.json
CHANGED
package/skills/instabox/SKILL.md
CHANGED
|
@@ -13,11 +13,23 @@ An instabox sandbox is a Linux VM (Debian, user `dev`, sudo). It has a root disk
|
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
15
|
instabox whoami # who is signed in (fails if nobody)
|
|
16
|
-
instabox login # browser sign-in
|
|
16
|
+
instabox login # browser sign-in with GitHub; --provider google for Google
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
A GitHub sign-in and a Google sign-in are separate accounts with separate boxes, so sign in the way the user did. `npx instabox@latest` signs in with Google unless given `--github`, and the CLI shares its sign-in.
|
|
20
|
+
|
|
19
21
|
For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it on the dashboard's API Keys page). Never print, commit or log a key.
|
|
20
22
|
|
|
23
|
+
## Templates
|
|
24
|
+
|
|
25
|
+
`universal` is the default, so `create` needs no `--template` for ordinary work. It is Debian with git, gh, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker, Claude Code and Codex; a desktop (VNC, Chromium) installs itself the first time it is used. Pick another template only for its one purpose:
|
|
26
|
+
|
|
27
|
+
- `agent-browser`: universal with the desktop already installed, for browsing or driving a GUI from the start.
|
|
28
|
+
- `herdr`: for the herdr client.
|
|
29
|
+
- `orca`: for Orca desktop and mobile.
|
|
30
|
+
|
|
31
|
+
`instabox templates` lists the ones this server offers.
|
|
32
|
+
|
|
21
33
|
## Everyday commands
|
|
22
34
|
|
|
23
35
|
```sh
|
|
@@ -35,13 +47,33 @@ instabox rm work # delete the box
|
|
|
35
47
|
```
|
|
36
48
|
|
|
37
49
|
- Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
|
|
38
|
-
- `create` options: `--template T` or `--snapshot S`, `--
|
|
39
|
-
- `exec`
|
|
40
|
-
-
|
|
41
|
-
-
|
|
50
|
+
- `create` options: `--template T` or `--snapshot S`, `--timeout D [--on-timeout pause|kill]`, `--meta K=V`, `--no-vault`, `--no-wait`. Every box is the same size (`instabox usage` shows it); there are no size flags.
|
|
51
|
+
- Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
|
|
52
|
+
- `exec` returns the command's exit code and takes `--stdin` to pipe input. It stops the command after `--timeout` seconds: 60 by default, 600 at most. Run anything longer (installs, builds, test suites) over SSH, which has no time limit and also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`.
|
|
53
|
+
- The box's HOME has `MACHINE.md` (which box it is, its size, what survives a stop, the installed tools) and `AGENTS.md`. Both are worth reading before working in the box; they appear a moment after it starts.
|
|
54
|
+
- A public URL for a web server in the box: `instabox port publish work 3000` prints it (`instabox port ls`, `instabox port unpublish work 3000`). The URL works while the box runs.
|
|
55
|
+
|
|
56
|
+
## Vault: the user's secrets in every box
|
|
57
|
+
|
|
58
|
+
The user's Vault holds their secrets (API keys for model providers, a `.env`, a service-account file) on the server and writes them into every box of theirs when it boots or resumes: `env` items as environment variables of every login shell (so of `instabox exec` too), `file` items as files under HOME.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
instabox vault show work # what the box gets (names, never values) and the last write
|
|
62
|
+
instabox vault push work # write the current items into a running box now
|
|
63
|
+
instabox vault set ANTHROPIC_API_KEY # value from stdin, or prompted without echo (sign-in only)
|
|
64
|
+
instabox vault set-file .config/svc.json --from ./svc.json
|
|
65
|
+
instabox vault ls; instabox vault rm NAME
|
|
66
|
+
instabox create --name tenant --no-vault # a box for someone else: never gets the user's items
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Before asking the user for a credential, check names with `instabox vault show <box>`. Inside the box, use silent presence checks: `test "${NAME+x}" = x` for an environment variable (replace `NAME` with its name), or `test -f "$HOME/.config/svc.json"` for a credential file. Exit status 0 means present, 1 means absent. Never print credential values or read credential-file contents into agent output, chat or logs.
|
|
70
|
+
- A changed item reaches the box at its next boot or resume, or at once with `vault push` (a changed `env` item reaches new shells only).
|
|
71
|
+
- A box made for another person or tenant must be created with `--no-vault` (`vault: false` in the API): anything running in it can read what the Vault writes. This is for good; `instabox start|resume <box> --no-vault` does the same to an existing box.
|
|
72
|
+
- An item's selector (`--selector K=V`) limits it to boxes whose metadata has those labels: create the box with the `--meta` the user's items expect.
|
|
73
|
+
- Managing items needs a sign-in; an API key cannot read, create or change them. Never ask for a secret's value to put it somewhere else.
|
|
42
74
|
|
|
43
75
|
## Habits
|
|
44
76
|
|
|
45
77
|
- Give boxes a `--timeout` so forgotten ones pause or delete themselves.
|
|
46
|
-
-
|
|
78
|
+
- When the user asks for a new box, create one, with a name `instabox ls` does not already show. Offer an existing box only when they want to continue earlier work in it.
|
|
47
79
|
- `instabox usage` shows storage use and limits. A quota error means delete or stop something; do not retry the same create in a loop.
|
|
@@ -24,17 +24,17 @@ Before starting, tell the user plainly:
|
|
|
24
24
|
```bash
|
|
25
25
|
instabox whoami # signed in? otherwise: instabox login
|
|
26
26
|
git rev-parse --show-toplevel # must be inside a git repo
|
|
27
|
-
du -sh "$(git rev-parse --show-toplevel)/.git" #
|
|
27
|
+
du -sh "$(git rev-parse --show-toplevel)/.git" # must fit on /data (instabox usage shows the size)
|
|
28
28
|
instabox ls # pick a box name that is not taken
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
If `.git` is a file (a git worktree or submodule checkout), move the main checkout instead. Submodules: their history comes along inside `.git/modules`; run `git submodule update --init` on the box. Uncommitted changes inside a submodule are not copied.
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
instabox create --name <box>
|
|
34
|
+
instabox create --name <box>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Box names are lowercase letters, digits and `-`. The default template (`universal`) has git, node/npm, python3/uv, go, cargo and docker. Do not add a kill timeout: this box is meant to stay (offer `instabox timeout <box> 8h --on-timeout pause` if the user wants it to pause itself).
|
|
37
|
+
Box names are lowercase letters, digits and `-`. Every box is the same size. The default template (`universal`) has git, node/npm, python3/uv, go, cargo and docker. Do not add a kill timeout: this box is meant to stay (offer `instabox timeout <box> 8h --on-timeout pause` if the user wants it to pause itself).
|
|
38
38
|
|
|
39
39
|
## 2. Copy the repo and history
|
|
40
40
|
|
|
@@ -18,7 +18,7 @@ instabox snapshot save work --name work-ready --description "deps installed, rep
|
|
|
18
18
|
- A running box is saved live, without stopping it: the snapshot is `pending` in `instabox snapshot ls` until its disks finish uploading (`--no-wait` returns at once).
|
|
19
19
|
- Do not stop the box first: `stop` ends every process in it. A live snapshot is like a clean power cut: what is on disk is kept, what programs hold only in memory is not. Only if the server answers that this box must be stopped, or if a program must write its data to disk first (a database mid-write), run `instabox stop work`, save, then `instabox start work`.
|
|
20
20
|
- `--root-only` saves just the root disk (smaller; `/data` is not included).
|
|
21
|
-
- A snapshot includes credentials stored under HOME (gh, Claude, Codex, git, SSH keys). Sign out of tools or remove secrets first if the snapshot will be shared or kept long.
|
|
21
|
+
- A snapshot includes credentials stored under HOME (gh, Claude, Codex, git, SSH keys), and the Vault's `file` items (its `env` items live on tmpfs and are not saved). Sign out of tools or remove secrets first if the snapshot will be shared or kept long.
|
|
22
22
|
- Pick names that say what is inside (`node20-playwright`, `api-db-seeded`); names must be unique among your snapshots.
|
|
23
23
|
|
|
24
24
|
## Load
|