instabox 0.6.4 → 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.4",
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 in the dashboard: Profile → API keys (account menu)). 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
 
@@ -68,6 +68,7 @@ instabox rm work # delete the box
68
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
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.
70
70
  - Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
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'`).
71
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.
72
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.
73
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.
@@ -92,7 +93,7 @@ instabox create --name tenant --no-vault --secrets none
92
93
  ```
93
94
 
94
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.
95
- - `--host` is required and repeatable: give each exact hostname the credential may be sent to (no scheme, port, path, wildcard or IP address). Use the service's API host, such as `api.openai.com`, never a guess from the credential's value.
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.
96
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.
97
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.
98
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.
@@ -120,7 +121,7 @@ instabox feedback --json --type bug --component cli --area exec \
120
121
 
121
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.
122
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.
123
- - 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.
124
125
  - A failed or unconfirmed submission prints a warning and exits 0: do not retry. File one report per problem, not one per attempt.
125
126
 
126
127
  ## Habits