instabox 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/instabox.mjs CHANGED
@@ -34,7 +34,8 @@ const API = (
34
34
  process.env.INSTABOX_API ||
35
35
  'https://api.instabox.sh'
36
36
  ).replace(/\/$/, '');
37
- const IMAGE = process.env.INSTABOX_IMAGE || 'universal';
37
+ // The template a new box starts from: the CLI's and dashboard's default (shown as standard).
38
+ const IMAGE = process.env.INSTABOX_IMAGE || 'agent-browser';
38
39
  // SSH certificate lifetime for a box with no timeout of its own.
39
40
  const CERT_MIN = 120;
40
41
  // With no provider selected, the browser confirms its existing Instabox account
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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": {
@@ -7,7 +7,7 @@ metadata:
7
7
 
8
8
  # instabox CLI
9
9
 
10
- An instabox sandbox is a Linux VM (Debian, one user with passwordless sudo: named after your local username when created with `instabox create`, `user` from the dashboard, `dev` on older boxes; `instabox get <box>` shows it). It has a root disk and a persistent `/data` disk; HOME is `/data/home`. Boxes are private to the signed-in account.
10
+ An instabox sandbox is a Linux VM (Debian, one user with passwordless sudo: named after your local username when created with `instabox create`, `user` from the dashboard, `dev` on older boxes; `instabox get <box>` shows it). It has a root disk and a persistent `/data` disk; HOME is `/data/home`. Each box belongs to an organization; access follows its membership and sandbox permissions.
11
11
 
12
12
  ## Sign in
13
13
 
@@ -16,19 +16,31 @@ instabox whoami # who is signed in (fails if nobody)
16
16
  instabox login # confirm the browser's account, or choose GitHub / Google
17
17
  ```
18
18
 
19
- A GitHub sign-in and a Google sign-in 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` 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.
20
20
 
21
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.
22
22
 
23
23
  ## Templates
24
24
 
25
- `universal` is the default, so `create` needs no `--template` for ordinary work. It is Debian with git, gh, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker, Claude Code and Codex; a desktop (VNC, Chromium) installs itself the first time it is used. Pick another template only for its one purpose:
25
+ `agent-browser` is the default, so `create` needs no `--template` for ordinary work. It is Debian with git, gh, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker, Claude Code and Codex, plus a desktop (VNC, Chromium) ready for browsing or driving a GUI from the start. The dashboard shows it as **standard**. Pick another template only for its one purpose:
26
26
 
27
- - `agent-browser`: universal with the desktop already installed, for browsing or driving a GUI from the start.
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
30
 
31
- `instabox templates` lists the ones this server offers.
31
+ `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
+ ## Sizes
34
+
35
+ A box is 4 vCPU / 8 GB of memory unless `create` names another size of the account's plan with both `--vcpus` and `--mem-mib`:
36
+
37
+ | Plan | Sizes (`--vcpus` / `--mem-mib`) |
38
+ | ---- | --------------------------------------- |
39
+ | Free | 4 / 8192 (default), 2 / 4096 |
40
+ | Pro | 2 / 4096, 4 / 8192 (default), 8 / 16384 |
41
+ | Team | 4 / 8192 (default) up to 32 / 65536 |
42
+
43
+ `instabox whoami --json` lists the sizes this account may create as `allowedSpecs` (the default first, then the plan's others and any an admin granted); check it rather than assuming the plan. Another size is refused with `400 spec_not_allowed`, whose message names the account's sizes. Pick a size for the work: 2 vCPU for light editing or a single agent, the default for most builds, 8 vCPU (Pro) for heavy builds or many processes. `--snapshot` takes the same flags. The size is fixed once the box exists; for another size, save a snapshot and create a new box from it with the size flags.
32
44
 
33
45
  ## Everyday commands
34
46
 
@@ -40,39 +52,50 @@ instabox ssh work # interactive shell (leave with:
40
52
  instabox exec work -- uname -a # one command, no SSH client needed
41
53
  instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
42
54
  instabox files work /data/home # list a directory (or print a text file)
43
- instabox auto-suspend work 10m # pause after 10 idle minutes (default 30m; 0: never)
55
+ instabox auto-suspend work 10m # idle time before it pauses (fixed at 30m unless an admin gave the account manual lifecycle)
44
56
  instabox timeout work 2h --on-timeout pause # pause (keeps memory) or kill at the deadline
45
57
  instabox stop work && instabox start work # stop ends processes, keeps disks
46
58
  instabox suspend work && instabox resume work # pause/resume keeps running processes
59
+ instabox rename work api # rename in place, any state; ID and port URLs stay
47
60
  instabox rm work # delete the box
48
61
  ```
49
62
 
50
63
  - Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
51
- - `create` options: `--template T` or `--snapshot S`, `--auto-suspend D|0`, `--timeout D [--on-timeout pause|kill]`, `--meta K=V`, `--no-vault`, `--no-wait`. Every box is the same size (`instabox usage` shows it); there are no size flags.
52
- - A new 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. `--auto-suspend 0` keeps a box always on (a server that must keep answering); `--auto-suspend 2h` changes the idle time.
64
+ - `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--timeout D [--on-timeout pause|kill]`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--env-secrets NAME,NAME|none`, `--no-wait`.
65
+ - 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. Timeouts are capped at 4h the same way. Do not try to keep a box on by other means; a paused box resumes on the next command.
53
66
  - Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
54
67
  - `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'`.
55
68
  - 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.
56
69
  - 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.
57
70
 
58
- ## Vault: the user's secrets in every box
71
+ ## Organization Secrets
72
+
73
+ Secrets are encrypted key=value entries owned by an org. Each entry has one delivery mode:
74
+
75
+ - `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.
76
+ - `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.
59
77
 
60
- The user's Vault holds their secrets (API keys for model providers, a `.env`, a service-account file) on the server and writes them into every box of theirs when it boots or resumes: `env` items as environment variables of every login shell (so of `instabox exec` too), `file` items as files under HOME.
78
+ 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.
61
79
 
62
80
  ```sh
63
- instabox vault show work # what the box gets (names, never values) and the last write
64
- instabox vault push work # write the current items into a running box now
65
- instabox vault set ANTHROPIC_API_KEY # value from stdin, or prompted without echo (sign-in only)
66
- instabox vault set-file .config/svc.json --from ./svc.json
67
- instabox vault ls; instabox vault rm NAME
68
- instabox create --name tenant --no-vault # a box for someone else: never gets the user's items
81
+ instabox secrets ls --org team # metadata only
82
+ instabox secrets show work # selection and independent proxy/guest status
83
+ instabox secrets set SERVICE_API_KEY --org team --delivery proxy --host api.example.com
84
+ instabox secrets set DATABASE_URL --org team --delivery env
85
+ instabox create --name app --org team --secrets SERVICE_API_KEY --env-secrets DATABASE_URL
86
+ instabox secrets select work --env-secrets DATABASE_URL
87
+ instabox secrets push work # refresh managed env/trust on a running box
88
+ instabox create --name tenant --no-vault --secrets none --env-secrets none
69
89
  ```
70
90
 
71
- - Before asking the user for a credential, check names with `instabox vault show <box>`. Inside the box, use silent presence checks: `test "${NAME+x}" = x` for an environment variable (replace `NAME` with its name), or `test -f "$HOME/.config/svc.json"` for a credential file. Exit status 0 means present, 1 means absent. Never print credential values or read credential-file contents into agent output, chat or logs.
72
- - A changed item reaches the box at its next boot or resume, or at once with `vault push` (a changed `env` item reaches new shells only).
73
- - A box made for another person or tenant must be created with `--no-vault` (`vault: false` in the API): anything running in it can read what the Vault writes. This is for good; `instabox start|resume <box> --no-vault` does the same to an existing box.
74
- - An item's selector (`--selector K=V`) limits it to boxes whose metadata has those labels: create the box with the `--meta` the user's items expect.
75
- - Managing items needs a sign-in; an API key cannot read, create or change them. Never ask for a secret's value to put it somewhere else.
91
+ - 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.
92
+ - 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.
93
+ - 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`.
94
+ - 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.
95
+ - 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.
96
+ - 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.
97
+ - 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.
98
+ - 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.
76
99
 
77
100
  ## Moving work onto a box
78
101
 
@@ -1,108 +1,108 @@
1
1
  ---
2
2
  name: instabox-basic-setup
3
- description: Use when the user wants their instabox boxes set up like their laptop — "put my tokens / dotfiles / git config into instabox", "set up my boxes like my laptop", "give every box my GitHub and Claude tokens", "move my setup to instabox", "move everything to instabox". Scans this machine, lets the user pick from a checkbox plan, then puts the picks into their instabox Vault, so every box (and every box created later) gets them: tokens (GH_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, model and service API keys) as environment variables, and git and editor config (~/.gitconfig, global gitignore, ~/.vimrc, nvim, ~/.tmux.conf) as files. 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 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.
4
4
  metadata:
5
5
  installed-by: instabox
6
6
  ---
7
7
 
8
- # Put a basic setup into the instabox Vault
8
+ # Basic setup with org Secrets and sandbox config copies
9
9
 
10
- This skill puts the user's personal setup into their instabox **Vault** (`instabox vault`: per-user secrets and files, written into their boxes), so every box of theirs starts with it: the ones running now (at their next start or resume, or at once with `instabox vault push <box>`) and every box created later. It copies no repository, no agent history, installs nothing and touches no box directly. To move a repository, use the `instabox-move-repo` skill. See the `instabox` skill for the CLI.
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.
11
11
 
12
- What it moves, and nothing else:
12
+ 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
13
 
14
- | Group | Becomes | From |
14
+ | Group | Destination | Sources |
15
15
  | --- | --- | --- |
16
- | Tokens | Vault `env` items: on tmpfs in the box, set in every login shell; gone at a stop, back at the next start; never in a snapshot | `GH_TOKEN` from `gh auth token`; well-known model and service tokens exported in the shell files (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `OPENROUTER_API_KEY`, `HF_TOKEN`, `NPM_TOKEN`, `VERCEL_TOKEN`, `CLOUDFLARE_API_TOKEN`, `SUPABASE_ACCESS_TOKEN`, `FLY_API_TOKEN`, `SENTRY_AUTH_TOKEN`, `LINEAR_API_KEY`, ...) or in this shell's environment. Other credential-looking exports are listed unchecked. |
17
- | Git | Vault `file` items in the box's home | `~/.gitconfig` (and `~/.config/git/config`), the global gitignore |
18
- | Editor and terminal | Vault `file` items | `~/.vimrc`, nvim's `init.lua` / `init.vim` and the `.lua` / `.vim` files around it (no plugin checkouts), `~/.tmux.conf` |
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 |
17
+ | Git | Files under the selected sandbox's home | `.gitconfig`, `.config/git/config`, global gitignore |
18
+ | Editor and terminal | Files under the selected sandbox's home | `.vimrc`, nvim config without plugin checkouts, `.tmux.conf` |
19
19
 
20
- Four scripts in `scripts/` next to this file do the parts that must be exact (Node 18+, nothing to install):
20
+ The scripts beside this file require Node 18+ locally and Python 3 in sandboxes receiving config files:
21
21
 
22
- | Script | Does |
22
+ | Script | Responsibility |
23
23
  | --- | --- |
24
- | `scan.mjs` | Read-only inventory, as JSON: tokens by name and place (never values), the files, and what cannot move. |
25
- | `plan.mjs` | `write`: the checkbox plan. `parse`: reads the edited plan back, strictly, as JSON. |
26
- | `push.mjs` | Puts the picks into the Vault, piping each value from its source into `instabox vault set` / `set-file`. `--dry-run` first. |
27
- | `content.mjs` | What a file becomes in the Vault (used by the other two). |
24
+ | `scan.mjs` | Read-only inventory of names, source locations, sizes and manual setup requirements; no credential values in output |
25
+ | `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 |
27
+ | `content.mjs` | Rewrite HOME paths, remove recognized credential lines, and adapt git/editor configuration for Linux |
28
+ | `install-files.py` | Install ordinary config files privately, refusing symlinks and unsafe paths; no Secret-store or legacy Vault access |
28
29
 
29
- **Never read, print or paste a credential value yourself**: not `gh auth token`, not `echo $OPENAI_API_KEY`, not `cat` of a shell file or of `~/.gitconfig`, never the macOS keychain. `push.mjs` is the only thing that touches values. If something needs a value the scripts cannot get, tell the user the command to run themselves (`instabox vault set NAME` prompts without echo).
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.
30
31
 
31
- Before starting, tell the user plainly: a scan of this machine (nothing changes here), a plan file they edit, then their Vault gets what they checked. By default every box they have and will create gets it; they can scope it with a selector. Nothing is copied from a box back here, and nothing keeps the two in sync: to change something later, change it here and run this again.
32
-
33
- ## 1. Scan
32
+ ## 1. Scan and choose the org
34
33
 
35
34
  ```bash
36
- instabox whoami # signed in? otherwise: instabox login
37
- S=~/.claude/skills/instabox-basic-setup/scripts # or ~/.agents/skills/instabox-basic-setup/scripts: wherever this file is
38
- W=~/instabox-basic-setup && mkdir -p -m 700 "$W"
35
+ instabox whoami
36
+ instabox org ls --json
37
+ S=~/.claude/skills/instabox-basic-setup/scripts # or the installed ~/.agents/skills path
38
+ W=$(mktemp -d)
39
39
  node "$S/scan.mjs" > "$W/scan.json"
40
- instabox vault ls --json > "$W/vault.json" # what is in the Vault already (names, no values)
40
+ ORG_ID=<stable-org-id>
41
+ instabox secrets ls --org "$ORG_ID" --json > "$W/secrets.json"
41
42
  instabox usage --json | jq '.limits.vault // {}' > "$W/limits.json"
42
43
  ```
43
44
 
44
- `instabox vault ls` fails with `API keys cannot manage the vault` when `INSTABOX_API_KEY` is set: ask the user to unset it and `instabox login`. A `404` means the server has no Vault: stop and say so.
45
+ Saving Secret values requires an org owner/admin sign-in. An API key can read metadata but cannot import values. Do not change the user's authentication silently: if value management is rejected, explain that they need an owner/admin session. A 404 for Secret routes means Secret management is not configured on that server.
46
+
47
+ Use `instabox get <box> --json` to resolve each intended sandbox to its stable `m_...` ID and confirm its `orgId`. No sandbox is required when the user only wants to save credentials. Config copying requires explicitly named running sandboxes; do not create a box merely to verify setup unless creation is authorized.
45
48
 
46
- ## 2. Write the plan and let the user choose
49
+ ## 2. Prepare a concrete plan
47
50
 
48
51
  ```bash
49
- node "$S/plan.mjs" write --scan "$W/scan.json" --vault "$W/vault.json" > "$W/plan.md"
52
+ node "$S/plan.mjs" write --scan "$W/scan.json" --secrets "$W/secrets.json" \
53
+ --org "$ORG_ID" --sandbox <sandbox-id> > "$W/plan.md"
50
54
  ```
51
55
 
52
- Add `--selector key=value` (comma-separated for more) only if the user wants the setup on some boxes, not all; the plan's `Selector:` line can be edited too (`*` is every box). A box gets the items when its metadata has every label (`instabox create --meta key=value`).
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.
53
57
 
54
- `plan.md` has a `- [x]` / `- [ ]` line per item, in three sections (Tokens, Git, Editor and terminal), then a "Can't move" list with no checkboxes. Defaults:
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
+ ```
55
64
 
56
- - Well-known tokens and every file are checked. Other credential-looking exports, and tokens the shell reads from the macOS keychain, are unchecked (checking the latter runs that keychain lookup once, at push time).
57
- - Something already in the Vault, set there some other way than this skill, is unchecked and says so: checking it **replaces** it (for every box its selector reached). Items this skill set on an earlier run are replaced without asking (the manifest, step 3).
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
66
 
59
- Show the user a short summary (which tokens, which files, what cannot move) and the path of the file, and ask them to edit the checkboxes and the selector, then tell you to go on. In Claude Code you may ask a coarse question first with AskUserQuestion (multiSelect: "Tokens / Git / Editor and terminal") and apply the answer by changing only `[x]` and `[ ]`; the user still gets the file for details.
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
68
 
61
- When they are done, read it back:
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.
70
+
71
+ Parse the approved plan:
62
72
 
63
73
  ```bash
64
- node "$S/plan.mjs" parse --scan "$W/scan.json" --vault "$W/vault.json" --limits "$W/limits.json" "$W/plan.md" > "$W/selection.json"
74
+ node "$S/plan.mjs" parse --scan "$W/scan.json" --secrets "$W/secrets.json" \
75
+ --org "$ORG_ID" --limits "$W/limits.json" "$W/plan.md" > "$W/selection.json"
65
76
  ```
66
77
 
67
- - Exit 1: the plan has problems (a malformed checkbox, a line deleted or duplicated, an unknown id, a bad selector, a plan from another scan or Vault state, a file that became empty or over 64 KiB). All are listed; show them, fix or let the user fix, parse again. Never fix a plan by editing ids or deleting lines. If the scan or Vault changed, run step 1 and 2 again.
68
- - Exit 3: over the Vault's limits (100 items, 256 KiB in all, items already there included; 64 KiB per item). The message gives the totals and the biggest lines: ask the user what to uncheck (nvim's files are one item each), or which old Vault items to remove (`instabox vault rm NAME`), then parse again.
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.
69
79
 
70
- ## 3. Push into the Vault
80
+ ## 3. Dry-run, then apply the approved selection
71
81
 
72
82
  ```bash
73
- node "$S/push.mjs" --selection "$W/selection.json" --dry-run # names, sizes, totals; writes nothing
83
+ node "$S/push.mjs" --selection "$W/selection.json" --dry-run
74
84
  node "$S/push.mjs" --selection "$W/selection.json"
75
85
  ```
76
86
 
77
- Show the user the dry run's lines before the real run. Values go from their source (`gh auth token`, a variable after sourcing the user's shell file, this environment) straight into the CLI's stdin. A shell file that only exports when interactive (the usual `case $- in *i*)` guard) is sourced a second time in an interactive shell, which may run its prompt setup; a token it still does not yield comes from this environment, or fails with the reason; the output is names, sizes and outcomes. Every value is read and the totals checked before anything is written: over the limits, nothing is written (exit 3). An item that appeared in the Vault after the plan was written is left alone (`kept`). A `failed` line names the item and why; give the user the command to set it themselves (`instabox vault set NAME`, which prompts without echo). Re-running is safe: the same items are set again.
87
+ The dry-run reads the approved credential sources and prepares filtered config, but makes no writes to InstaBox and does not change its local manifest. Sourcing a shell startup file may run its local initialization code; an approved keychain-backed source may invoke its lookup. Values remain in memory and CLI stdin, never in the plan, manifest, arguments or logs. Review the dry-run's metadata before applying any action not yet authorized.
78
88
 
79
- The run records what it set in `~/.config/instabox-basic-setup/pushed.json` (names, kinds, paths; no values), so the next run knows those are its own.
89
+ 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.
80
90
 
81
- What a file becomes (counted per file in the plan's sizes):
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.
82
92
 
83
- - This machine's HOME is rewritten to the box's (`/data/home`); lines that look like a credential are replaced by a comment; macOS-only lines (`osxkeychain`, Homebrew paths, `pbcopy`, `reattach-to-user-namespace`, ...) are commented out.
84
- - `~/.gitconfig` also: commit and tag signing is turned off (`gpgsign`: the signing key is not on the box), `url."git@...".insteadOf` rewrites are commented out (the box has no SSH key), and when `GH_TOKEN` is in the Vault, gh's credential helper for github.com is added (what `gh auth setup-git` writes), so `git clone https://github.com/...` and `git push` use the token.
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.
85
94
 
86
- ## 4. Verify on a box (optional)
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.
87
96
 
88
- If the user names a running box (`instabox ls`), or wants one to try, write the Vault into it now and look:
97
+ ## 4. Verify and report
89
98
 
90
99
  ```bash
91
- BOX=<box>
92
- instabox vault push "$BOX" # writes now; lists variables and files written, and any "kept" path
93
- instabox vault show "$BOX" # what it gets with its metadata
94
- instabox exec "$BOX" -- 'gh auth status; git config --get user.name; test -n "${CLAUDE_CODE_OAUTH_TOKEN-}${ANTHROPIC_API_KEY-}" && echo claude token set'
100
+ instabox secrets show <box>
101
+ instabox exec <box> -- 'test "${GH_TOKEN+x}" = x && test -f "$HOME/.gitconfig"'
95
102
  ```
96
103
 
97
- A `kept: ~/path (not written: ... symlink ...)` line means the box has a symlink in that path; tell the user. Check presence of a token, never print one. A box created with `--no-vault` gets nothing, ever; one whose metadata does not match the selector gets nothing either. Do not create a box for this unless the user asks.
98
-
99
- ## 5. Report
100
-
101
- Tell the user, in short sections:
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.
102
105
 
103
- - **In your Vault**: each token by name, each file by path, and which boxes get them (every box, or the selector). Running boxes get them at their next start or resume, or now with `instabox vault push <box>`; new boxes at once.
104
- - **Left alone**: what they unchecked, items kept because they were set some other way, failures with the reason. Items from an earlier run they unchecked this time are still in the Vault (the push prints them): `instabox vault rm NAME` removes one, and boxes lose it at their next write.
105
- - **Do these yourself**: every entry of `.manual` in `selection.json`, with its command, for example: Claude Code's sign-in — run `claude setup-token` here yourself, then `instabox vault set CLAUDE_CODE_OAUTH_TOKEN` and paste it (or `claude`, then `/login`, on each box); Codex — `codex login --device-auth` on the box, or `instabox vault set OPENAI_API_KEY`; `gh auth login` here if gh is not signed in.
106
- - **Good to know**: Vault files are written again at every start and resume, so an edit made to them on a box is replaced; change them here and run this skill again (or `instabox vault rm` the item to keep a box's own copy). Tokens are environment variables of login shells (SSH, `instabox exec`, the terminal), not of systemd services. Shell aliases, SSH keys, cloud credential files and agent config are not part of this setup.
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.
107
107
 
108
- Delete `$W` when the user is done with it (it holds the scan, the plan and the Vault's item names, no values).
108
+ 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.
@@ -1,4 +1,4 @@
1
- // What a file item's content becomes in the Vault: the local file with this
1
+ // What a directly copied configuration file becomes: the local file with this
2
2
  // machine's HOME rewritten to the box's, lines that look like credentials
3
3
  // removed and macOS-only lines commented out. ~/.gitconfig also gets a few
4
4
  // changes so git works on a box that has GH_TOKEN and no SSH key.
@@ -43,7 +43,7 @@ export function scrubText(text, { path, home }) {
43
43
  // The credential helper `gh auth setup-git` writes: git over HTTPS uses GH_TOKEN through gh.
44
44
  export const GH_HELPER = [
45
45
  '',
46
- `# ${TAG}: git over HTTPS to GitHub uses GH_TOKEN (from the Vault) through gh`,
46
+ `# ${TAG}: git over HTTPS to GitHub uses an explicitly selected env GH_TOKEN through gh`,
47
47
  '[credential "https://github.com"]',
48
48
  '\thelper =',
49
49
  '\thelper = !gh auth git-credential',
@@ -53,7 +53,7 @@ export const GH_HELPER = [
53
53
  '',
54
54
  ].join('\n');
55
55
 
56
- // ~/.gitconfig for the box. ghToken: GH_TOKEN goes to the Vault too, so the gh helper is added.
56
+ // ~/.gitconfig for the box. ghToken: GH_TOKEN is explicitly selected for env delivery, so the gh helper is added.
57
57
  // Changes, besides scrubText's:
58
58
  // - commit/tag signing is turned off: the signing key (GPG, SSH, 1Password) is not on the box;
59
59
  // - url.<ssh>.insteadOf rewrites are commented out: the box has no SSH key, and they would
@@ -90,9 +90,14 @@ export function fileContent(f, { home, ghToken = false } = {}) {
90
90
  } catch (e) {
91
91
  return { error: `${f.src}: ${e.code || 'unreadable'}` };
92
92
  }
93
- if (buf.includes(0)) return { error: `${f.src}: a binary file (the Vault holds text only)` };
93
+ if (buf.includes(0)) return { error: `${f.src}: a binary file (config copying supports UTF-8 text only)` };
94
94
  const text = buf.toString('utf8');
95
+ if (!Buffer.from(text).equals(buf)) return { error: `${f.src}: invalid UTF-8 text` };
95
96
  if (f.transform === 'gitconfig') return gitconfigText(text, { home, ghToken });
96
- if (f.transform === 'raw') return { text, secrets: 0, macos: 0 };
97
+ if (f.transform === 'raw') {
98
+ if (SECRET_VALUE.test(text) || text.split('\n').some((line) => SECRET_KEY_LINE.test(line)))
99
+ return { error: `${f.src}: possible credential in structured config; review it separately` };
100
+ return { text, secrets: 0, macos: 0 };
101
+ }
97
102
  return scrubText(text, { path: f.path, home });
98
103
  }
@@ -0,0 +1,82 @@
1
+ # One-time config copying, run as the sandbox user. This is not Secret delivery.
2
+ import json
3
+ import os
4
+ import stat
5
+ import sys
6
+
7
+ HOME = '/data/home'
8
+ MARKER = 'INSTABOX-BASIC-SETUP '
9
+ DIRECTORY = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
10
+
11
+
12
+ def valid_path(path):
13
+ return (isinstance(path, str) and 0 < len(path) <= 255
14
+ and not any(ord(c) < 32 or ord(c) == 127 or c == '\\' for c in path)
15
+ and all(p and p != '.' and not p.startswith('..') for p in path.split('/')))
16
+
17
+
18
+ def install_files(files, home=HOME):
19
+ results = []
20
+ for entry in files:
21
+ path = entry.get('path')
22
+ result = {'path': path, 'status': 'failed'}
23
+ results.append(result)
24
+ if not valid_path(path) or not isinstance(entry.get('text'), str):
25
+ continue
26
+ data = entry['text'].encode('utf-8')
27
+ if len(data) > 64 * 1024 or b'\x00' in data:
28
+ continue
29
+ fd = None
30
+ temporary = None
31
+ try:
32
+ fd = os.open(home, DIRECTORY)
33
+ parts = path.split('/')
34
+ for part in parts[:-1]:
35
+ try:
36
+ os.mkdir(part, 0o700, dir_fd=fd)
37
+ except FileExistsError:
38
+ pass
39
+ child = os.open(part, DIRECTORY, dir_fd=fd)
40
+ os.close(fd)
41
+ fd = child
42
+ name = parts[-1]
43
+ try:
44
+ current = os.stat(name, dir_fd=fd, follow_symlinks=False)
45
+ if not stat.S_ISREG(current.st_mode):
46
+ continue
47
+ except FileNotFoundError:
48
+ pass
49
+ temporary = '.instabox-setup-' + os.urandom(8).hex()
50
+ output = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=fd)
51
+ with os.fdopen(output, 'wb') as stream:
52
+ stream.write(data)
53
+ stream.flush()
54
+ os.fsync(stream.fileno())
55
+ os.fchmod(stream.fileno(), 0o600)
56
+ os.rename(temporary, name, src_dir_fd=fd, dst_dir_fd=fd)
57
+ temporary = None
58
+ result['status'] = 'written'
59
+ except Exception:
60
+ # The remote shell and filesystem are untrusted. Return only fixed
61
+ # outcomes; exception text and config contents never reach logs.
62
+ pass
63
+ finally:
64
+ if fd is not None:
65
+ if temporary:
66
+ try:
67
+ os.unlink(temporary, dir_fd=fd)
68
+ except OSError:
69
+ pass
70
+ os.close(fd)
71
+ return results
72
+
73
+
74
+ if __name__ == '__main__':
75
+ try:
76
+ payload = json.load(sys.stdin)
77
+ result = install_files(payload['files'])
78
+ sys.stdout.write(MARKER + json.dumps({'files': result}) + '\n')
79
+ sys.exit(0 if all(item['status'] == 'written' for item in result) else 1)
80
+ except Exception:
81
+ sys.stdout.write(MARKER + '{"error":"config copy failed"}\n')
82
+ sys.exit(1)