instabox 0.6.3 → 0.7.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/README.md +3 -3
- package/bin/instabox.mjs +23 -102
- package/lib/cli.mjs +1 -1
- package/lib/login.mjs +37 -36
- package/package.json +1 -1
- package/skills/instabox/SKILL.md +33 -27
- package/skills/instabox-basic-setup/SKILL.md +16 -23
- package/skills/instabox-basic-setup/scripts/content.mjs +6 -22
- package/skills/instabox-basic-setup/scripts/lib.mjs +49 -68
- package/skills/instabox-basic-setup/scripts/plan.mjs +17 -48
- package/skills/instabox-basic-setup/scripts/push.mjs +29 -32
- package/skills/instabox-basic-setup/scripts/scan.mjs +41 -32
- package/skills/instabox-move-repo/SKILL.md +7 -30
- package/skills/instabox-move-repo/scripts/deps.sh +1 -1
- package/skills/instabox-move-repo/scripts/repo-files.mjs +2 -2
- package/skills/instabox-snapshots/SKILL.md +4 -4
package/README.md
CHANGED
|
@@ -5,17 +5,17 @@ npx instabox@latest # sign in, install the CLI and the agent skills
|
|
|
5
5
|
npx instabox@latest sh # the same, then start a new box and SSH into it
|
|
6
6
|
```
|
|
7
7
|
|
|
8
|
-
`npx instabox@latest` signs you in to instabox
|
|
8
|
+
`npx instabox@latest` signs you in to instabox with a device code (first run only: open the page it prints on any device and enter the code; it opens the page for you where there is a browser, never with `--no-browser`), installs the `instabox` CLI already signed in or updates an out-of-date one (skip with `--no-cli`), installs or updates the agent skills from [`../skills`](../skills) in `~/.claude/skills` and `~/.agents/skills` (skip with `--no-skills`), and prints the next steps. It does not touch your boxes.
|
|
9
9
|
|
|
10
10
|
`npx instabox@latest sh` does all that, then starts a new box (`box-<hex>`) and drops you into it over SSH, no questions. Run by an agent (no terminal), it starts the box and prints `instabox ssh <box> -- <command>` instead of opening a shell. At the box limit it starts nothing and lists your boxes, to remove one (`instabox rm <box>`) or open one (`instabox ssh <box>`).
|
|
11
11
|
|
|
12
|
-
A GitHub and a Google sign-in are separate instabox accounts, each with its own boxes. Both this entry point and `instabox login` let the
|
|
12
|
+
A GitHub and a Google sign-in are separate instabox accounts, each with its own boxes. Both this entry point and `instabox login` let the sign-in page offer both. `--google` asks for Google directly, as does `instabox login --provider google`.
|
|
13
13
|
|
|
14
14
|
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.
|
|
15
15
|
|
|
16
16
|
## How it works
|
|
17
17
|
|
|
18
|
-
- `bin/instabox.mjs` runs instabox's
|
|
18
|
+
- `bin/instabox.mjs` runs instabox's device-code sign-in (RFC 8628, `POST /v1/auth/device`, then polls `/v1/auth/device/token`), as `instabox login` does, 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.
|
|
19
19
|
- `sh` always starts a new box, with no timeout and no idle auto-suspend of its own. An existing box is entered with the CLI: `instabox ssh <box>`.
|
|
20
20
|
- 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.
|
|
21
21
|
- 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.
|
package/bin/instabox.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// npx instabox@latest: sign in + CLI + skills; `sh` also starts a new box and SSHes in. Anything else goes to the CLI.
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import {
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
5
|
import {
|
|
6
6
|
accessSync,
|
|
7
7
|
constants as fsConstants,
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
mkdirSync,
|
|
14
14
|
readdirSync,
|
|
15
15
|
} from 'node:fs';
|
|
16
|
-
import http from 'node:http';
|
|
17
16
|
import { createInterface } from 'node:readline/promises';
|
|
18
17
|
import { tmpdir, homedir } from 'node:os';
|
|
19
18
|
import { join, dirname } from 'node:path';
|
|
@@ -22,7 +21,7 @@ import { createClient } from '../lib/api.mjs';
|
|
|
22
21
|
import { orderBoxes } from '../lib/boxes.mjs';
|
|
23
22
|
import { guestUserNote, localGuestUser } from '../lib/guestuser.mjs';
|
|
24
23
|
import { checkCli, findCli, installCli, installCommand, isOnboarding, real, runCli } from '../lib/cli.mjs';
|
|
25
|
-
import { browserCommand,
|
|
24
|
+
import { browserCommand, canOpenBrowser, deviceLogin } from '../lib/login.mjs';
|
|
26
25
|
import { makeDirPrivate, sessionArgs } from '../lib/ssh.mjs';
|
|
27
26
|
import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
|
|
28
27
|
import { agentName, track } from '../lib/telemetry.mjs';
|
|
@@ -38,9 +37,10 @@ const API = (
|
|
|
38
37
|
const IMAGE = process.env.INSTABOX_IMAGE || 'agent-browser';
|
|
39
38
|
// SSH certificate lifetime for a box with no timeout of its own.
|
|
40
39
|
const CERT_MIN = 120;
|
|
41
|
-
// With no provider selected, the
|
|
42
|
-
// or offers sign-in. Explicit flags still start the chosen provider directly.
|
|
40
|
+
// With no provider selected, the sign-in page offers GitHub or Google; --google asks for Google's.
|
|
43
41
|
const PROVIDER = process.argv.includes('--google') ? 'google' : process.argv.includes('--github') ? 'github' : '';
|
|
42
|
+
// The code is always printed; --no-browser only stops opening the sign-in page.
|
|
43
|
+
const NO_BROWSER = process.argv.includes('--no-browser');
|
|
44
44
|
const bold = (s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s);
|
|
45
45
|
const dim = (s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
|
|
46
46
|
const green = (s) => (process.stdout.isTTY ? `\x1b[32m${s}\x1b[0m` : s);
|
|
@@ -56,10 +56,11 @@ function usage() {
|
|
|
56
56
|
console.log(`
|
|
57
57
|
${bold('instabox')}: a cloud box in seconds
|
|
58
58
|
|
|
59
|
-
npx instabox@latest sign in (first time), install the instabox
|
|
59
|
+
npx instabox@latest sign in with a device code (first time), install the instabox
|
|
60
|
+
CLI and agent skills
|
|
60
61
|
npx instabox@latest sh the same, then start a new box and SSH into it
|
|
61
|
-
--github, --google sign in
|
|
62
|
-
|
|
62
|
+
--github, --google sign in with that provider
|
|
63
|
+
--no-browser only print the sign-in page and code; do not open a browser
|
|
63
64
|
--no-cli do not install or update the instabox CLI
|
|
64
65
|
--no-skills do not install the instabox agent skills
|
|
65
66
|
npx instabox@latest <command> run an instabox CLI command (instabox help lists them)
|
|
@@ -89,54 +90,6 @@ function openBrowser(url) {
|
|
|
89
90
|
} catch {}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// The page the browser lands on after sign-in (served from the loopback listener).
|
|
93
|
-
function signInPage(ok, title, text, who) {
|
|
94
|
-
const esc = (s) =>
|
|
95
|
-
String(s ?? '').replace(
|
|
96
|
-
/[&<>"']/g,
|
|
97
|
-
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c],
|
|
98
|
-
);
|
|
99
|
-
const accent = ok ? '#6ee7b7' : '#f87171';
|
|
100
|
-
const icon = ok ? '<path d="M7 12.5l3.2 3.2L17 9" />' : '<path d="M8 8l8 8M16 8l-8 8" />';
|
|
101
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
102
|
-
<title>${esc(title)} · instabox</title>
|
|
103
|
-
<style>
|
|
104
|
-
:root { --bg:#0b0b0c; --panel:#141416; --line:#26262a; --fg:#ededed; --dim:#8b8b92; --accent:${accent}; }
|
|
105
|
-
* { box-sizing:border-box; }
|
|
106
|
-
html, body { height:100%; margin:0; }
|
|
107
|
-
body { background:radial-gradient(1200px 600px at 50% -10%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 60%), var(--bg);
|
|
108
|
-
color:var(--fg); font:15px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif; display:grid; place-items:center; padding:24px 16px; }
|
|
109
|
-
.card { width:100%; max-width:420px; background:var(--panel); border:1px solid var(--line); border-radius:16px; padding:36px 28px 28px; text-align:center;
|
|
110
|
-
box-shadow:0 30px 80px rgba(0,0,0,.45); animation:rise .5s cubic-bezier(.2,.9,.3,1.1) both; }
|
|
111
|
-
.brand { font:600 13px ui-monospace,SFMono-Regular,Menlo,monospace; letter-spacing:.08em; color:var(--dim); margin-bottom:28px; }
|
|
112
|
-
.badge { width:64px; height:64px; margin:0 auto 20px; border-radius:50%; display:grid; place-items:center;
|
|
113
|
-
background:color-mix(in srgb, var(--accent) 14%, transparent); border:1px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
|
114
|
-
box-shadow:0 0 36px color-mix(in srgb, var(--accent) 35%, transparent); animation:pop .6s .15s cubic-bezier(.2,.9,.3,1.4) both; }
|
|
115
|
-
.badge svg { width:30px; height:30px; stroke:var(--accent); stroke-width:2.4; fill:none; stroke-linecap:round; stroke-linejoin:round;
|
|
116
|
-
stroke-dasharray:30; stroke-dashoffset:30; animation:draw .45s .45s ease-out forwards; }
|
|
117
|
-
h1 { font-size:22px; margin:0 0 6px; }
|
|
118
|
-
.who { color:var(--accent); font-weight:600; margin:0 0 10px; }
|
|
119
|
-
p { color:var(--dim); margin:0; }
|
|
120
|
-
.hint { margin-top:24px; padding-top:18px; border-top:1px solid var(--line); font-size:13px; color:var(--dim); }
|
|
121
|
-
kbd { white-space:nowrap; font:12px ui-monospace,SFMono-Regular,Menlo,monospace; background:var(--bg); border:1px solid var(--line); border-radius:6px; padding:2px 6px; color:var(--fg); }
|
|
122
|
-
@keyframes rise { from { opacity:0; transform:translateY(12px); } }
|
|
123
|
-
@keyframes pop { from { opacity:0; transform:scale(.4); } }
|
|
124
|
-
@keyframes draw { to { stroke-dashoffset:0; } }
|
|
125
|
-
@media (prefers-reduced-motion: reduce) { .card, .badge, .badge svg { animation:none; stroke-dashoffset:0; } }
|
|
126
|
-
</style></head><body>
|
|
127
|
-
<main class="card">
|
|
128
|
-
<div class="brand">INSTABOX</div>
|
|
129
|
-
<div class="badge"><svg viewBox="0 0 24 24" aria-hidden="true">${icon}</svg></div>
|
|
130
|
-
<h1>${esc(title)}</h1>
|
|
131
|
-
${who ? `<p class="who">Signed in as ${esc(who)}</p>` : ''}
|
|
132
|
-
<p>${esc(text)}</p>
|
|
133
|
-
${ok ? '<div class="hint">You can close this tab. Next time, <kbd>npx instabox@latest</kbd> skips this step.</div>' : ''}
|
|
134
|
-
</main>
|
|
135
|
-
</body></html>`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Browser sign-in, the same loopback flow as instabox's own CLI (RFC 8252 + PKCE): listen on
|
|
139
|
-
// 127.0.0.1, open /auth/cli/start, receive the code at /callback, exchange it with the verifier.
|
|
140
93
|
// askInvite asks for an invite code on the terminal; '' without one (a pipe, CI) or when left empty.
|
|
141
94
|
async function askInvite(again) {
|
|
142
95
|
if (!process.stdin.isTTY || !process.stdout.isTTY) return '';
|
|
@@ -151,11 +104,22 @@ async function askInvite(again) {
|
|
|
151
104
|
}
|
|
152
105
|
}
|
|
153
106
|
|
|
154
|
-
// signInWithInvite is
|
|
107
|
+
// signInWithInvite is a device-code sign-in, asking for an invite code when the account is on the waitlist.
|
|
155
108
|
async function signInWithInvite() {
|
|
156
109
|
let invite = '';
|
|
157
110
|
for (;;) {
|
|
158
|
-
const r = await
|
|
111
|
+
const r = await deviceLogin({
|
|
112
|
+
anon: client.anon,
|
|
113
|
+
invite,
|
|
114
|
+
provider: PROVIDER,
|
|
115
|
+
show: (ds) => {
|
|
116
|
+
console.log(` ${bold('Sign in to instabox')}: open ${ds.verificationUri}`);
|
|
117
|
+
console.log(` and enter the code ${bold(ds.userCode)}\n`);
|
|
118
|
+
console.log(` ${dim('Waiting for you to sign in…')}`);
|
|
119
|
+
},
|
|
120
|
+
open: NO_BROWSER || !canOpenBrowser() ? null : openBrowser,
|
|
121
|
+
sleep,
|
|
122
|
+
});
|
|
159
123
|
if (r.tok) return r.tok;
|
|
160
124
|
const waitlisted = r.code === 'invite_required' || r.code === 'invite_invalid';
|
|
161
125
|
if (waitlisted) invite = await askInvite(r.code === 'invite_invalid');
|
|
@@ -168,50 +132,7 @@ async function signInWithInvite() {
|
|
|
168
132
|
}
|
|
169
133
|
}
|
|
170
134
|
|
|
171
|
-
|
|
172
|
-
const verifier = randomBytes(32).toString('base64url');
|
|
173
|
-
const state = randomBytes(24).toString('base64url');
|
|
174
|
-
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
175
|
-
let finish;
|
|
176
|
-
const done = new Promise((r) => {
|
|
177
|
-
finish = r;
|
|
178
|
-
});
|
|
179
|
-
const exchange = async (code) => {
|
|
180
|
-
const tok = await client.anon('POST', '/v1/auth/cli/token', {
|
|
181
|
-
code,
|
|
182
|
-
codeVerifier: verifier,
|
|
183
|
-
redirectUri: redirect,
|
|
184
|
-
});
|
|
185
|
-
client.use(tok);
|
|
186
|
-
return { tok, me: await client.call('GET', '/v1/me').catch(() => null) };
|
|
187
|
-
};
|
|
188
|
-
const page = {
|
|
189
|
-
stale: () => signInPage(false, 'This sign-in link is stale', 'Run the command in your terminal again.'),
|
|
190
|
-
again: () => signInPage(true, 'Already signing you in', 'Head back to your terminal.'),
|
|
191
|
-
ok: (r) => signInPage(true, "You're in", 'Head back to your terminal.', r.me && (r.me.name || r.me.login)),
|
|
192
|
-
failed: (err, code) =>
|
|
193
|
-
code === 'invite_required' || code === 'invite_invalid'
|
|
194
|
-
? signInPage(false, "You're on the waitlist", 'Return to your terminal to enter an invite code.')
|
|
195
|
-
: signInPage(false, 'Sign-in failed', `${err}. Return to your terminal and run the command again.`),
|
|
196
|
-
};
|
|
197
|
-
const server = http.createServer(callbackHandler({ state, exchange, finish, page }));
|
|
198
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
199
|
-
const redirect = `http://127.0.0.1:${server.address().port}/callback`;
|
|
200
|
-
const q = { redirect_uri: redirect, state, code_challenge: challenge, code_challenge_method: 'S256' };
|
|
201
|
-
if (PROVIDER) q.provider = PROVIDER;
|
|
202
|
-
if (invite) q.invite = invite;
|
|
203
|
-
const start = `${API}/auth/cli/start?` + new URLSearchParams(q);
|
|
204
|
-
console.log(` ${bold('Sign in to instabox')} in your browser ${dim('(opening it now)')}`);
|
|
205
|
-
console.log(` ${dim('If it did not open:')} ${start}\n`);
|
|
206
|
-
openBrowser(start);
|
|
207
|
-
const timer = setTimeout(() => finish({ err: 'timed out waiting for the browser sign-in' }), 5 * 60_000);
|
|
208
|
-
const result = await done;
|
|
209
|
-
clearTimeout(timer);
|
|
210
|
-
server.close();
|
|
211
|
-
return result.err ? { err: result.err, code: result.code } : { tok: result.tok };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// The CLI's saved token, a refreshed one, or a browser sign-in; saved where the CLI reads it.
|
|
135
|
+
// The CLI's saved token, a refreshed one, or a device-code sign-in; saved where the CLI reads it.
|
|
215
136
|
async function signedIn() {
|
|
216
137
|
const c = loadCredentials(API);
|
|
217
138
|
if (c?.accessToken && new Date(c.accessExpiresAt) - Date.now() > 60_000) return c;
|
package/lib/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { createReadStream, mkdirSync, realpathSync, renameSync, rmSync, statSync
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join, sep } from 'node:path';
|
|
7
7
|
|
|
8
|
-
const OUR_FLAGS = new Set(['--github', '--google', '--no-cli', '--no-skills']);
|
|
8
|
+
const OUR_FLAGS = new Set(['--github', '--google', '--no-browser', '--no-cli', '--no-skills']);
|
|
9
9
|
|
|
10
10
|
// Bare `instabox` or `sh`, with only our flags; anything else is a CLI command.
|
|
11
11
|
export function isOnboarding(args) {
|
package/lib/login.mjs
CHANGED
|
@@ -1,40 +1,41 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// Device-code sign-in (RFC 8628), as `instabox login` does it: POST /v1/auth/device for a code,
|
|
2
|
+
// show it, then poll /v1/auth/device/token until the user approves, denies, or the code expires.
|
|
3
|
+
export async function deviceLogin({ anon, invite = '', provider = '', show, open, sleep, now = Date.now }) {
|
|
4
|
+
const body = {};
|
|
5
|
+
if (invite) body.invite = invite;
|
|
6
|
+
// GitHub is the server's default device sign-in; only WorkOS's offers Google.
|
|
7
|
+
if (provider === 'google' || provider === 'workos') body.provider = provider;
|
|
8
|
+
let ds;
|
|
9
|
+
try {
|
|
10
|
+
ds = await anon('POST', '/v1/auth/device', body);
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return { err: e.message, code: e.code };
|
|
13
|
+
}
|
|
14
|
+
if (body.provider && ds.provider !== 'workos')
|
|
15
|
+
return { err: "this server's device sign-in supports GitHub only: use --github" };
|
|
16
|
+
show(ds);
|
|
17
|
+
if (open) open(ds.verificationUriComplete || ds.verificationUri);
|
|
18
|
+
let interval = (Number.isFinite(ds.interval) && ds.interval >= 0 ? ds.interval : 5) * 1000;
|
|
19
|
+
const deadline = now() + (ds.expiresIn > 0 ? ds.expiresIn : 900) * 1000;
|
|
20
|
+
for (;;) {
|
|
21
|
+
await sleep(interval);
|
|
22
|
+
try {
|
|
23
|
+
return { tok: await anon('POST', '/v1/auth/device/token', { deviceCode: ds.deviceCode }) };
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e.code === 'slow_down') interval += 5000;
|
|
26
|
+
else if (e.code === 'expired_token') return { err: 'the code expired; run the command again', code: e.code };
|
|
27
|
+
else if (e.code === 'access_denied') return { err: 'sign-in was denied', code: e.code };
|
|
28
|
+
else if (e.code !== 'authorization_pending') return { err: e.message, code: e.code };
|
|
10
29
|
}
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
taken = true;
|
|
21
|
-
let result;
|
|
22
|
-
if (u.searchParams.get('error')) {
|
|
23
|
-
result = {
|
|
24
|
-
err: u.searchParams.get('error_description') || u.searchParams.get('error'),
|
|
25
|
-
code: u.searchParams.get('error'),
|
|
26
|
-
};
|
|
27
|
-
} else {
|
|
28
|
-
try {
|
|
29
|
-
result = await exchange(u.searchParams.get('code'));
|
|
30
|
-
} catch (e) {
|
|
31
|
-
result = { err: e.message };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
res.writeHead(200, html);
|
|
35
|
-
res.end(result.tok ? page.ok(result) : page.failed(result.err, result.code));
|
|
36
|
-
finish(result);
|
|
37
|
-
};
|
|
30
|
+
if (now() > deadline) return { err: 'the code expired; run the command again', code: 'expired_token' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whether a local browser can show the sign-in page; the code is printed either way.
|
|
35
|
+
export function canOpenBrowser(env = process.env, platform = process.platform) {
|
|
36
|
+
const display = Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
37
|
+
if (env.SSH_CONNECTION && !display) return false;
|
|
38
|
+
return display || platform === 'darwin' || platform === 'win32';
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// The command that opens url in the browser. On Windows never `cmd /c start`: cmd ends the URL at its first &.
|
package/package.json
CHANGED
package/skills/instabox/SKILL.md
CHANGED
|
@@ -13,12 +13,12 @@ An instabox sandbox is a Linux VM (Debian, one user with passwordless sudo: name
|
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
15
|
instabox whoami # who is signed in (fails if nobody)
|
|
16
|
-
instabox login #
|
|
16
|
+
instabox login # device code: open the printed page, enter the code
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
A GitHub sign-in and a Google sign-in with the same verified email share one account; with different emails they are separate accounts with separate boxes. Both `instabox login` and `npx instabox@latest`
|
|
19
|
+
A GitHub sign-in and a Google sign-in with the same verified email share one account; with different emails they are separate accounts with separate boxes. Both `instabox login` and `npx instabox@latest` sign in with a device code: they print a page and a code (and open the page when there is a browser); the user opens it on any device, enters the code and chooses GitHub or Google there. It works the same over SSH or on a machine without a browser. Relay the page and the code to the user and wait; the command finishes on its own. For Google directly, use `instabox login --provider google`, or `npx instabox@latest --google`. The CLI shares the sign-in `npx` made.
|
|
20
20
|
|
|
21
|
-
For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it
|
|
21
|
+
For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it in the dashboard: API keys in the sidebar). Never print, commit or log a key.
|
|
22
22
|
|
|
23
23
|
## Templates
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ For CI or an agent without a browser, use an API key instead: `export INSTABOX_A
|
|
|
27
27
|
- `universal`: the same tools without the desktop, which installs itself the first time it is used (about half a minute); a smaller root disk. The dashboard shows it as **basic**.
|
|
28
28
|
- `herdr`: for the herdr client.
|
|
29
29
|
- `orca`: for Orca desktop and mobile.
|
|
30
|
+
- `t3`: for the T3 Code desktop and mobile clients.
|
|
30
31
|
|
|
31
32
|
`instabox templates` lists the ones this server offers. The free plan creates boxes from `agent-browser` only: another template is refused with `403 template_not_allowed` and needs a paid plan (the user upgrades on the dashboard). A box from a snapshot (`--snapshot`) works on every plan, whatever template the snapshot came from. Do not retry another template on the free plan; use `agent-browser`.
|
|
32
33
|
|
|
@@ -52,7 +53,9 @@ instabox ssh work # interactive shell (leave with:
|
|
|
52
53
|
instabox exec work -- uname -a # one command, no SSH client needed
|
|
53
54
|
instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
|
|
54
55
|
instabox files work /data/home # list a directory (or print a text file)
|
|
55
|
-
instabox auto-suspend work 10m # idle time before
|
|
56
|
+
instabox auto-suspend work 10m # idle time before a paid box pauses
|
|
57
|
+
instabox max-run work 120 # pause a paid box after two running hours
|
|
58
|
+
instabox max-run work -1 # remove the runtime cap; idle policy still applies
|
|
56
59
|
instabox stop work && instabox start work # stop ends processes, keeps disks
|
|
57
60
|
instabox suspend work && instabox resume work # pause/resume keeps running processes
|
|
58
61
|
instabox rename work api # rename in place, any state; ID and port URLs stay
|
|
@@ -60,41 +63,44 @@ instabox rm work # delete the box
|
|
|
60
63
|
```
|
|
61
64
|
|
|
62
65
|
- Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
|
|
63
|
-
- `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--
|
|
64
|
-
-
|
|
66
|
+
- `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--max-run-minutes N`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--no-wait`.
|
|
67
|
+
- Idle auto-pause defaults to 30 minutes. Paid boxes allow 5, 10, 15, 20, 25 or 30 minutes, or `0` to turn it off. Free boxes keep 30 minutes unless an admin grants an idle override. Idle means no agent working, no terminal use and little CPU.
|
|
68
|
+
- Max run minutes is independent of idle activity: `-1` is unlimited, `0` pauses immediately, and a positive integer forces a pause after that many running minutes. Free boxes have a fixed 480-minute cap that only an admin session can change; paid boxes default to `-1` and can change it. An idle override does not permit changes to a free box's runtime cap. The first limit reached pauses the box; both timers restart after sleep and wake.
|
|
69
|
+
- A runtime pause blocks every wake path during a server-configured cooldown, 10 minutes by default. Wait until the `cooldownUntil` deadline shown by `instabox get`; edits cannot clear it. Afterward, an automatically paused box wakes on `exec`, `ssh`, `files`, a public port request or `instabox wake <box>`. A zero runtime cap must be changed before waking. A normal pause preserves memory and processes.
|
|
65
70
|
- Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
|
|
66
|
-
- `exec`
|
|
71
|
+
- `exec` with one argument after `--` runs it as a shell string (`instabox exec work -- 'ls | wc -l'`); several arguments run as that exact argv, each quoted for you (`instabox exec work -- bash -c 'echo a b > /tmp/x'`).
|
|
72
|
+
- `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 longer commands (installs, builds, test suites) over SSH, which also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`. The box's runtime cap still applies during SSH or exec.
|
|
67
73
|
- 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.
|
|
68
74
|
- 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.
|
|
69
75
|
|
|
70
76
|
## Organization Secrets
|
|
71
77
|
|
|
72
|
-
Secrets are encrypted
|
|
78
|
+
Secrets are encrypted API credentials owned by an org, each limited to the HTTPS hosts it may be sent to. A box that selects a Secret gets a placeholder in its environment, never the credential itself. When a request from the box goes to one of the Secret's allowed hosts over HTTPS, the outbound proxy swaps the placeholder in the request headers for the credential. Code in the box can call the API but cannot read or copy the credential. No header-name configuration is needed.
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
- `env`: the box receives the real value. Use this explicitly for native database credentials, local signing, and clients that cannot use header substitution. Code running in the box can read and copy the value.
|
|
80
|
+
Secrets suit HTTP API keys and tokens that a client sends in a request header: model providers, GitHub's API, deploy and SaaS APIs. They do not cover database URLs or passwords, signing keys, or anything a client hashes, encodes or sends outside an HTTPS header; those need their own setup on the box.
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
Secrets replace the Vault, which is legacy and will be retired: save every new API credential as a Secret, never in the Vault, and do not suggest `vault set`, `set-file` or `import` to users. A new box without `--secrets` still gets an existing user's Vault items (see the last point below). Opt into Secrets with `create --secrets`, or use `secrets select` later. Metadata labels do not select Secrets, and Secrets do not deliver files.
|
|
78
83
|
|
|
79
84
|
```sh
|
|
80
85
|
instabox secrets ls --org team # metadata only
|
|
81
|
-
instabox secrets show work
|
|
82
|
-
instabox secrets set SERVICE_API_KEY --org team --
|
|
83
|
-
instabox secrets
|
|
84
|
-
instabox create --name app --org team --secrets SERVICE_API_KEY
|
|
85
|
-
instabox secrets select work --
|
|
86
|
-
instabox secrets push work
|
|
87
|
-
instabox create --name tenant --no-vault --secrets none
|
|
86
|
+
instabox secrets show work # selection and proxy status
|
|
87
|
+
instabox secrets set SERVICE_API_KEY --org team --host api.example.com
|
|
88
|
+
instabox secrets import .env --org team --host api.example.com # every value gets these hosts
|
|
89
|
+
instabox create --name app --org team --secrets SERVICE_API_KEY
|
|
90
|
+
instabox secrets select work --secrets SERVICE_API_KEY,OTHER_API_KEY
|
|
91
|
+
instabox secrets push work # refresh the selection on a running box
|
|
92
|
+
instabox create --name tenant --no-vault --secrets none
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
- Values for `set` come from stdin or a hidden prompt. Never print a credential or put its value into agent output, chat, command arguments, or logs. Before asking for a credential, check metadata with `secrets ls --org <org>` and `secrets show <box>`. A silent check such as `test "${NAME+x}" = x` tests
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- For
|
|
97
|
-
-
|
|
95
|
+
- Values for `set` come from stdin or a hidden prompt. Never print a credential or put its value into agent output, chat, command arguments, or logs. Before asking for a credential, check metadata with `secrets ls --org <org>` and `secrets show <box>`. A silent check such as `test "${NAME+x}" = x` tests that the placeholder is present without revealing it; it does not prove the upstream accepts the credential.
|
|
96
|
+
- `--host` is optional and repeatable. Give each exact hostname the credential may be sent to; without `--host` (or with `--host '*'`) every public HTTPS destination is allowed. Schemes, ports, paths, IP addresses, and partial wildcards such as `*.example.com` are rejected. Use the service's API host, such as `api.openai.com`, never a guess from the credential's value.
|
|
97
|
+
- Org commands default to the signed-in user's personal org. Use `--org` for a team, and select only Secrets from the box's org. Value, host and deletion changes require an org owner/admin sign-in. API keys with the appropriate machine scopes can inspect metadata and manage sandbox selections, but cannot retrieve or change Secret values.
|
|
98
|
+
- `secrets select BOX --secrets` replaces the box's selection: `all`, `none`, comma-separated names, or a JSON array (which allows a Secret literally named `all` or `none`). Keep existing names when adding one.
|
|
99
|
+
- Placeholders work only in HTTPS request headers on port 443 to the allowed hosts. Do not hash, encode, or sign with a placeholder; other traffic carries the placeholder unchanged.
|
|
100
|
+
- Rotating a value keeps its placeholder, so running clients need no change. Newly selected names appear in new shells that load the managed environment; restart existing applications to pick them up.
|
|
101
|
+
- For a box intended for another person or tenant, disable personal Vault injection with `--no-vault` and select only the org Secrets they may use. For a fresh box with neither system's credentials, use `--no-vault --secrets none`. An empty Secret selection alone still permits Vault injection. These flags do not remove credentials already copied into a source snapshot.
|
|
102
|
+
- For an existing box, disable Vault with `start BOX --no-vault` or `resume BOX --no-vault` and clear the Secret selection with `secrets select BOX --secrets none`. Refresh a running guest with both `vault push BOX` and `secrets push BOX`. Vault values already copied into processes or files remain there; disabling Vault does not recover them.
|
|
103
|
+
- The Vault is legacy and will be retired; every `vault` command prints a warning. Its commands remain only for items a user already saved: `vault ls`, `show`, `push` and `rm`, and `vault export` to recover a retained item. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store copies or deletes the other. To move a Vault env item that is an HTTP API credential, the user enters its value again as a Secret with its hosts (`secrets set NAME --host HOST` reads it from a hidden prompt), you select it on the box, then `vault rm` it with the user's go-ahead. Never read a Vault value to copy it. Database credentials and other non-HTTP secrets are not supported by Secrets and need their own setup on the box; Vault file items likewise.
|
|
98
104
|
|
|
99
105
|
## Moving work onto a box
|
|
100
106
|
|
|
@@ -115,7 +121,7 @@ instabox feedback --json --type bug --component cli --area exec \
|
|
|
115
121
|
|
|
116
122
|
- `--type`: `bug` when something does not work as its help or the docs say (when instructions did not match reality, also give `--doc`, `--expected` and `--workaround`), `feature-request` when what you need is not supported, `friction` when it works but is confusing or awkward, `other` otherwise.
|
|
117
123
|
- `--component`: `cli`, `api`, `sandbox` (the box itself, its templates or tools), `dashboard`, `skills`, `docs` or `other`. `--severity blocker|major|minor` (default `minor`); `--sandbox NAME` names the box involved.
|
|
118
|
-
- Emails, keys, tokens, home paths and public IPv4 addresses are redacted before sending, but do not paste secrets into it.
|
|
124
|
+
- Emails, keys, tokens, home paths and public IPv4 addresses are redacted before sending, but do not paste secrets into it. Signed in, the report opens a support ticket on the account, and the team's replies show in the dashboard at the link it prints (`ticket` in `--json`); signed out or waitlisted, it reaches the team without one.
|
|
119
125
|
- A failed or unconfirmed submission prints a warning and exits 0: do not retry. File one report per problem, not one per attempt.
|
|
120
126
|
|
|
121
127
|
## Habits
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: instabox-basic-setup
|
|
3
|
-
description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves
|
|
3
|
+
description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves known API tokens as org Secrets for their service's HTTPS API hosts, selects them on named sandboxes, and copies approved ordinary config files directly to named running sandboxes. It does not put files in the Secret store. Moves no code; for a repo, use instabox-move-repo.
|
|
4
4
|
metadata:
|
|
5
5
|
installed-by: instabox
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Basic setup with org Secrets and sandbox config copies
|
|
9
9
|
|
|
10
|
-
Use the user's chosen org and sandbox IDs. Secrets are shared within an org
|
|
10
|
+
Use the user's chosen org and sandbox IDs. Secrets are shared within an org. A sandbox that selects a Secret gets a placeholder in its environment; the outbound proxy puts the token into HTTPS request headers sent to the Secret's allowed hosts, and nowhere else. Other org sandboxes whose selection is `all`, including future sandboxes, can also use a newly saved Secret. Updating an existing Secret affects every sandbox already selecting it.
|
|
11
|
+
|
|
12
|
+
Only tokens in the built-in registry (`TOKEN_HOSTS` in `scripts/lib.mjs`) are saved, each for its service's API hosts: for example `ANTHROPIC_API_KEY` for `api.anthropic.com`, `OPENAI_API_KEY` for `api.openai.com`, `GH_TOKEN` and `GITHUB_TOKEN` for `api.github.com`. Every other credential-looking export becomes a manual step. Secrets work only for clients that send the token in an HTTPS header; database URLs, signing keys and git's own credential exchange need their own setup on the box.
|
|
11
13
|
|
|
12
14
|
Git and editor configuration is copied separately, once, to the named running sandboxes. It is not stored as Secrets, does not consume Secret quota, and is not reapplied on start or resume. This skill moves no repository or agent history. Use `instabox-move-repo` for code and conversation history.
|
|
13
15
|
|
|
14
16
|
| Group | Destination | Sources |
|
|
15
17
|
| --- | --- | --- |
|
|
16
|
-
| Tokens | Encrypted org
|
|
18
|
+
| Tokens | Encrypted org Secrets, each limited to its registry hosts | GitHub token through `gh auth token`; registry tokens in the environment or shell startup files; other credential-like exports listed as manual steps |
|
|
17
19
|
| Git | Files under the selected sandbox's home | `.gitconfig`, `.config/git/config`, global gitignore |
|
|
18
20
|
| Editor and terminal | Files under the selected sandbox's home | `.vimrc`, nvim config without plugin checkouts, `.tmux.conf` |
|
|
19
21
|
|
|
@@ -23,11 +25,11 @@ The scripts beside this file require Node 18+ locally and Python 3 in sandboxes
|
|
|
23
25
|
| --- | --- |
|
|
24
26
|
| `scan.mjs` | Read-only inventory of names, source locations, sizes and manual setup requirements; no credential values in output |
|
|
25
27
|
| `plan.mjs` | Write and strictly parse the editable plan, binding it to an org and Secret metadata |
|
|
26
|
-
| `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets through CLI stdin, merge
|
|
28
|
+
| `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets with their registry hosts through CLI stdin, merge sandbox selections, and copy config |
|
|
27
29
|
| `content.mjs` | Rewrite HOME paths, remove recognized credential lines, and adapt git/editor configuration for Linux |
|
|
28
30
|
| `install-files.py` | Install ordinary config files privately, refusing symlinks and unsafe paths; no Secret-store or legacy Vault access |
|
|
29
31
|
|
|
30
|
-
Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through
|
|
32
|
+
Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through the hidden prompt of `instabox secrets set NAME --org ORG --host HOST`.
|
|
31
33
|
|
|
32
34
|
## 1. Scan and choose the org
|
|
33
35
|
|
|
@@ -53,20 +55,11 @@ node "$S/plan.mjs" write --scan "$W/scan.json" --secrets "$W/secrets.json" \
|
|
|
53
55
|
--org "$ORG_ID" --sandbox <sandbox-id> > "$W/plan.md"
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
Repeat `--sandbox` for additional targets, or omit it to save Secrets without
|
|
57
|
-
|
|
58
|
-
A token policy looks like either of these:
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
Delivery `env:SERVICE_API_KEY`: `proxy` hosts `api.example.com`
|
|
62
|
-
Delivery `env:DATABASE_URL`: `env` hosts `-`
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The `env:` prefix is the scanner's variable ID, not its delivery mode. New token rows start with explicit `env` mode for client compatibility; this gives the named sandboxes the real value. Choose `proxy` and exact approved hosts when the client sends the value literally in an HTTPS header on port 443. Do not infer hosts from a token's value or silently switch modes when a client fails. Native database protocols, local signing and git's encoded credential exchange need the real value or a separate protocol-aware broker. Proxy placeholders cannot be used as real signing or encoding inputs.
|
|
58
|
+
Repeat `--sandbox` for additional targets, or omit it to save Secrets without changing any selection or copying files. The plan lists `Org`, `Sandboxes` and checkboxes; each token row names the hosts its Secret is limited to. The org is bound to the metadata used to generate the plan; regenerate for another org. Sandbox IDs and checkboxes can be edited in the plan; hosts come from the registry and cannot.
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
The `env:` prefix in a row ID is the scanner's variable ID. Registry token rows are checked by default; keychain-backed sources are unchecked. Existing Secrets changed outside the last successful run are unchecked, and checking them authorizes replacement in the org. Files are unchecked when no sandbox is selected. Checking a config file authorizes replacement of that path on each named sandbox.
|
|
68
61
|
|
|
69
|
-
Show the concrete names,
|
|
62
|
+
Show the concrete names, allowed hosts, org and target sandboxes. Obtain any choices or authorization the original request did not already provide before reading values or writing. Do not ask again for an action already explicitly authorized.
|
|
70
63
|
|
|
71
64
|
Parse the approved plan:
|
|
72
65
|
|
|
@@ -75,7 +68,7 @@ node "$S/plan.mjs" parse --scan "$W/scan.json" --secrets "$W/secrets.json" \
|
|
|
75
68
|
--org "$ORG_ID" --limits "$W/limits.json" "$W/plan.md" > "$W/selection.json"
|
|
76
69
|
```
|
|
77
70
|
|
|
78
|
-
Exit 1 identifies malformed or missing rows,
|
|
71
|
+
Exit 1 identifies malformed or missing rows, a changed org/catalog, a scan with a token outside the registry, unsafe config, or a file selection without targets. Fix choices without changing item IDs or deleting rows. Exit 3 identifies quota or copy-size limits. Config files have their own copy limits and do not count as Secret entries. Values are measured before push; plan generation never reads token values. Legacy selector-based plans and manifests grant no authority in the new model and must be regenerated.
|
|
79
72
|
|
|
80
73
|
## 3. Dry-run, then apply the approved selection
|
|
81
74
|
|
|
@@ -88,11 +81,11 @@ The dry-run reads the approved credential sources and prepares filtered config,
|
|
|
88
81
|
|
|
89
82
|
The pusher verifies org ownership, target orgs, Secret IDs and versions, input limits and total Secret quota before mutations. If a selected Secret changed since planning, regenerate the plan. These are preflight checks, not a transaction across all CLI operations: concurrent org edits can still occur during application. A failed run reports partial outcomes; successfully saved Secrets remain in the org. Do not assume rollback or automatic removal of previously copied files.
|
|
90
83
|
|
|
91
|
-
Only selected token names are added to each target's
|
|
84
|
+
Only selected token names are added to each target's selection; existing names are preserved, and a target selecting `all` stays `all`. Running targets receive an immediate refresh; stopped targets receive the selection at their next start, but config copying requires them to be running. A failed or pending refresh does not install config that may depend on those credentials. Existing processes need to reload their environment or restart to see new placeholders.
|
|
92
85
|
|
|
93
|
-
Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free.
|
|
86
|
+
Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free. No git credential helper is added: git over HTTPS sends credentials in a form the proxy cannot fill in, so the box needs its own git sign-in (for example `gh auth login` there). An existing org Secret by itself is not a sandbox selection; add it through an explicitly reviewed `instabox secrets select BOX --secrets ...` that keeps the other selected names.
|
|
94
87
|
|
|
95
|
-
The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs
|
|
88
|
+
The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs, versions and hosts, and copied sandbox paths, never values. It does not grant reuse across orgs or across deletion and recreation of a Secret. It records completed work for a repeat plan; it does not sync changes automatically.
|
|
96
89
|
|
|
97
90
|
## 4. Verify and report
|
|
98
91
|
|
|
@@ -101,8 +94,8 @@ instabox secrets show <box>
|
|
|
101
94
|
instabox exec <box> -- 'test "${GH_TOKEN+x}" = x && test -f "$HOME/.gitconfig"'
|
|
102
95
|
```
|
|
103
96
|
|
|
104
|
-
Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove
|
|
97
|
+
Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove the upstream accepts the token. Confirm client behavior without logging credentials, and explain application restarts.
|
|
105
98
|
|
|
106
|
-
Report the org Secret names and
|
|
99
|
+
Report the org Secret names and hosts saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving Secrets without targets changes no selection; replacing a value still affects sandboxes already selecting it. Say that git over HTTPS needs its own sign-in on the box, and that credentials outside the registry (database URLs, cloud keys, other tokens) were not moved and need their own setup there; an HTTP API key can be added with `instabox secrets set NAME --org ORG --host HOST`. Future boxes created without `--secrets` keep their existing Vault behavior. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope.
|
|
107
100
|
|
|
108
101
|
Config edits in a sandbox are not overwritten at every resume. Shell aliases, SSH keys, cloud credential files, agent sign-in databases and repositories are outside this inventory. Complete the selected manual sign-in steps only within existing authorization. Remove `$W` when no longer needed; it contains metadata and choices, not credential values.
|