instabox 0.6.3 → 0.6.4
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/package.json +1 -1
- package/skills/instabox/SKILL.md +29 -24
- package/skills/instabox-basic-setup/SKILL.md +16 -23
- package/skills/instabox-basic-setup/scripts/content.mjs +6 -22
- package/skills/instabox-basic-setup/scripts/lib.mjs +49 -68
- package/skills/instabox-basic-setup/scripts/plan.mjs +17 -48
- package/skills/instabox-basic-setup/scripts/push.mjs +29 -32
- package/skills/instabox-basic-setup/scripts/scan.mjs +41 -32
- package/skills/instabox-move-repo/SKILL.md +7 -30
- package/skills/instabox-move-repo/scripts/deps.sh +1 -1
- package/skills/instabox-move-repo/scripts/repo-files.mjs +2 -2
- package/skills/instabox-snapshots/SKILL.md +4 -4
package/package.json
CHANGED
package/skills/instabox/SKILL.md
CHANGED
|
@@ -18,7 +18,7 @@ instabox login # confirm the browser's account, or choose GitHub /
|
|
|
18
18
|
|
|
19
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
|
-
For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it
|
|
21
|
+
For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it in the dashboard: Profile → API keys (account menu)). Never print, commit or log a key.
|
|
22
22
|
|
|
23
23
|
## Templates
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ For CI or an agent without a browser, use an API key instead: `export INSTABOX_A
|
|
|
27
27
|
- `universal`: the same tools without the desktop, which installs itself the first time it is used (about half a minute); a smaller root disk. The dashboard shows it as **basic**.
|
|
28
28
|
- `herdr`: for the herdr client.
|
|
29
29
|
- `orca`: for Orca desktop and mobile.
|
|
30
|
+
- `t3`: for the T3 Code desktop and mobile clients.
|
|
30
31
|
|
|
31
32
|
`instabox templates` lists the ones this server offers. The free plan creates boxes from `agent-browser` only: another template is refused with `403 template_not_allowed` and needs a paid plan (the user upgrades on the dashboard). A box from a snapshot (`--snapshot`) works on every plan, whatever template the snapshot came from. Do not retry another template on the free plan; use `agent-browser`.
|
|
32
33
|
|
|
@@ -52,7 +53,9 @@ instabox ssh work # interactive shell (leave with:
|
|
|
52
53
|
instabox exec work -- uname -a # one command, no SSH client needed
|
|
53
54
|
instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
|
|
54
55
|
instabox files work /data/home # list a directory (or print a text file)
|
|
55
|
-
instabox auto-suspend work 10m # idle time before
|
|
56
|
+
instabox auto-suspend work 10m # idle time before a paid box pauses
|
|
57
|
+
instabox max-run work 120 # pause a paid box after two running hours
|
|
58
|
+
instabox max-run work -1 # remove the runtime cap; idle policy still applies
|
|
56
59
|
instabox stop work && instabox start work # stop ends processes, keeps disks
|
|
57
60
|
instabox suspend work && instabox resume work # pause/resume keeps running processes
|
|
58
61
|
instabox rename work api # rename in place, any state; ID and port URLs stay
|
|
@@ -60,41 +63,43 @@ instabox rm work # delete the box
|
|
|
60
63
|
```
|
|
61
64
|
|
|
62
65
|
- Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
|
|
63
|
-
- `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--
|
|
64
|
-
-
|
|
66
|
+
- `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--max-run-minutes N`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--no-wait`.
|
|
67
|
+
- Idle auto-pause defaults to 30 minutes. Paid boxes allow 5, 10, 15, 20, 25 or 30 minutes, or `0` to turn it off. Free boxes keep 30 minutes unless an admin grants an idle override. Idle means no agent working, no terminal use and little CPU.
|
|
68
|
+
- Max run minutes is independent of idle activity: `-1` is unlimited, `0` pauses immediately, and a positive integer forces a pause after that many running minutes. Free boxes have a fixed 480-minute cap that only an admin session can change; paid boxes default to `-1` and can change it. An idle override does not permit changes to a free box's runtime cap. The first limit reached pauses the box; both timers restart after sleep and wake.
|
|
69
|
+
- A runtime pause blocks every wake path during a server-configured cooldown, 10 minutes by default. Wait until the `cooldownUntil` deadline shown by `instabox get`; edits cannot clear it. Afterward, an automatically paused box wakes on `exec`, `ssh`, `files`, a public port request or `instabox wake <box>`. A zero runtime cap must be changed before waking. A normal pause preserves memory and processes.
|
|
65
70
|
- Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
|
|
66
|
-
- `exec` 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
|
|
71
|
+
- `exec` returns the command's exit code and takes `--stdin` to pipe input. It stops the command after `--timeout` seconds: 60 by default, 600 at most. Run longer commands (installs, builds, test suites) over SSH, which also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`. The box's runtime cap still applies during SSH or exec.
|
|
67
72
|
- The box's HOME has `MACHINE.md` (which box it is, its size, what survives a stop, the installed tools) and `AGENTS.md`. Both are worth reading before working in the box; they appear a moment after it starts.
|
|
68
73
|
- A public URL for a web server in the box: `instabox port publish work 3000` prints it (`instabox port ls`, `instabox port unpublish work 3000`). The URL works while the box runs.
|
|
69
74
|
|
|
70
75
|
## Organization Secrets
|
|
71
76
|
|
|
72
|
-
Secrets are encrypted
|
|
77
|
+
Secrets are encrypted API credentials owned by an org, each limited to the HTTPS hosts it may be sent to. A box that selects a Secret gets a placeholder in its environment, never the credential itself. When a request from the box goes to one of the Secret's allowed hosts over HTTPS, the outbound proxy swaps the placeholder in the request headers for the credential. Code in the box can call the API but cannot read or copy the credential. No header-name configuration is needed.
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
- `env`: the box receives the real value. Use this explicitly for native database credentials, local signing, and clients that cannot use header substitution. Code running in the box can read and copy the value.
|
|
79
|
+
Secrets suit HTTP API keys and tokens that a client sends in a request header: model providers, GitHub's API, deploy and SaaS APIs. They do not cover database URLs or passwords, signing keys, or anything a client hashes, encodes or sends outside an HTTPS header; those need their own setup on the box.
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
Secrets replace the Vault, which is legacy and will be retired: save every new API credential as a Secret, never in the Vault, and do not suggest `vault set`, `set-file` or `import` to users. A new box without `--secrets` still gets an existing user's Vault items (see the last point below). Opt into Secrets with `create --secrets`, or use `secrets select` later. Metadata labels do not select Secrets, and Secrets do not deliver files.
|
|
78
82
|
|
|
79
83
|
```sh
|
|
80
84
|
instabox secrets ls --org team # metadata only
|
|
81
|
-
instabox secrets show work
|
|
82
|
-
instabox secrets set SERVICE_API_KEY --org team --
|
|
83
|
-
instabox secrets
|
|
84
|
-
instabox create --name app --org team --secrets SERVICE_API_KEY
|
|
85
|
-
instabox secrets select work --
|
|
86
|
-
instabox secrets push work
|
|
87
|
-
instabox create --name tenant --no-vault --secrets none
|
|
85
|
+
instabox secrets show work # selection and proxy status
|
|
86
|
+
instabox secrets set SERVICE_API_KEY --org team --host api.example.com
|
|
87
|
+
instabox secrets import .env --org team --host api.example.com # every value gets these hosts
|
|
88
|
+
instabox create --name app --org team --secrets SERVICE_API_KEY
|
|
89
|
+
instabox secrets select work --secrets SERVICE_API_KEY,OTHER_API_KEY
|
|
90
|
+
instabox secrets push work # refresh the selection on a running box
|
|
91
|
+
instabox create --name tenant --no-vault --secrets none
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
- Values for `set` come from stdin or a hidden prompt. Never print a credential or put its value into agent output, chat, command arguments, or logs. Before asking for a credential, check metadata with `secrets ls --org <org>` and `secrets show <box>`. A silent check such as `test "${NAME+x}" = x` tests
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- For
|
|
97
|
-
-
|
|
94
|
+
- 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
|
+
- 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
|
+
- `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
|
+
- 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.
|
|
99
|
+
- Rotating a value keeps its placeholder, so running clients need no change. Newly selected names appear in new shells that load the managed environment; restart existing applications to pick them up.
|
|
100
|
+
- For a box intended for another person or tenant, disable personal Vault injection with `--no-vault` and select only the org Secrets they may use. For a fresh box with neither system's credentials, use `--no-vault --secrets none`. An empty Secret selection alone still permits Vault injection. These flags do not remove credentials already copied into a source snapshot.
|
|
101
|
+
- For an existing box, disable Vault with `start BOX --no-vault` or `resume BOX --no-vault` and clear the Secret selection with `secrets select BOX --secrets none`. Refresh a running guest with both `vault push BOX` and `secrets push BOX`. Vault values already copied into processes or files remain there; disabling Vault does not recover them.
|
|
102
|
+
- The Vault is legacy and will be retired; every `vault` command prints a warning. Its commands remain only for items a user already saved: `vault ls`, `show`, `push` and `rm`, and `vault export` to recover a retained item. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store copies or deletes the other. To move a Vault env item that is an HTTP API credential, the user enters its value again as a Secret with its hosts (`secrets set NAME --host HOST` reads it from a hidden prompt), you select it on the box, then `vault rm` it with the user's go-ahead. Never read a Vault value to copy it. Database credentials and other non-HTTP secrets are not supported by Secrets and need their own setup on the box; Vault file items likewise.
|
|
98
103
|
|
|
99
104
|
## Moving work onto a box
|
|
100
105
|
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: instabox-basic-setup
|
|
3
|
-
description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves
|
|
3
|
+
description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves known API tokens as org Secrets for their service's HTTPS API hosts, selects them on named sandboxes, and copies approved ordinary config files directly to named running sandboxes. It does not put files in the Secret store. Moves no code; for a repo, use instabox-move-repo.
|
|
4
4
|
metadata:
|
|
5
5
|
installed-by: instabox
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Basic setup with org Secrets and sandbox config copies
|
|
9
9
|
|
|
10
|
-
Use the user's chosen org and sandbox IDs. Secrets are shared within an org
|
|
10
|
+
Use the user's chosen org and sandbox IDs. Secrets are shared within an org. A sandbox that selects a Secret gets a placeholder in its environment; the outbound proxy puts the token into HTTPS request headers sent to the Secret's allowed hosts, and nowhere else. Other org sandboxes whose selection is `all`, including future sandboxes, can also use a newly saved Secret. Updating an existing Secret affects every sandbox already selecting it.
|
|
11
|
+
|
|
12
|
+
Only tokens in the built-in registry (`TOKEN_HOSTS` in `scripts/lib.mjs`) are saved, each for its service's API hosts: for example `ANTHROPIC_API_KEY` for `api.anthropic.com`, `OPENAI_API_KEY` for `api.openai.com`, `GH_TOKEN` and `GITHUB_TOKEN` for `api.github.com`. Every other credential-looking export becomes a manual step. Secrets work only for clients that send the token in an HTTPS header; database URLs, signing keys and git's own credential exchange need their own setup on the box.
|
|
11
13
|
|
|
12
14
|
Git and editor configuration is copied separately, once, to the named running sandboxes. It is not stored as Secrets, does not consume Secret quota, and is not reapplied on start or resume. This skill moves no repository or agent history. Use `instabox-move-repo` for code and conversation history.
|
|
13
15
|
|
|
14
16
|
| Group | Destination | Sources |
|
|
15
17
|
| --- | --- | --- |
|
|
16
|
-
| Tokens | Encrypted org
|
|
18
|
+
| Tokens | Encrypted org Secrets, each limited to its registry hosts | GitHub token through `gh auth token`; registry tokens in the environment or shell startup files; other credential-like exports listed as manual steps |
|
|
17
19
|
| Git | Files under the selected sandbox's home | `.gitconfig`, `.config/git/config`, global gitignore |
|
|
18
20
|
| Editor and terminal | Files under the selected sandbox's home | `.vimrc`, nvim config without plugin checkouts, `.tmux.conf` |
|
|
19
21
|
|
|
@@ -23,11 +25,11 @@ The scripts beside this file require Node 18+ locally and Python 3 in sandboxes
|
|
|
23
25
|
| --- | --- |
|
|
24
26
|
| `scan.mjs` | Read-only inventory of names, source locations, sizes and manual setup requirements; no credential values in output |
|
|
25
27
|
| `plan.mjs` | Write and strictly parse the editable plan, binding it to an org and Secret metadata |
|
|
26
|
-
| `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets through CLI stdin, merge
|
|
28
|
+
| `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets with their registry hosts through CLI stdin, merge sandbox selections, and copy config |
|
|
27
29
|
| `content.mjs` | Rewrite HOME paths, remove recognized credential lines, and adapt git/editor configuration for Linux |
|
|
28
30
|
| `install-files.py` | Install ordinary config files privately, refusing symlinks and unsafe paths; no Secret-store or legacy Vault access |
|
|
29
31
|
|
|
30
|
-
Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through
|
|
32
|
+
Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through the hidden prompt of `instabox secrets set NAME --org ORG --host HOST`.
|
|
31
33
|
|
|
32
34
|
## 1. Scan and choose the org
|
|
33
35
|
|
|
@@ -53,20 +55,11 @@ node "$S/plan.mjs" write --scan "$W/scan.json" --secrets "$W/secrets.json" \
|
|
|
53
55
|
--org "$ORG_ID" --sandbox <sandbox-id> > "$W/plan.md"
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
Repeat `--sandbox` for additional targets, or omit it to save Secrets without
|
|
57
|
-
|
|
58
|
-
A token policy looks like either of these:
|
|
59
|
-
|
|
60
|
-
```text
|
|
61
|
-
Delivery `env:SERVICE_API_KEY`: `proxy` hosts `api.example.com`
|
|
62
|
-
Delivery `env:DATABASE_URL`: `env` hosts `-`
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The `env:` prefix is the scanner's variable ID, not its delivery mode. New token rows start with explicit `env` mode for client compatibility; this gives the named sandboxes the real value. Choose `proxy` and exact approved hosts when the client sends the value literally in an HTTPS header on port 443. Do not infer hosts from a token's value or silently switch modes when a client fails. Native database protocols, local signing and git's encoded credential exchange need the real value or a separate protocol-aware broker. Proxy placeholders cannot be used as real signing or encoding inputs.
|
|
58
|
+
Repeat `--sandbox` for additional targets, or omit it to save Secrets without changing any selection or copying files. The plan lists `Org`, `Sandboxes` and checkboxes; each token row names the hosts its Secret is limited to. The org is bound to the metadata used to generate the plan; regenerate for another org. Sandbox IDs and checkboxes can be edited in the plan; hosts come from the registry and cannot.
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
The `env:` prefix in a row ID is the scanner's variable ID. Registry token rows are checked by default; keychain-backed sources are unchecked. Existing Secrets changed outside the last successful run are unchecked, and checking them authorizes replacement in the org. Files are unchecked when no sandbox is selected. Checking a config file authorizes replacement of that path on each named sandbox.
|
|
68
61
|
|
|
69
|
-
Show the concrete names,
|
|
62
|
+
Show the concrete names, allowed hosts, org and target sandboxes. Obtain any choices or authorization the original request did not already provide before reading values or writing. Do not ask again for an action already explicitly authorized.
|
|
70
63
|
|
|
71
64
|
Parse the approved plan:
|
|
72
65
|
|
|
@@ -75,7 +68,7 @@ node "$S/plan.mjs" parse --scan "$W/scan.json" --secrets "$W/secrets.json" \
|
|
|
75
68
|
--org "$ORG_ID" --limits "$W/limits.json" "$W/plan.md" > "$W/selection.json"
|
|
76
69
|
```
|
|
77
70
|
|
|
78
|
-
Exit 1 identifies malformed or missing rows,
|
|
71
|
+
Exit 1 identifies malformed or missing rows, a changed org/catalog, a scan with a token outside the registry, unsafe config, or a file selection without targets. Fix choices without changing item IDs or deleting rows. Exit 3 identifies quota or copy-size limits. Config files have their own copy limits and do not count as Secret entries. Values are measured before push; plan generation never reads token values. Legacy selector-based plans and manifests grant no authority in the new model and must be regenerated.
|
|
79
72
|
|
|
80
73
|
## 3. Dry-run, then apply the approved selection
|
|
81
74
|
|
|
@@ -88,11 +81,11 @@ The dry-run reads the approved credential sources and prepares filtered config,
|
|
|
88
81
|
|
|
89
82
|
The pusher verifies org ownership, target orgs, Secret IDs and versions, input limits and total Secret quota before mutations. If a selected Secret changed since planning, regenerate the plan. These are preflight checks, not a transaction across all CLI operations: concurrent org edits can still occur during application. A failed run reports partial outcomes; successfully saved Secrets remain in the org. Do not assume rollback or automatic removal of previously copied files.
|
|
90
83
|
|
|
91
|
-
Only selected token names are added to each target's
|
|
84
|
+
Only selected token names are added to each target's selection; existing names are preserved, and a target selecting `all` stays `all`. Running targets receive an immediate refresh; stopped targets receive the selection at their next start, but config copying requires them to be running. A failed or pending refresh does not install config that may depend on those credentials. Existing processes need to reload their environment or restart to see new placeholders.
|
|
92
85
|
|
|
93
|
-
Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free.
|
|
86
|
+
Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free. No git credential helper is added: git over HTTPS sends credentials in a form the proxy cannot fill in, so the box needs its own git sign-in (for example `gh auth login` there). An existing org Secret by itself is not a sandbox selection; add it through an explicitly reviewed `instabox secrets select BOX --secrets ...` that keeps the other selected names.
|
|
94
87
|
|
|
95
|
-
The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs
|
|
88
|
+
The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs, versions and hosts, and copied sandbox paths, never values. It does not grant reuse across orgs or across deletion and recreation of a Secret. It records completed work for a repeat plan; it does not sync changes automatically.
|
|
96
89
|
|
|
97
90
|
## 4. Verify and report
|
|
98
91
|
|
|
@@ -101,8 +94,8 @@ instabox secrets show <box>
|
|
|
101
94
|
instabox exec <box> -- 'test "${GH_TOKEN+x}" = x && test -f "$HOME/.gitconfig"'
|
|
102
95
|
```
|
|
103
96
|
|
|
104
|
-
Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove
|
|
97
|
+
Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove the upstream accepts the token. Confirm client behavior without logging credentials, and explain application restarts.
|
|
105
98
|
|
|
106
|
-
Report the org Secret names and
|
|
99
|
+
Report the org Secret names and hosts saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving Secrets without targets changes no selection; replacing a value still affects sandboxes already selecting it. Say that git over HTTPS needs its own sign-in on the box, and that credentials outside the registry (database URLs, cloud keys, other tokens) were not moved and need their own setup there; an HTTP API key can be added with `instabox secrets set NAME --org ORG --host HOST`. Future boxes created without `--secrets` keep their existing Vault behavior. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope.
|
|
107
100
|
|
|
108
101
|
Config edits in a sandbox are not overwritten at every resume. Shell aliases, SSH keys, cloud credential files, agent sign-in databases and repositories are outside this inventory. Complete the selected manual sign-in steps only within existing authorization. Remove `$W` when no longer needed; it contains metadata and choices, not credential values.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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
|
-
// changes so git works on a box that has
|
|
4
|
+
// changes so git works on a box that has no signing or SSH key.
|
|
5
5
|
//
|
|
6
6
|
// Used by plan.mjs (for sizes) and push.mjs (for the upload). Nothing here
|
|
7
7
|
// prints a file's content.
|
|
@@ -40,25 +40,11 @@ export function scrubText(text, { path, home }) {
|
|
|
40
40
|
return { text: out.join('\n'), secrets, macos };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
export const GH_HELPER = [
|
|
45
|
-
'',
|
|
46
|
-
`# ${TAG}: git over HTTPS to GitHub uses an explicitly selected env GH_TOKEN through gh`,
|
|
47
|
-
'[credential "https://github.com"]',
|
|
48
|
-
'\thelper =',
|
|
49
|
-
'\thelper = !gh auth git-credential',
|
|
50
|
-
'[credential "https://gist.github.com"]',
|
|
51
|
-
'\thelper =',
|
|
52
|
-
'\thelper = !gh auth git-credential',
|
|
53
|
-
'',
|
|
54
|
-
].join('\n');
|
|
55
|
-
|
|
56
|
-
// ~/.gitconfig for the box. ghToken: GH_TOKEN is explicitly selected for env delivery, so the gh helper is added.
|
|
57
|
-
// Changes, besides scrubText's:
|
|
43
|
+
// ~/.gitconfig for the box. Changes, besides scrubText's:
|
|
58
44
|
// - commit/tag signing is turned off: the signing key (GPG, SSH, 1Password) is not on the box;
|
|
59
45
|
// - url.<ssh>.insteadOf rewrites are commented out: the box has no SSH key, and they would
|
|
60
46
|
// send HTTPS remotes over SSH.
|
|
61
|
-
export function gitconfigText(text, { home
|
|
47
|
+
export function gitconfigText(text, { home }) {
|
|
62
48
|
const r = scrubText(text, { path: '.gitconfig', home });
|
|
63
49
|
let section = '';
|
|
64
50
|
let signing = 0;
|
|
@@ -77,13 +63,11 @@ export function gitconfigText(text, { home, ghToken }) {
|
|
|
77
63
|
}
|
|
78
64
|
return line;
|
|
79
65
|
});
|
|
80
|
-
|
|
81
|
-
if (ghToken) out = out.replace(/\n*$/, '\n') + GH_HELPER;
|
|
82
|
-
return { text: out, secrets: r.secrets, macos: r.macos, signing, insteadOf };
|
|
66
|
+
return { text: lines.join('\n'), secrets: r.secrets, macos: r.macos, signing, insteadOf };
|
|
83
67
|
}
|
|
84
68
|
|
|
85
69
|
// The content of one file of a file item, or {error}. f: {path, src, transform}.
|
|
86
|
-
export function fileContent(f, { home
|
|
70
|
+
export function fileContent(f, { home } = {}) {
|
|
87
71
|
let buf;
|
|
88
72
|
try {
|
|
89
73
|
buf = readFileSync(f.src);
|
|
@@ -93,7 +77,7 @@ export function fileContent(f, { home, ghToken = false } = {}) {
|
|
|
93
77
|
if (buf.includes(0)) return { error: `${f.src}: a binary file (config copying supports UTF-8 text only)` };
|
|
94
78
|
const text = buf.toString('utf8');
|
|
95
79
|
if (!Buffer.from(text).equals(buf)) return { error: `${f.src}: invalid UTF-8 text` };
|
|
96
|
-
if (f.transform === 'gitconfig') return gitconfigText(text, { home
|
|
80
|
+
if (f.transform === 'gitconfig') return gitconfigText(text, { home });
|
|
97
81
|
if (f.transform === 'raw') {
|
|
98
82
|
if (SECRET_VALUE.test(text) || text.split('\n').some((line) => SECRET_KEY_LINE.test(line)))
|
|
99
83
|
return { error: `${f.src}: possible credential in structured config; review it separately` };
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// The one rule every function here keeps: a credential's value is never
|
|
4
4
|
// returned to the agent, stored in a file or printed. Code that looks at a
|
|
5
5
|
// secret returns names, places and sizes only.
|
|
6
|
-
import { isIP } from 'node:net';
|
|
7
6
|
import { spawnSync } from 'node:child_process';
|
|
8
7
|
import { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
9
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -38,50 +37,55 @@ export const SECRET_VALUE = new RegExp(
|
|
|
38
37
|
export const SECRET_KEY_LINE =
|
|
39
38
|
/(?:^|[\s{,;:])["']?[\w.-]*(?:token|password|passwd|secret|api[_-]?key|apikey|_auth|authorization|extraheader|private[_-]?key)[\w.-]*["']?\s*[=:]\s*["']?(?![0-9.]+\b|true\b|false\b)[^\s"',}]/i;
|
|
40
39
|
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
]
|
|
40
|
+
// The tokens this skill saves, each with the HTTPS API hosts its usual client sends it to in a
|
|
41
|
+
// request header. The box gets a placeholder and the outbound proxy fills in the token only for
|
|
42
|
+
// these hosts. A token whose client signs with it, sends it in a body or URL, or talks to hosts
|
|
43
|
+
// that cannot be named exactly is left out: the scan lists it as a manual step.
|
|
44
|
+
// FORMAT is the scan and approved-selection format. 4: proxy-only Secrets, hosts from
|
|
45
|
+
// TOKEN_HOSTS. Older files approved env delivery or other hosts; they are refused, never
|
|
46
|
+
// reinterpreted, so an approval cannot widen a credential's reach.
|
|
47
|
+
export const FORMAT = 4;
|
|
48
|
+
|
|
49
|
+
export const TOKEN_HOSTS = new Map(
|
|
50
|
+
Object.entries({
|
|
51
|
+
GH_TOKEN: ['api.github.com'],
|
|
52
|
+
GITHUB_TOKEN: ['api.github.com'],
|
|
53
|
+
ANTHROPIC_API_KEY: ['api.anthropic.com'],
|
|
54
|
+
OPENAI_API_KEY: ['api.openai.com'],
|
|
55
|
+
GEMINI_API_KEY: ['generativelanguage.googleapis.com'],
|
|
56
|
+
GOOGLE_API_KEY: ['generativelanguage.googleapis.com'],
|
|
57
|
+
GOOGLE_GENERATIVE_AI_API_KEY: ['generativelanguage.googleapis.com'],
|
|
58
|
+
OPENROUTER_API_KEY: ['openrouter.ai'],
|
|
59
|
+
GROQ_API_KEY: ['api.groq.com'],
|
|
60
|
+
MISTRAL_API_KEY: ['api.mistral.ai'],
|
|
61
|
+
DEEPSEEK_API_KEY: ['api.deepseek.com'],
|
|
62
|
+
XAI_API_KEY: ['api.x.ai'],
|
|
63
|
+
TOGETHER_API_KEY: ['api.together.ai', 'api.together.xyz'],
|
|
64
|
+
FIREWORKS_API_KEY: ['api.fireworks.ai'],
|
|
65
|
+
PERPLEXITY_API_KEY: ['api.perplexity.ai'],
|
|
66
|
+
COHERE_API_KEY: ['api.cohere.ai', 'api.cohere.com'],
|
|
67
|
+
VOYAGE_API_KEY: ['api.voyageai.com'],
|
|
68
|
+
REPLICATE_API_TOKEN: ['api.replicate.com'],
|
|
69
|
+
ELEVENLABS_API_KEY: ['api.elevenlabs.io'],
|
|
70
|
+
TAVILY_API_KEY: ['api.tavily.com'],
|
|
71
|
+
EXA_API_KEY: ['api.exa.ai'],
|
|
72
|
+
FIRECRAWL_API_KEY: ['api.firecrawl.dev'],
|
|
73
|
+
BRAVE_API_KEY: ['api.search.brave.com'],
|
|
74
|
+
HF_TOKEN: ['huggingface.co', 'router.huggingface.co'],
|
|
75
|
+
HUGGING_FACE_HUB_TOKEN: ['huggingface.co', 'router.huggingface.co'],
|
|
76
|
+
NPM_TOKEN: ['registry.npmjs.org'],
|
|
77
|
+
VERCEL_TOKEN: ['api.vercel.com'],
|
|
78
|
+
NETLIFY_AUTH_TOKEN: ['api.netlify.com'],
|
|
79
|
+
CLOUDFLARE_API_TOKEN: ['api.cloudflare.com'],
|
|
80
|
+
SUPABASE_ACCESS_TOKEN: ['api.supabase.com'],
|
|
81
|
+
RENDER_API_KEY: ['api.render.com'],
|
|
82
|
+
NEON_API_KEY: ['console.neon.tech'],
|
|
83
|
+
SENTRY_AUTH_TOKEN: ['sentry.io'],
|
|
84
|
+
LINEAR_API_KEY: ['api.linear.app'],
|
|
85
|
+
GITLAB_TOKEN: ['gitlab.com'],
|
|
86
|
+
DIGITALOCEAN_ACCESS_TOKEN: ['api.digitalocean.com'],
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
85
89
|
|
|
86
90
|
// Reserved Secret variable names (internal/secrets/validate.go).
|
|
87
91
|
const RESERVED = new Set([
|
|
@@ -206,28 +210,6 @@ export function configPathOK(p) {
|
|
|
206
210
|
return p.split('/').every((c) => c !== '' && c !== '.' && !c.startsWith('..'));
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
export function deliveryPolicy(delivery, hosts = []) {
|
|
210
|
-
if (!['env', 'proxy'].includes(delivery) || !Array.isArray(hosts)) throw new Error('delivery must be env or proxy');
|
|
211
|
-
if (delivery === 'env' && hosts.length) throw new Error('env delivery cannot enforce hosts; use -');
|
|
212
|
-
if (delivery === 'proxy' && (!hosts.length || hosts.length > 32))
|
|
213
|
-
throw new Error('proxy delivery requires 1-32 exact hosts');
|
|
214
|
-
const normalized = hosts.map((name) => {
|
|
215
|
-
if (typeof name !== 'string') throw new Error('hosts must be exact public DNS names');
|
|
216
|
-
const h = name.toLowerCase().replace(/\.$/, '');
|
|
217
|
-
if (
|
|
218
|
-
h.length > 253 ||
|
|
219
|
-
!h.includes('.') ||
|
|
220
|
-
isIP(h) ||
|
|
221
|
-
/(?:^|\.)(?:local|localhost)$/.test(h) ||
|
|
222
|
-
h.split('.').some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
|
|
223
|
-
) {
|
|
224
|
-
throw new Error('hosts must be exact public DNS names without a scheme, port, path, IP address or wildcard');
|
|
225
|
-
}
|
|
226
|
-
return h;
|
|
227
|
-
});
|
|
228
|
-
return { delivery, hosts: [...new Set(normalized)].sort() };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
213
|
export function validID(s) {
|
|
232
214
|
return typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(s);
|
|
233
215
|
}
|
|
@@ -237,7 +219,6 @@ export function secretIdentity(s) {
|
|
|
237
219
|
name: s.name,
|
|
238
220
|
orgId: s.orgId,
|
|
239
221
|
version: s.version,
|
|
240
|
-
delivery: s.delivery,
|
|
241
222
|
hosts: [...(s.hosts || [])].sort(),
|
|
242
223
|
};
|
|
243
224
|
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
FILE_MAX_VALUE,
|
|
12
12
|
FILE_MAX_TOTAL,
|
|
13
13
|
configPathOK,
|
|
14
|
-
|
|
14
|
+
FORMAT,
|
|
15
|
+
TOKEN_HOSTS,
|
|
15
16
|
human,
|
|
16
17
|
isMain,
|
|
17
18
|
sameSecret,
|
|
@@ -60,30 +61,29 @@ export class PlanError extends Error {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function checkContext(scan, secrets, orgId) {
|
|
63
|
-
if (
|
|
64
|
+
if (
|
|
65
|
+
scan.version !== FORMAT ||
|
|
66
|
+
!Array.isArray(scan.items) ||
|
|
67
|
+
scan.items.some((it) => it.kind === 'env' && !TOKEN_HOSTS.has(it.name))
|
|
68
|
+
)
|
|
64
69
|
throw new PlanError(['scan format changed; run scan.mjs again']);
|
|
65
70
|
if (!validID(orgId)) throw new PlanError(['an explicit org ID is required']);
|
|
66
71
|
if (!Array.isArray(secrets) || secrets.some((s) => s.orgId !== orgId || !s.id || !Number.isSafeInteger(s.version)))
|
|
67
72
|
throw new PlanError(['Secret metadata must belong to the selected org and include IDs and versions']);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
export function writePlan(
|
|
71
|
-
scan,
|
|
72
|
-
{ secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [], delivery = 'env', hosts = [] } = {},
|
|
73
|
-
) {
|
|
75
|
+
export function writePlan(scan, { secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [] } = {}) {
|
|
74
76
|
checkContext(scan, secrets, orgId);
|
|
75
77
|
if (sandboxes.some((id) => !validID(id) || !id.startsWith('m_')))
|
|
76
78
|
throw new PlanError(['sandboxes must use stable m_ IDs']);
|
|
77
|
-
const policy = deliveryPolicy(delivery, hosts);
|
|
78
79
|
const out = [
|
|
79
80
|
'# instabox basic setup: plan',
|
|
80
81
|
'',
|
|
81
82
|
'Check `[x]` the items to save or copy. Do not change IDs or delete lines.',
|
|
82
|
-
'Tokens become encrypted org Secrets.
|
|
83
|
-
'This run
|
|
84
|
-
'
|
|
83
|
+
'Tokens become encrypted org Secrets. A box sees a placeholder; the outbound proxy puts the token into HTTPS headers sent to the hosts listed for each token, and nowhere else.',
|
|
84
|
+
'This run selects them only on the sandbox IDs below; an empty list changes no selection. Existing selections remain in force.',
|
|
85
|
+
'Saved Secrets also become available to other org sandboxes whose selection is all, including future sandboxes.',
|
|
85
86
|
'Replacing an existing Secret affects its other authorized sandboxes. Config files are copied once to the named running sandboxes and may overwrite those paths; they are not Secrets.',
|
|
86
|
-
'Env values can be copied into files, processes or snapshots; removing a selection cannot recover those copies.',
|
|
87
87
|
'',
|
|
88
88
|
`Org: \`${orgId}\``,
|
|
89
89
|
`Sandboxes: \`${[...new Set(sandboxes)].join(',') || '-'}\``,
|
|
@@ -104,7 +104,7 @@ export function writePlan(
|
|
|
104
104
|
const checked = it.checked && !other && (it.kind === 'env' || sandboxes.length > 0);
|
|
105
105
|
let description =
|
|
106
106
|
it.kind === 'env'
|
|
107
|
-
? `${it.name} from ${it.from}`
|
|
107
|
+
? `${it.name} from ${it.from}, for ${TOKEN_HOSTS.get(it.name).join(', ')}`
|
|
108
108
|
: `~/${it.path}: ${it.files.length} file(s), ${human(it.bytes)}`;
|
|
109
109
|
if (it.reason) description += `; off by default: ${it.reason}`;
|
|
110
110
|
if (it.note) description += `; ${it.note}`;
|
|
@@ -112,12 +112,6 @@ export function writePlan(
|
|
|
112
112
|
else if (ex.length) description += '; replaces the unchanged Secret from your previous run';
|
|
113
113
|
if (it.kind === 'file' && !sandboxes.length) description += '; select running sandbox IDs before checking this';
|
|
114
114
|
out.push(`- [${checked ? 'x' : ' '}] \`${it.id}\` ${description}`);
|
|
115
|
-
if (it.kind === 'env') {
|
|
116
|
-
// Preserve an existing mode as a starting point; never silently change
|
|
117
|
-
// a proxy credential into a real-value env credential on a repeat run.
|
|
118
|
-
const p = ex.length ? deliveryPolicy(ex[0].item.delivery, ex[0].item.hosts || []) : policy;
|
|
119
|
-
out.push(`Delivery \`${it.id}\`: \`${p.delivery}\` hosts \`${p.hosts.join(',') || '-'}\``);
|
|
120
|
-
}
|
|
121
115
|
}
|
|
122
116
|
}
|
|
123
117
|
out.push('', '## Manual setup', '', 'These entries have no checkbox; no credentials are read for them.', '');
|
|
@@ -129,8 +123,7 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
|
|
|
129
123
|
checkContext(scan, secrets, orgId);
|
|
130
124
|
const errors = [];
|
|
131
125
|
const byId = new Map(scan.items.map((it) => [it.id, it]));
|
|
132
|
-
const states = new Map()
|
|
133
|
-
policies = new Map();
|
|
126
|
+
const states = new Map();
|
|
134
127
|
let planOrg, sandboxes, digest;
|
|
135
128
|
for (const [index, raw] of text.split('\n').entries()) {
|
|
136
129
|
const line = raw.replace(/\r$/, ''),
|
|
@@ -155,20 +148,6 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
|
|
|
155
148
|
}
|
|
156
149
|
continue;
|
|
157
150
|
}
|
|
158
|
-
if (/^Delivery\b/.test(line)) {
|
|
159
|
-
const m = line.match(/^Delivery `([^`]+)`: `([^`]+)` hosts `([^`]*)`$/);
|
|
160
|
-
if (!m || byId.get(m[1])?.kind !== 'env') {
|
|
161
|
-
errors.push(`line ${n}: invalid Delivery line`);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (policies.has(m[1])) errors.push(`line ${n}: duplicate delivery for ${m[1]}`);
|
|
165
|
-
try {
|
|
166
|
-
policies.set(m[1], deliveryPolicy(m[2], m[3] === '-' ? [] : m[3].split(',').map((h) => h.trim())));
|
|
167
|
-
} catch (e) {
|
|
168
|
-
errors.push(`line ${n}: ${e.message}`);
|
|
169
|
-
}
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
151
|
if (/^Selector:/.test(line)) errors.push(`line ${n}: legacy selectors are retired`);
|
|
173
152
|
const d = line.match(/^<!-- plan ([0-9a-f]+):/);
|
|
174
153
|
if (d) {
|
|
@@ -195,27 +174,21 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
|
|
|
195
174
|
errors.push('different scan or org Secret state; scan, list Secrets and write a new plan');
|
|
196
175
|
for (const it of scan.items) {
|
|
197
176
|
if (!states.has(it.id)) errors.push(`\`${it.id}\` is missing from the plan`);
|
|
198
|
-
if (it.kind === 'env' && !policies.has(it.id))
|
|
199
|
-
errors.push(`\`${it.id}\` needs an explicit delivery mode and hosts line`);
|
|
200
177
|
}
|
|
201
178
|
if (errors.length) throw new PlanError(errors);
|
|
202
179
|
|
|
203
180
|
const picked = scan.items.filter((it) => states.get(it.id));
|
|
204
|
-
const ghToken = picked.some(
|
|
205
|
-
(it) => it.kind === 'env' && it.name === 'GH_TOKEN' && policies.get(it.id).delivery === 'env',
|
|
206
|
-
);
|
|
207
181
|
const items = picked.map((it) => {
|
|
208
182
|
const item = { ...it };
|
|
209
183
|
delete item.checked;
|
|
210
184
|
if (it.kind === 'env') {
|
|
211
|
-
Object.assign(item, policies.get(it.id));
|
|
212
185
|
item.expected = secrets.find((s) => s.name === it.name);
|
|
213
186
|
item.expected = item.expected ? secretIdentity(item.expected) : null;
|
|
214
187
|
} else {
|
|
215
188
|
if (!sandboxes.length) errors.push(`\`${it.id}\`: config copying requires explicit running sandbox IDs`);
|
|
216
189
|
item.files = it.files.map((f) => {
|
|
217
190
|
if (!configPathOK(f.path)) errors.push(`\`${it.id}\`: invalid config path`);
|
|
218
|
-
const c = fileContent(f, { home: scan.home
|
|
191
|
+
const c = fileContent(f, { home: scan.home });
|
|
219
192
|
if (c.error) errors.push(`\`${it.id}\`: ${c.error}`);
|
|
220
193
|
const bytes = c.error ? 0 : Buffer.byteLength(c.text);
|
|
221
194
|
if (bytes > FILE_MAX_VALUE) errors.push(`\`${it.id}\`: file exceeds the config-copy limit of 64 KiB`);
|
|
@@ -249,11 +222,10 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
|
|
|
249
222
|
if (fileBytes > FILE_MAX_TOTAL)
|
|
250
223
|
throw new PlanError(['config copies exceed 512 KiB; uncheck files or transfer them separately'], 3);
|
|
251
224
|
return {
|
|
252
|
-
version:
|
|
225
|
+
version: FORMAT,
|
|
253
226
|
home: scan.home,
|
|
254
227
|
orgId,
|
|
255
228
|
sandboxes,
|
|
256
|
-
ghToken,
|
|
257
229
|
items,
|
|
258
230
|
manual: scan.manual,
|
|
259
231
|
skipped: scan.items.filter((it) => !states.get(it.id)).map((it) => it.id),
|
|
@@ -285,14 +257,11 @@ function main() {
|
|
|
285
257
|
const secrets = JSON.parse(readFileSync(secretsFile, 'utf8')).items;
|
|
286
258
|
if (!Array.isArray(secrets)) throw new Error('metadata must be instabox secrets ls --json output');
|
|
287
259
|
if (command === 'write') {
|
|
288
|
-
const sandboxes = []
|
|
289
|
-
hosts = [];
|
|
260
|
+
const sandboxes = [];
|
|
290
261
|
let value;
|
|
291
262
|
while ((value = opt('--sandbox')) !== undefined) sandboxes.push(value);
|
|
292
|
-
while ((value = opt('--host')) !== undefined) hosts.push(value);
|
|
293
|
-
const delivery = opt('--delivery') || 'env';
|
|
294
263
|
if (args.length) throw new Error('unknown write arguments');
|
|
295
|
-
process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes
|
|
264
|
+
process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes }));
|
|
296
265
|
} else if (command === 'parse') {
|
|
297
266
|
const limitFile = opt('--limits');
|
|
298
267
|
const limits = limitFile ? JSON.parse(readFileSync(limitFile, 'utf8')) : {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Save reviewed org Secrets, select them for named sandboxes, and copy
|
|
3
|
-
// config files separately. Credential values are held only in memory/stdin.
|
|
2
|
+
// Save reviewed org Secrets for their registry hosts, select them for named sandboxes, and copy
|
|
3
|
+
// ordinary config files separately. Credential values are held only in memory/stdin.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
FILE_MAX_VALUE,
|
|
14
14
|
FILE_MAX_TOTAL,
|
|
15
15
|
configPathOK,
|
|
16
|
-
|
|
16
|
+
FORMAT,
|
|
17
|
+
TOKEN_HOSTS,
|
|
17
18
|
human,
|
|
18
19
|
isMain,
|
|
19
20
|
run,
|
|
@@ -107,8 +108,8 @@ function validSource(it, home) {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function checkSelection(sel) {
|
|
110
|
-
if (sel.version !==
|
|
111
|
-
throw new Error('
|
|
111
|
+
if (sel.version !== FORMAT || Object.hasOwn(sel, 'selector'))
|
|
112
|
+
throw new Error('this selection was approved in an older format; scan and approve a new org Secret plan');
|
|
112
113
|
if (
|
|
113
114
|
!validID(sel.orgId) ||
|
|
114
115
|
!Array.isArray(sel.sandboxes) ||
|
|
@@ -124,12 +125,15 @@ function checkSelection(sel) {
|
|
|
124
125
|
if (ids.has(it.id)) throw new Error('duplicate selection ID');
|
|
125
126
|
ids.add(it.id);
|
|
126
127
|
if (it.kind === 'env') {
|
|
127
|
-
if (
|
|
128
|
+
if (
|
|
129
|
+
!TOKEN_HOSTS.has(it.name) ||
|
|
130
|
+
!secretNameOK(it.name) ||
|
|
131
|
+
names.has(it.name) ||
|
|
132
|
+
!validSource(it, sel.home) ||
|
|
133
|
+
!Object.hasOwn(it, 'expected')
|
|
134
|
+
)
|
|
128
135
|
throw new Error('invalid Secret selection or source');
|
|
129
136
|
names.add(it.name);
|
|
130
|
-
const policy = deliveryPolicy(it.delivery, it.hosts);
|
|
131
|
-
if (JSON.stringify(policy.hosts) !== JSON.stringify(it.hosts))
|
|
132
|
-
throw new Error('hosts must use the normalized policy from the approved plan');
|
|
133
137
|
} else if (it.kind === 'file') {
|
|
134
138
|
if (!sel.sandboxes.length || !Array.isArray(it.files))
|
|
135
139
|
throw new Error('config copying needs explicit sandbox IDs');
|
|
@@ -191,7 +195,7 @@ export function push(
|
|
|
191
195
|
if (sel.items.some((it) => it.kind === 'file') && box.state !== 'running')
|
|
192
196
|
throw new Error(`${id}: start the sandbox before copying config files`);
|
|
193
197
|
const selection = json(['secrets', 'show', id]);
|
|
194
|
-
if (
|
|
198
|
+
if (selection.secrets !== 'all' && !Array.isArray(selection.secrets))
|
|
195
199
|
throw new Error('sandbox returned invalid Secret selections');
|
|
196
200
|
return box;
|
|
197
201
|
});
|
|
@@ -211,7 +215,6 @@ export function push(
|
|
|
211
215
|
|
|
212
216
|
const tokens = [],
|
|
213
217
|
files = [];
|
|
214
|
-
const ghToken = sel.items.some((it) => it.kind === 'env' && it.name === 'GH_TOKEN' && it.delivery === 'env');
|
|
215
218
|
const rcNames = new Map(),
|
|
216
219
|
rcValues = new Map();
|
|
217
220
|
for (const it of sel.items)
|
|
@@ -229,13 +232,13 @@ export function push(
|
|
|
229
232
|
typeof value !== 'string' ||
|
|
230
233
|
value.includes('\0') ||
|
|
231
234
|
Buffer.byteLength(value) > SECRET_MAX_VALUE ||
|
|
232
|
-
|
|
235
|
+
/[\x00-\x08\x0a-\x1f\x7f]/.test(value)
|
|
233
236
|
)
|
|
234
|
-
return fail(`${it.name}: could not prepare a valid
|
|
235
|
-
tokens.push({ item: it, value });
|
|
237
|
+
return fail(`${it.name}: could not prepare a valid header value; nothing written`);
|
|
238
|
+
tokens.push({ item: it, hosts: TOKEN_HOSTS.get(it.name), value });
|
|
236
239
|
} else {
|
|
237
240
|
for (const f of it.files) {
|
|
238
|
-
const got = fileContent(f, { home: sel.home
|
|
241
|
+
const got = fileContent(f, { home: sel.home });
|
|
239
242
|
if (got.error || Buffer.byteLength(got.text) > FILE_MAX_VALUE)
|
|
240
243
|
return fail(`~/${f.path}: config could not be prepared; nothing written`);
|
|
241
244
|
files.push({ path: f.path, text: got.text });
|
|
@@ -267,11 +270,11 @@ export function push(
|
|
|
267
270
|
|
|
268
271
|
const manifest = readManifest(manifestPath);
|
|
269
272
|
let ok = true;
|
|
270
|
-
for (const { item, value } of tokens) {
|
|
273
|
+
for (const { item, hosts, value } of tokens) {
|
|
271
274
|
const r = {
|
|
272
275
|
name: item.name,
|
|
273
276
|
kind: 'secret',
|
|
274
|
-
|
|
277
|
+
hosts,
|
|
275
278
|
bytes: Buffer.byteLength(value),
|
|
276
279
|
status: dryRun ? 'would save' : 'failed',
|
|
277
280
|
};
|
|
@@ -284,9 +287,7 @@ export function push(
|
|
|
284
287
|
item.name,
|
|
285
288
|
'--org',
|
|
286
289
|
sel.orgId,
|
|
287
|
-
'--
|
|
288
|
-
item.delivery,
|
|
289
|
-
...item.hosts.flatMap((h) => ['--host', h]),
|
|
290
|
+
...hosts.flatMap((h) => ['--host', h]),
|
|
290
291
|
'--json',
|
|
291
292
|
];
|
|
292
293
|
const response = run(args, { env, input: value + '\n' });
|
|
@@ -298,8 +299,7 @@ export function push(
|
|
|
298
299
|
saved.orgId !== sel.orgId ||
|
|
299
300
|
!saved.id ||
|
|
300
301
|
!Number.isSafeInteger(saved.version) ||
|
|
301
|
-
saved.
|
|
302
|
-
JSON.stringify(deliveryPolicy(saved.delivery, saved.hosts || []).hosts) !== JSON.stringify(item.hosts)
|
|
302
|
+
JSON.stringify(unique(saved.hosts || [])) !== JSON.stringify(unique(hosts))
|
|
303
303
|
)
|
|
304
304
|
throw new Error();
|
|
305
305
|
r.status = 'saved';
|
|
@@ -310,7 +310,7 @@ export function push(
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
log(
|
|
313
|
-
`${r.status} ${item.name} (${
|
|
313
|
+
`${r.status} ${item.name} (for ${hosts.join(',')}, ${r.bytes} bytes) in ${sel.orgId}`,
|
|
314
314
|
);
|
|
315
315
|
}
|
|
316
316
|
if (!dryRun && tokens.length) saveManifest(manifestPath, manifest);
|
|
@@ -333,17 +333,14 @@ export function push(
|
|
|
333
333
|
try {
|
|
334
334
|
if (tokens.length) {
|
|
335
335
|
const current = json(['secrets', 'show', box.id]);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (proxyNames.length && current.secrets !== 'all')
|
|
341
|
-
flags.push('--secrets', JSON.stringify(unique([...current.secrets, ...proxyNames])));
|
|
342
|
-
if (flags.length) json(['secrets', 'select', box.id, ...flags]);
|
|
336
|
+
if (current.secrets !== 'all') {
|
|
337
|
+
const names = unique([...current.secrets, ...tokens.map((t) => t.item.name)]);
|
|
338
|
+
json(['secrets', 'select', box.id, '--secrets', JSON.stringify(names)]);
|
|
339
|
+
}
|
|
343
340
|
if (box.state === 'running') {
|
|
344
341
|
const refreshed = json(['secrets', 'push', box.id]);
|
|
345
342
|
if (refreshed.guest?.state !== 'applied')
|
|
346
|
-
throw new Error('
|
|
343
|
+
throw new Error('the box has not applied the Secret selection; inspect secrets show and retry');
|
|
347
344
|
}
|
|
348
345
|
}
|
|
349
346
|
if (files.length) {
|
|
@@ -377,7 +374,7 @@ export function push(
|
|
|
377
374
|
}
|
|
378
375
|
result.status = 'configured';
|
|
379
376
|
log(
|
|
380
|
-
`configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; Secret
|
|
377
|
+
`configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; the Secret selection applies at the next start'}`,
|
|
381
378
|
);
|
|
382
379
|
} catch (e) {
|
|
383
380
|
ok = false;
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
MACOS_ONLY,
|
|
14
14
|
SECRET_VALUE,
|
|
15
15
|
FILE_MAX_VALUE,
|
|
16
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 },
|
|
129
|
+
add(a.name, { type: 'rc', file: join(home, f), name: a.name }, place, extra);
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
|
-
//
|
|
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 (!
|
|
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> --
|
|
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> --
|
|
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:
|
|
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`:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
56
|
+
instabox create --name work2 --snapshot work-copy --no-vault --secrets none
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To
|
|
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
|
|