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.
@@ -13,7 +13,8 @@ import {
13
13
  MACOS_ONLY,
14
14
  SECRET_VALUE,
15
15
  FILE_MAX_VALUE,
16
- WELL_KNOWN,
16
+ FORMAT,
17
+ TOKEN_HOSTS,
17
18
  hasCommand,
18
19
  isMain,
19
20
  isSecretName,
@@ -76,7 +77,6 @@ function scanTokens({ home, env, procEnv }) {
76
77
  byName.set(name, item);
77
78
  items.push(item);
78
79
  };
79
- const reserved = new Set();
80
80
 
81
81
  // gh: the token gh keeps (keychain or hosts.yml), read with `gh auth token` at push time.
82
82
  if (hasCommand('gh', env)) {
@@ -84,42 +84,54 @@ function scanTokens({ home, env, procEnv }) {
84
84
  if (st.ok) {
85
85
  const who = (st.stdout + st.stderr).match(/account (\S+)/)?.[1] || null;
86
86
  add('GH_TOKEN', { type: 'cmd', argv: ['gh', 'auth', 'token', '--hostname', 'github.com'] }, '`gh auth token`', {
87
- note: `${who ? `GitHub account ${who}; ` : ''}gh, and git over HTTPS to GitHub, use it on the box`,
87
+ note: `${who ? `GitHub account ${who}; ` : ''}gh uses it on the box; git over HTTPS needs its own sign-in`,
88
88
  });
89
89
  }
90
90
  }
91
91
 
92
- // Shell startup files: exported tokens.
92
+ // Shell startup files: exported tokens. Only names in TOKEN_HOSTS can be saved; any other
93
+ // credential-looking export is a manual step.
94
+ const listed = new Set();
95
+ const listManual = (name, place, why, todo) => {
96
+ if (listed.has(name)) return;
97
+ listed.add(name);
98
+ manual.push({ what: `${name} (${place})`, why, todo });
99
+ };
93
100
  for (const f of SHELL_RC) {
94
101
  const text = readText(join(home, f));
95
102
  if (text === null) continue;
96
103
  for (const a of shellAssignments(text)) {
97
- if (!WELL_KNOWN.has(a.name) && !isSecretName(a.name) && !a.secretValue) continue;
104
+ if (!TOKEN_HOSTS.has(a.name) && !isSecretName(a.name) && !a.secretValue) continue;
105
+ const place = `~/${f}:${a.line}`;
98
106
  if (!secretNameOK(a.name)) {
99
- if (!reserved.has(a.name)) {
100
- reserved.add(a.name);
101
- manual.push({
102
- what: `${a.name} (~/${f}:${a.line})`,
103
- why: 'Secrets reserve this variable name for sandbox or trust configuration',
104
- todo: `if a box needs it, set it there by hand`,
105
- });
106
- }
107
+ listManual(
108
+ a.name,
109
+ place,
110
+ 'Secrets reserve this variable name for sandbox or trust configuration',
111
+ 'if a box needs it, set it there by hand',
112
+ );
113
+ continue;
114
+ }
115
+ if (!TOKEN_HOSTS.has(a.name)) {
116
+ listManual(
117
+ a.name,
118
+ place,
119
+ 'this skill does not know which HTTPS API hosts it is for',
120
+ `if its client sends it in an HTTPS header, \`instabox secrets set ${a.name} --org <org> --host <api-host>\` and select it on the box; other credentials need their own setup there`,
121
+ );
107
122
  continue;
108
123
  }
109
124
  const extra = {};
110
125
  if (a.keychain) {
111
126
  extra.checked = false;
112
127
  extra.reason = 'your shell reads it from the macOS keychain: checking it runs that lookup once, at push time';
113
- } else if (!WELL_KNOWN.has(a.name)) {
114
- extra.checked = false;
115
- extra.reason = 'not a well-known model or service token: check it only for the org and sandboxes in the plan';
116
128
  }
117
- add(a.name, { type: 'rc', file: join(home, f), name: a.name }, `~/${f}:${a.line}`, extra);
129
+ add(a.name, { type: 'rc', file: join(home, f), name: a.name }, place, extra);
118
130
  }
119
131
  }
120
- // Well-known tokens in the environment the scan runs in (set some other way than a shell file).
132
+ // Known tokens in the environment the scan runs in (set some other way than a shell file).
121
133
  for (const name of Object.keys(procEnv).sort()) {
122
- if (!WELL_KNOWN.has(name) || !procEnv[name]) continue;
134
+ if (!TOKEN_HOSTS.has(name) || !procEnv[name]) continue;
123
135
  add(name, { type: 'env', name }, 'this shell’s environment');
124
136
  }
125
137
 
@@ -127,23 +139,20 @@ function scanTokens({ home, env, procEnv }) {
127
139
  manual.push({
128
140
  what: 'GitHub token (GH_TOKEN)',
129
141
  why: hasCommand('gh', env) ? 'gh is not signed in on this machine' : 'gh is not installed on this machine',
130
- todo: 'sign in here with `gh auth login` and run this again; or put a token in yourself: `instabox secrets set GH_TOKEN --org <org> --delivery env` (it prompts without echo)',
131
- });
132
- if (!byName.has('CLAUDE_CODE_OAUTH_TOKEN'))
133
- manual.push({
134
- what: 'Claude Code sign-in (CLAUDE_CODE_OAUTH_TOKEN)',
135
- why: 'not found in your shell files; the sign-in on this machine is in the macOS keychain, and is never read',
136
- todo:
137
- 'run `claude setup-token` yourself, then `instabox secrets set CLAUDE_CODE_OAUTH_TOKEN --org <org> --delivery env` and paste it at the prompt' +
138
- (byName.has('ANTHROPIC_API_KEY')
139
- ? ' (or skip it: ANTHROPIC_API_KEY is in the plan, and Claude Code uses that)'
140
- : '; or `claude`, then /login, on each box'),
142
+ todo: 'sign in here with `gh auth login` and run this again; or put a token in yourself: `instabox secrets set GH_TOKEN --org <org> --host api.github.com` (it prompts without echo)',
141
143
  });
144
+ manual.push({
145
+ what: 'Claude Code sign-in',
146
+ why: 'the sign-in on this machine is in the macOS keychain, and is never read',
147
+ todo: byName.has('ANTHROPIC_API_KEY')
148
+ ? 'skip it: ANTHROPIC_API_KEY is in the plan, and Claude Code uses that; or run `claude`, then /login, on each box'
149
+ : 'run `claude`, then /login, on each box',
150
+ });
142
151
  if (!byName.has('OPENAI_API_KEY') && (hasCommand('codex', env) || statOrNull(join(home, '.codex'))))
143
152
  manual.push({
144
153
  what: 'Codex sign-in',
145
154
  why: 'no OPENAI_API_KEY in your shell files; a ChatGPT sign-in cannot be copied (its refresh token rotates)',
146
- todo: 'on each box: `codex login --device-auth`; or `instabox secrets set OPENAI_API_KEY --org <org> --delivery env`, then select it explicitly for each intended sandbox',
155
+ todo: 'on each box: `codex login --device-auth`; or `instabox secrets set OPENAI_API_KEY --org <org> --host api.openai.com`, then select it on each box',
147
156
  });
148
157
  return { items, manual };
149
158
  }
@@ -259,7 +268,7 @@ export function scan(opts = {}) {
259
268
  const home = resolve(opts.home || env.HOME);
260
269
  const tokens = scanTokens({ home, env, procEnv: opts.procEnv || env });
261
270
  const items = [...tokens.items, ...scanGit({ home, env }), ...scanEditor({ home })];
262
- return { version: 3, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
271
+ return { version: FORMAT, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
263
272
  }
264
273
 
265
274
  function main() {
@@ -20,16 +20,16 @@ Three scripts in `scripts/` next to this file do the parts that must be exact (N
20
20
  What goes over:
21
21
 
22
22
  - All of the main checkout's `.git` (branches, tags, stashes, unpushed commits, the index so staged changes stay staged, each worktree's own HEAD and index), every tracked file, and every untracked file git does not ignore, in the main checkout and in each linked worktree.
23
- - **Never build output or dependencies**, even when the repo forgot to ignore them: `node_modules`, `target`, `dist`, `build`, `out`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.cache`, `coverage`, `__pycache__`, `.venv`, `venv`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `bazel-*`, `*.egg-info`, `*.o`, `*.so`, `*.pyc`, `*.class`, ... (the full list is `EXCLUDED_DIRS` and `EXCLUDED_FILES` in `repo-files.mjs`). Files git ignores stay too. A **tracked** file always goes, even under such a name, so `git status` matches. `--keep NAME` copies an excluded name anyway, for a repo whose source really lives in, say, `build/` (pass it to every `repo-files.mjs` run, step 1 and step 3, through `KEEP`). It does not bring back an untracked `.env`: their values need explicit Secret import and selection (step 7).
23
+ - **Never build output or dependencies**, even when the repo forgot to ignore them: `node_modules`, `target`, `dist`, `build`, `out`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.cache`, `coverage`, `__pycache__`, `.venv`, `venv`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `bazel-*`, `*.egg-info`, `*.o`, `*.so`, `*.pyc`, `*.class`, ... (the full list is `EXCLUDED_DIRS` and `EXCLUDED_FILES` in `repo-files.mjs`). Files git ignores stay too. A **tracked** file always goes, even under such a name, so `git status` matches. `--keep NAME` copies an excluded name anyway, for a repo whose source really lives in, say, `build/` (pass it to every `repo-files.mjs` run, step 1 and step 3, through `KEEP`). It does not bring back an untracked `.env`: those stay behind (step 7 lists them).
24
24
  - Claude Code and Codex conversations for the repo and its worktrees (on by default, the user can opt out).
25
25
 
26
- Not moved: `.env` files that are not committed (their values can be imported as Secrets in step 7), credentials stored outside the repo (git credential helpers, gh, cloud, API keys in your environment), other local tools and config, other repos. A `.env` that is committed or staged is in `.git` and goes with it. Everything inside `.git` goes, including `.git/config` and so a remote URL with a token embedded in it (`https://x-access-token:TOKEN@...`); and only `.env`-named untracked files are held back, so an untracked `.npmrc` with a token, a `*.pem` or a `secrets.yaml` that git does not ignore goes too.
26
+ Not moved: `.env` files that are not committed, credentials stored outside the repo (git credential helpers, gh, cloud, API keys in your environment), other local tools and config, other repos. A `.env` that is committed or staged is in `.git` and goes with it. Everything inside `.git` goes, including `.git/config` and so a remote URL with a token embedded in it (`https://x-access-token:TOKEN@...`); and only `.env`-named untracked files are held back, so an untracked `.npmrc` with a token, a `*.pem` or a `secrets.yaml` that git does not ignore goes too.
27
27
 
28
28
  **Never read, print or paste a credential value yourself**: not a `.env`, not `gh auth token`, not `echo $SOME_KEY`.
29
29
 
30
30
  Before starting, tell the user plainly:
31
31
 
32
- - **git push/pull from the box will not work until they set up credentials there** (for example `instabox ssh <box>` then `gh auth login`; org Secrets support key=value credentials, while git configuration must be set up separately).
32
+ - **git push/pull from the box will not work until they set up credentials there** (for example `instabox ssh <box>` then `gh auth login`; org Secrets cover HTTP API keys only, so git sign-in is set up separately).
33
33
  - **The copied conversation history may contain secrets they pasted earlier** (tokens, keys, passwords). It goes by default; ask whether to skip it (or only Codex / only Claude Code).
34
34
  - Their local copy stays as it is, and nothing keeps the two in sync afterwards.
35
35
 
@@ -72,7 +72,7 @@ BOX=<box>
72
72
  instabox create --name "$BOX" --meta repo="$(basename "$DEST")"
73
73
  ```
74
74
 
75
- Box names are lowercase letters, digits and `-`. The box gets the default size, 4 vCPU / 8 GB (the instabox skill's Sizes lists the others a plan offers). The default template (`agent-browser`, shown as standard in the dashboard) has git, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker and a desktop with Chromium. This box is meant to stay; it pauses itself after 30 idle minutes. The `repo=` label can match existing Vault selectors; it does not select org Secrets. Without Secret selection flags the box keeps the existing Vault behavior. Opt into org Secrets explicitly with `--secrets` or `--env-secrets`; at creation, `--env-secrets` alone also selects all org proxy Secrets. Use `--no-vault --secrets none --env-secrets none` when neither store should deliver credentials.
75
+ Box names are lowercase letters, digits and `-`. The box gets the default size, 4 vCPU / 8 GB (the instabox skill's Sizes lists the others a plan offers). The default template (`agent-browser`, shown as standard in the dashboard) has git, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker and a desktop with Chromium. This box is meant to stay; it pauses itself after 30 idle minutes. The `repo=` label can match a user's existing Vault selectors (the Vault is legacy and will be retired; never save credentials there); it does not select org Secrets. Without `--secrets` the box still gets such existing Vault items. Opt into org Secrets explicitly with `--secrets NAME,NAME`. Use `--no-vault --secrets none` when neither store should deliver credentials.
76
76
 
77
77
  ## 3. Copy the repo and its worktrees
78
78
 
@@ -111,7 +111,7 @@ done
111
111
  git worktree list; instabox exec "$BOX" --cwd "$DEST" -- git worktree list
112
112
  ```
113
113
 
114
- The only allowed differences are `<` lines starting with `??` for what the plan left out (a build directory, an untracked `.env`, or a whole `?? dir/` that held only those), and ` M <submodule>` for a submodule with uncommitted changes inside it (those are not copied: note it for step 8). A name passed with `--keep` must not show up as a `??` difference: it was meant to go. HEADs must match, and the box's `git worktree list` must show the same branches, detached HEADs and `locked` marks at the box paths, without the worktrees left behind. Anything else: stop and look before going on.
114
+ The only allowed differences are `<` lines starting with `??` for what the plan left out (a build directory, an untracked `.env`, or a whole `?? dir/` that held only those), and ` M <submodule>` for a submodule with uncommitted changes inside it (those are not copied: note it for step 7). A name passed with `--keep` must not show up as a `??` difference: it was meant to go. HEADs must match, and the box's `git worktree list` must show the same branches, detached HEADs and `locked` marks at the box paths, without the worktrees left behind. Anything else: stop and look before going on.
115
115
 
116
116
  ## 4. Conversation history (unless the user opted out)
117
117
 
@@ -165,36 +165,13 @@ instabox exec "$BOX" --cwd "$DEST" -- 'git status --porcelain | sort; git rev-pa
165
165
 
166
166
  Compare with step 3. New `??` lines from the installs (for example a lockfile the project does not commit) are worth a mention; a modified tracked file is not expected: find which step did it.
167
167
 
168
- ## 7. `.env` values: explicit Secret import and selection
169
-
170
- The plan's `envLeftOut` lists excluded files by name (`jq -r '.main.envLeftOut, (.worktrees[] | select(.move) | .envLeftOut) | .[]' "$W/plan.json"`). Do not print their contents. The new Secret model stores individual key=value entries, not files, and does not create or link a `.env` in the repo.
171
-
172
- For a project that needs the real values, explain that env delivery makes them readable by code in the box. Use the user's authorization to import the intended file and select its names for this box. Import is org-wide and replaces existing entries with the same names: resolve conflicts and confirm any intended replacement before importing. Different projects needing different values under the same variable name should use separate orgs, or configure their applications to use distinct names; do not silently rename or overwrite a shared credential.
173
-
174
- After that choice, per approved file (the CLI reads values; saved JSON contains metadata only):
175
-
176
- ```bash
177
- BOX_ORG=$(instabox get "$BOX" --json | jq -r '.orgId')
178
- instabox secrets show "$BOX" --json > "$W/selected-secrets.json"
179
- instabox secrets import "<local .env path>" --org "$BOX_ORG" --delivery env --json > "$W/imported-secrets.json"
180
- ENV_NAMES=$(jq -c -s '.[0].envSecrets + [.[1].items[].name] | unique' "$W/selected-secrets.json" "$W/imported-secrets.json")
181
- instabox secrets select "$BOX" --env-secrets "$ENV_NAMES"
182
- instabox secrets push "$BOX"
183
- ```
184
-
185
- This merges the approved env names with the box's current env selection and leaves its proxy selection unchanged. Import alone grants no env access to any box. Start a new login shell, or restart/reconfigure the application, to load the updated environment. Use silent presence checks; do not print values to verify delivery.
186
-
187
- For credentials sent literally in HTTPS headers, save them with `--delivery proxy --host <approved-host>` instead, and select the corresponding proxy names. Every entry in one import shares its delivery mode and host scope, so do not import a mixed `.env` as proxy credentials without separating those decisions. A native database client or a client that computes signatures needs an explicitly selected env value.
188
-
189
- If the application requires a physical `.env` or another credential file, it needs a separate, explicitly authorized file-transfer or guest-generation workflow with restricted file permissions. Such a file is readable in the guest and may be included in snapshots; it has no Secret-managed file cleanup. Report this requirement rather than claiming Secret import recreates the original file. Tokens and local editor or git configuration are separate setup decisions; metadata labels do not grant credentials.
190
-
191
- ## 8. Tell the user
168
+ ## 7. Tell the user
192
169
 
193
170
  - The box name and where the repo is: `instabox ssh <box>`, then `cd /data/workspace/<name>`; each worktree with its box path and branch; worktrees left behind and why.
194
171
  - Uncommitted work inside a submodule (a ` M <submodule>` line in step 3) did not come over: name each such submodule so the user can commit or copy that work themselves.
195
172
  - Sizes: on disk before, sent after, and the biggest things left out (they were rebuilt or reinstalled on the box).
196
173
  - What was installed, whether build and tests pass, and anything left to do (each FAILED step and note from `deps.sh` that is not fixed).
197
- - Again: git push/pull needs credentials set up on the box. State which Secret names and delivery modes were selected, if any; never include values. Ignored files, build output and untracked `.env` files were not copied. Secrets in committed or staged files, anywhere in `.git` (such as a token in a remote URL in `.git/config`), in untracked files not named `.env*`, or in the copied history, did go over.
174
+ - Again: git push/pull needs credentials set up on the box. Ignored files and build output were not copied. Untracked `.env` files were left behind: list them by name from the plan (`jq -r '.main.envLeftOut, (.worktrees[] | select(.move) | .envLeftOut) | .[]' "$W/plan.json"`), never their contents. For an HTTP API key the project needs, the user can run `instabox secrets set NAME --host <api-host>` and select it on the box with `instabox secrets select`; database URLs and other credentials need their own setup on the box. Secrets in committed or staged files, anywhere in `.git` (such as a token in a remote URL in `.git/config`), in untracked files not named `.env*`, or in the copied history, did go over.
198
175
  - How to pick up old conversations, kept short, with the session ids from step 4:
199
176
  - Claude Code, in the repo (or worktree) directory on the box: `claude --continue` (latest) or `claude --resume` (picker, or `claude --resume <id>`).
200
177
  - Codex, in that directory: `codex resume` (picker for this directory), `codex resume --last`, or `codex resume <id>`.
@@ -325,7 +325,7 @@ project() { # dir label
325
325
  note "$L: has a compose file; its services were not started (docker compose up -d)"
326
326
  for f in .env.example .env.sample .env.template; do
327
327
  if [ -f "$f" ] && [ ! -e .env ]; then
328
- note "$L: expects a .env (see $f); .env files are not copied: bring it through the Vault (instabox-move-repo skill, .env files)"
328
+ note "$L: expects a .env (see $f); .env files are not copied: the user sets it up (instabox-move-repo skill, step 7)"
329
329
  break
330
330
  fi
331
331
  done
@@ -10,7 +10,7 @@
10
10
  // relative to PATH
11
11
  // --skip-worktree PATH leave a linked worktree behind (repeatable; pass it to --list0 runs too)
12
12
  // --keep NAME copy an excluded name anyway (repeatable): --keep build (not an untracked
13
- // .env: those go through the Vault)
13
+ // .env: those always stay behind)
14
14
  //
15
15
  // What goes: all of the main checkout's .git (branches, stashes, the index, unpushed commits, the
16
16
  // worktrees' own HEAD and index under .git/worktrees/), every tracked file that exists, and every
@@ -108,7 +108,7 @@ export const EXCLUDED_FILES = [
108
108
  '.DS_Store',
109
109
  ];
110
110
 
111
- // Untracked .env files never go (secrets belong in the Vault). Templates do.
111
+ // Untracked .env files never go: the final report lists them as left behind. Templates do.
112
112
  const ENV_FILE = /^\.env(?:\..+)?$/;
113
113
  const ENV_TEMPLATE = /^\.env\.(?:example|sample|template|dist|defaults)$/;
114
114
  const isEnvName = (f) => {
@@ -18,7 +18,7 @@ instabox snapshot save work --name work-ready --description "deps installed, rep
18
18
  - A running box is saved live, without stopping it: the snapshot is `pending` in `instabox snapshot ls` until its disks finish uploading (`--no-wait` returns at once).
19
19
  - Do not stop the box first: `stop` ends every process in it. A live snapshot is like a clean power cut: what is on disk is kept, what programs hold only in memory is not. Only if the server answers that this box must be stopped, or if a program must write its data to disk first (a database mid-write), run `instabox stop work`, save, then `instabox start work`.
20
20
  - `--root-only` saves just the root disk (smaller; `/data` is not included).
21
- - A disk snapshot includes credential files written by applications, tool sign-ins, and historical Vault files. The managed Secret environment lives on tmpfs, but applications can copy real env values to disk; a suspend snapshot can also retain them in process memory. Do not describe snapshots as credential-free. Review copied credentials and rotate upstream when required before sharing or retaining them.
21
+ - A disk snapshot includes credential files written by applications, tool sign-ins, and historical Vault files; a suspend snapshot can also retain credentials in process memory. Org Secrets reach the box only as placeholders, so a snapshot holds none of their values, but anything an app or a person wrote to disk is kept. Do not describe snapshots as credential-free. Review copied credentials and rotate upstream when required before sharing or retaining them.
22
22
  - Pick names that say what is inside (`node20-playwright`, `api-db-seeded`); names must be unique among your snapshots.
23
23
 
24
24
  ## Load
@@ -50,13 +50,13 @@ instabox create --name work2 --snapshot work-copy
50
50
  instabox snapshot rm work-copy # optional; work2 is not affected
51
51
  ```
52
52
 
53
- The copy gets a new sandbox identity and the selections in its creation request. Without Secret selection flags it keeps the existing Vault behavior and does not enroll in org Secrets; it does not inherit the source's explicit grants or exclusions. To disable new delivery from both stores, use:
53
+ The copy gets a new sandbox identity and the Secret selection in its creation request. Without `--secrets` it keeps the existing Vault behavior and does not enroll in org Secrets; it does not inherit the source's selection. To give it neither store's credentials, use:
54
54
 
55
55
  ```sh
56
- instabox create --name work2 --snapshot work-copy --no-vault --secrets none --env-secrets none
56
+ instabox create --name work2 --snapshot work-copy --no-vault --secrets none
57
57
  ```
58
58
 
59
- To provide a database credential, select its env Secret explicitly with `--env-secrets DATABASE_URL`. At creation that flag alone also selects all org proxy Secrets; add `--secrets none` or specific names to narrow proxy access. Copied placeholders cannot authorize use as the source sandbox. New selections do not erase real credentials already present in copied files; a new proxy identity does not make those files safe to share.
59
+ To give the copy API credentials, name them with `--secrets NAME,NAME`. Placeholders copied from the source box do not work for the new one. New selections do not erase credentials that apps or people already wrote into files on the copied disks; review those before sharing the snapshot or the box.
60
60
 
61
61
  ## Clean up
62
62