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 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 in your browser (first run only; confirm its current Instabox account or choose another), 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.
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 browser confirm the account. Explicit `--github` or `--google` starts that provider directly, as does `instabox login --provider github|google`.
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 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.
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 { createHash, randomBytes } from 'node:crypto';
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, callbackHandler } from '../lib/login.mjs';
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 browser confirms its existing Instabox account
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 CLI and agent skills
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 directly with that provider instead of confirming
62
- the browser's current Instabox account
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 browserLogin, asking for an invite code when the account is on the waitlist.
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 browserLogin(invite);
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
- async function browserLogin(invite) {
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
- // The loopback sign-in callback (RFC 8252); one code exchange per sign-in, however often it is hit.
2
- export function callbackHandler({ state, exchange, finish, page }) {
3
- let taken = false;
4
- return async (req, res) => {
5
- const u = new URL(req.url, 'http://127.0.0.1');
6
- const html = { 'Content-Type': 'text/html; charset=utf-8' };
7
- if (u.pathname !== '/callback') {
8
- res.writeHead(404);
9
- return res.end();
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 (u.searchParams.get('state') !== state) {
12
- res.writeHead(400, html);
13
- return res.end(page.stale());
14
- }
15
- // A reload or a double navigation: the server revokes the whole session when a code comes back.
16
- if (taken) {
17
- res.writeHead(200, html);
18
- return res.end(page.again());
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "A cloud box for your coding agent, the instabox CLI and its agent skills in one command: npx instabox@latest",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 # confirm the browser's account, or choose GitHub / Google
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` open account confirmation in the browser: use its current Instabox account or choose another. To go directly to a provider, use `instabox login --provider github|google`, or `npx instabox@latest --github|--google`. The CLI shares the sign-in `npx` made. Device-code sign-in (`--device`) uses GitHub.
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 on the dashboard's API Keys page). Never print, commit or log a key.
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 it pauses (fixed at 30m unless an admin gave the account manual lifecycle)
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`, `--env-secrets NAME,NAME|none`, `--no-wait`.
64
- - A box pauses itself after 30 idle minutes (no agent working, no terminal use, little CPU) and wakes on the next `exec`, `ssh`, `files`, public port request or `instabox wake <box>`; a paused box keeps its memory and processes. The 30 minutes are fixed for every account unless an admin gave it manual lifecycle: only then do `--auto-suspend 0` (always on) and `--auto-suspend 2h` apply, and the CLI prints a note when the server kept 30m. Boxes have no other timeout. Do not try to keep a box on by other means; a paused box resumes on the next command.
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` 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'`.
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 key=value entries owned by an org. Each entry has one delivery mode:
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
- - `proxy`: the box receives a placeholder. The outbound proxy replaces it in ordinary HTTPS header values only when the destination matches the Secret's allowed hosts. Code in the box can use this credential but does not receive its real value through Secret delivery. No header-name configuration is needed.
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
- A new box without Secret selection flags keeps the existing Vault behavior. Opt into Secrets with `--secrets` or `--env-secrets`, or use `secrets select` later. At creation, `--env-secrets` alone also selects all org proxy Secrets. Select env names explicitly; there is no env `all` mode. Metadata labels do not select Secrets, and Secrets do not deliver files.
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 # selection and independent proxy/guest status
82
- instabox secrets set SERVICE_API_KEY --org team --delivery proxy --host api.example.com
83
- instabox secrets set DATABASE_URL --org team --delivery env
84
- instabox create --name app --org team --secrets SERVICE_API_KEY --env-secrets DATABASE_URL
85
- instabox secrets select work --env-secrets DATABASE_URL
86
- instabox secrets push work # refresh managed env/trust on a running box
87
- instabox create --name tenant --no-vault --secrets none --env-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 variable presence without revealing it; it does not prove successful proxy authorization.
91
- - 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, delivery-mode 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.
92
- - Selection updates replace each supplied list; omitted lists remain unchanged. Preserve existing names when adding to a selection. Proxy selection accepts `all`; both selections accept `none` or `[]`. JSON arrays allow a Secret literally named `all` or `none`.
93
- - Proxy delivery requires allowed hosts and works with literal placeholders in HTTPS headers on port 443. Do not hash, encode, or sign with a placeholder. Native database TLS is not HTTPS. Unsupported traffic never falls back to real-value env delivery.
94
- - Rotating an existing proxy value preserves its placeholder. Env refresh and newly added variables affect new shells that load the managed environment; restart or reconfigure existing applications and connection pools. Removing a selection cannot recover env values already copied into processes, files, or snapshots. Revoke or rotate those credentials at the upstream service when necessary.
95
- - For a box intended for another person or tenant, disable personal Vault injection with `--no-vault` and explicitly select only the org Secrets they may use. For a fresh box with neither system's credentials, use `--no-vault --secrets none --env-secrets none`. Empty Secret selections alone still permit Vault injection. These flags do not remove credentials already copied into a source snapshot.
96
- - For an existing box, disable Vault with `start BOX --no-vault` or `resume BOX --no-vault` and explicitly clear both Secret selections with `secrets select BOX --secrets none --env-secrets none`. Refresh a running guest with both `vault push BOX` and `secrets push BOX`. Existing processes and copied files can retain values; disabling either system does not recover those copies.
97
- - Vault remains available through `vault set`, `set-file`, `import`, `ls`, `rm`, `show`, and `push`. The CLI warns about future retirement. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store automatically copies or deletes the other. Move values deliberately when the user is ready.
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. The report carries the signed-in account (its user ID, and its login unless that is an email address), so the team can follow up.
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 selected credentials as org key=value Secrets with explicit proxy/env modes, grants them only through sandbox selections, and copies approved ordinary config files directly to named running sandboxes. It does not put files in the Secret store or automatically give real credentials to future boxes. Moves no code; for a repo, use instabox-move-repo.
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, but real-value `env` delivery requires explicit sandbox selection. A `proxy` Secret supplies a placeholder for HTTPS header substitution at its allowed hosts. Other org sandboxes selecting all proxy Secrets, including future sandboxes, can also use a newly saved proxy credential. Updating an existing Secret affects every sandbox already authorized to use it.
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 key=value Secrets; explicit per-token `proxy` or `env` mode | GitHub token through `gh auth token`; known service credentials in the environment or shell startup files; other credential-like exports listed unchecked |
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 explicit sandbox selections, and copy config |
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 a hidden CLI prompt with the appropriate org and delivery mode.
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 adding env grants or copying files. The plan lists `Org`, `Sandboxes`, checkboxes and one `Delivery` line per token. The org is bound to the metadata used to generate the plan; regenerate for another org. Sandbox IDs and delivery policies can be edited in the plan.
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
- Well-known token rows are checked by default; unknown and keychain-backed sources are unchecked. Existing Secrets changed outside the last successful run are unchecked, and checking them authorizes replacement in the org. A previous proxy mode is preserved as the starting policy on a repeat run. Files are unchecked when no sandbox is selected. Checking a config file authorizes replacement of that path on each named sandbox.
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, modes, 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. Explain that env values are readable by sandbox code and may persist in processes, files or snapshots; removing a selection cannot recover them.
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, invalid policies, a changed org/catalog, 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.
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 grants; existing selections are preserved, including proxy `all`. Env selection never becomes `all`. Running targets receive an immediate managed-environment refresh; stopped targets can receive selections for their next start, but config copying requires them to be running. A failed or pending guest refresh does not install config that may depend on those credentials. Existing processes need to reload their environment or restart.
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. The gh git helper is added only when this plan explicitly selects `GH_TOKEN` with env delivery. An existing org Secret by itself is not a sandbox grant; reuse it through an explicitly reviewed `instabox secrets select` update if needed, preserving other selected names.
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/versions/modes 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.
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 proxy authorization or upstream success. Confirm client behavior without logging credentials, and explain application restarts when env values changed.
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 modes saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving env Secrets without targets adds no grants; replacing a value still affects sandboxes already authorized to use it. Future boxes without Secret selection flags keep their existing Vault behavior. At creation, selecting env Secrets alone also selects all org proxy Secrets; add an explicit proxy selection to narrow that access. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope. Rotate or revoke upstream when a previously delivered env value must stop working.
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.