boxdown 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/assets/devcontainer/README.md +29 -6
- package/assets/devcontainer/devcontainer.json +4 -9
- package/assets/devcontainer/hooks/initialize.sh +49 -27
- package/assets/devcontainer/hooks/post-create.sh +23 -1
- package/assets/devcontainer/hooks/post-start.sh +0 -10
- package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
- package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
- package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
- package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
- package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
- package/dist/main-O__JaiXe.mjs.map +1 -0
- package/dist/main.cjs +4 -1
- package/dist/main.d.cts +98 -26
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +98 -26
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/features/commit-signing.md +71 -0
- package/docs/features/generated-config-and-state.md +19 -4
- package/docs/features/github-auth-refresh.md +3 -2
- package/docs/features/lifecycle.md +19 -0
- package/docs/features/setup.md +5 -0
- package/docs/features/start-and-shell.md +10 -0
- package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
- package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
- package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
- package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
- package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
- package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
- package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
- package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
- package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
- package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
- package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
- package/package.json +1 -1
- package/src/config.ts +14 -2
- package/src/constants.ts +7 -0
- package/src/container-runtime.ts +295 -0
- package/src/devcontainer.ts +57 -30
- package/src/doctor.ts +169 -46
- package/src/git-signing.ts +205 -25
- package/src/logging.ts +11 -1
- package/src/main.ts +97 -12
- package/src/paths.ts +21 -1
- package/src/process.ts +50 -10
- package/src/progress.ts +63 -8
- package/src/purge.ts +8 -0
- package/src/shell.ts +6 -0
- package/dist/main-XMBsKjIK.mjs.map +0 -1
|
@@ -8,10 +8,23 @@ If the agent is unavailable, has no identities, or has multiple identities
|
|
|
8
8
|
that Boxdown cannot distinguish, Boxdown warns and configures unsigned commits.
|
|
9
9
|
It never guesses a key and does not block setup or commits.
|
|
10
10
|
|
|
11
|
+
Signing readiness is resolved when generated devcontainer configuration is
|
|
12
|
+
written. A container created while readiness is unavailable does not gain an
|
|
13
|
+
SSH-agent mount later; recreate it after correcting the warning:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
boxdown setup --workspace /path/to/project --recreate
|
|
17
|
+
```
|
|
18
|
+
|
|
11
19
|
The container can request operations from identities exposed by the forwarded
|
|
12
20
|
agent. Use a dedicated signing identity or an agent that confirms sensitive
|
|
13
21
|
operations when that exposure is unacceptable.
|
|
14
22
|
|
|
23
|
+
On Docker Desktop, Boxdown places a small root-owned relay between the
|
|
24
|
+
host-facing agent socket and the non-root container user. The relay exposes no
|
|
25
|
+
private-key material and lets normal `node`-user Git commits reach the host
|
|
26
|
+
agent.
|
|
27
|
+
|
|
15
28
|
GitHub verification is separate from signing. Upload the selected public key as
|
|
16
29
|
a GitHub SSH signing key once to receive the Verified badge:
|
|
17
30
|
|
|
@@ -21,3 +34,61 @@ gh ssh-key add /path/to/signing-key.pub --type signing --title "Boxdown commit s
|
|
|
21
34
|
|
|
22
35
|
The same key may already be registered for GitHub SSH authentication; GitHub
|
|
23
36
|
requires a second registration with type `signing`.
|
|
37
|
+
|
|
38
|
+
## Optional: configure SSH signing on the host
|
|
39
|
+
|
|
40
|
+
Boxdown can sign commits without changing the host Git configuration. The
|
|
41
|
+
container uses the selected identity from the host SSH agent directly. Configure
|
|
42
|
+
the host as well when you also make commits outside Boxdown, or when you want
|
|
43
|
+
to make the selected Boxdown identity explicit.
|
|
44
|
+
|
|
45
|
+
First, verify the intended identity is loaded in the SSH agent. `ssh-add -l`
|
|
46
|
+
prints loaded key fingerprints:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ssh-add -l
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then configure Git to use that public key for SSH signing:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
git config --global gpg.format ssh
|
|
56
|
+
git config --global user.signingkey ~/.ssh/id_ed25519.pub
|
|
57
|
+
git config --global --unset-all gpg.program || true
|
|
58
|
+
git config --global commit.gpgsign true
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Replace `~/.ssh/id_ed25519.pub` with the selected public key when you use a
|
|
62
|
+
different identity. The public key must also be registered with GitHub as a
|
|
63
|
+
Signing key for pushed commits to display the Verified badge.
|
|
64
|
+
|
|
65
|
+
Boxdown accepts `user.signingkey` as an inline SSH public key, a `key::` public
|
|
66
|
+
key, an absolute path, a `~/` path, or a path relative to the workspace. Only
|
|
67
|
+
public-key files are read. An explicitly configured key is authoritative: if
|
|
68
|
+
it is unreadable, invalid, or absent from the SSH agent, Boxdown disables
|
|
69
|
+
signing instead of selecting another identity.
|
|
70
|
+
|
|
71
|
+
## User configuration precedence
|
|
72
|
+
|
|
73
|
+
Boxdown defaults to SSH signing only when Git has no explicit signing
|
|
74
|
+
preference. A repository-local `commit.gpgsign=false`, a non-SSH `gpg.format`,
|
|
75
|
+
or an explicit `gpg.program` is preserved. Explicit SSH signing remains
|
|
76
|
+
supported; Boxdown maps an inaccessible host public-key path to the selected
|
|
77
|
+
public-key snapshot inside the container. This policy is intentional and may
|
|
78
|
+
be revisited in a future release if Boxdown becomes authoritative.
|
|
79
|
+
|
|
80
|
+
Git can require `gpg.ssh.allowedSignersFile` for trust-aware local verification
|
|
81
|
+
such as `git log --show-signature`. It is not required to create SSH signatures
|
|
82
|
+
or for GitHub to verify a commit.
|
|
83
|
+
|
|
84
|
+
## Troubleshooting
|
|
85
|
+
|
|
86
|
+
Signing failures remain non-blocking. User-facing lifecycle commands print a
|
|
87
|
+
concise reason, while the workspace `boxdown.log` records a stable reason code
|
|
88
|
+
and sanitized diagnostic detail. Container validation further distinguishes a
|
|
89
|
+
missing mounted public key, an unavailable forwarded agent, a selected key that
|
|
90
|
+
is not loaded, and a failed disposable signed-commit probe.
|
|
91
|
+
|
|
92
|
+
Run `boxdown doctor --workspace /path/to/project` to recheck host identity
|
|
93
|
+
selection and GitHub signing-key registration. After fixing the reported
|
|
94
|
+
condition, use `--recreate`; existing unsigned commits are not rewritten.
|
|
@@ -62,6 +62,19 @@ entries with best-effort Docker state.
|
|
|
62
62
|
Metadata may also record the last inspected Docker image ID for the workspace so
|
|
63
63
|
`boxdown purge` can remove that exact image even after the container is gone.
|
|
64
64
|
|
|
65
|
+
## Runtime Secret State
|
|
66
|
+
|
|
67
|
+
`ANTHROPIC_API_KEY`, `SNYK_TOKEN`, and the optional 1Password service-account
|
|
68
|
+
token are written to owner-only files in a per-workspace runtime directory,
|
|
69
|
+
separate from persistent workspace data. Boxdown mounts that directory
|
|
70
|
+
read-only at `/run/boxdown/secrets`; only non-secret mount and Bash-bootstrap
|
|
71
|
+
paths appear in generated Docker configuration.
|
|
72
|
+
|
|
73
|
+
Bash sessions load available files as ordinary environment variables. Missing
|
|
74
|
+
host values and failed 1Password lookup are non-blocking. `boxdown down` and
|
|
75
|
+
`boxdown purge` remove the workspace runtime directory after container removal.
|
|
76
|
+
Boxdown does not use or modify project `.env.development` files.
|
|
77
|
+
|
|
65
78
|
## External App Config
|
|
66
79
|
|
|
67
80
|
External app integration config is not Boxdown workspace state. Boxdown writes
|
|
@@ -104,14 +117,16 @@ Boxdown starts from `assets/devcontainer/devcontainer.json` and rewrites:
|
|
|
104
117
|
- `postCreateCommand`, to call mounted container assets.
|
|
105
118
|
- `postStartCommand`, to call mounted container assets.
|
|
106
119
|
- `mounts`, to add the read-only asset mount, public-key mount, host Git config
|
|
107
|
-
snapshot mount, host `~/.agents` mount when that
|
|
108
|
-
`~/.codex/auth.json` read-only mount when that
|
|
120
|
+
snapshot mount, runtime secret mount, host `~/.agents` mount when that
|
|
121
|
+
directory exists, and host `~/.codex/auth.json` read-only mount when that
|
|
122
|
+
file exists.
|
|
109
123
|
- `containerEnv`, to point SSH bootstrap at the mounted public key and actual
|
|
110
124
|
container workspace.
|
|
111
125
|
|
|
112
126
|
The target repository is still the Dev Container workspace via
|
|
113
127
|
`--workspace-folder`.
|
|
114
128
|
|
|
115
|
-
Mounts are create-time container settings.
|
|
116
|
-
|
|
129
|
+
Mounts are create-time container settings. Existing containers created before
|
|
130
|
+
runtime-mounted secrets require `boxdown start --recreate`. The same applies
|
|
131
|
+
after creating or removing host `~/.agents` or `~/.codex/auth.json` so Docker
|
|
117
132
|
receives the updated mount set.
|
|
@@ -72,5 +72,6 @@ signing programs that are unavailable inside Linux.
|
|
|
72
72
|
If host `gh` is missing, logged out, or cannot return a token, the refresh is a
|
|
73
73
|
no-op.
|
|
74
74
|
|
|
75
|
-
`OP_SERVICE_ACCOUNT_TOKEN`, when
|
|
76
|
-
|
|
75
|
+
`OP_SERVICE_ACCOUNT_TOKEN`, when available through Boxdown's runtime secret
|
|
76
|
+
mount, is a 1Password service account token. It is not a GitHub token and is
|
|
77
|
+
not used by `gh` or GitHub Git operations.
|
|
@@ -129,6 +129,19 @@ do not prompt so existing targeted scripts keep working. Non-interactive
|
|
|
129
129
|
`purge` from an untracked directory fails safely instead of treating the current
|
|
130
130
|
directory as a workspace.
|
|
131
131
|
|
|
132
|
+
## Container runtime readiness
|
|
133
|
+
|
|
134
|
+
Commands that may create or start a devcontainer wait for Docker before writing
|
|
135
|
+
workspace metadata. Docker daemon and Buildx builder startup races are retried
|
|
136
|
+
for up to 60 seconds; the underlying `devcontainer up` command is still run only
|
|
137
|
+
once after readiness succeeds.
|
|
138
|
+
|
|
139
|
+
For a daemon timeout, run `docker info`. For a Buildx timeout, run
|
|
140
|
+
`docker buildx inspect`. Logged lifecycle errors also print the workspace
|
|
141
|
+
command-log path, which contains full redacted stdout and stderr. A generic Dev
|
|
142
|
+
Containers JSON error without nested output means the command log is the next
|
|
143
|
+
place to inspect.
|
|
144
|
+
|
|
132
145
|
## Doctor
|
|
133
146
|
|
|
134
147
|
`doctor` validates required host prerequisites: Node, Docker CLI, Docker
|
|
@@ -148,3 +161,9 @@ starts Docker. Required failures stop setup; warnings remain non-blocking.
|
|
|
148
161
|
|
|
149
162
|
GitHub CLI auth is optional. Missing or unauthenticated `gh` is reported as a
|
|
150
163
|
warning rather than a failure.
|
|
164
|
+
|
|
165
|
+
Doctor uses the same SSH signing identity precedence as environment creation.
|
|
166
|
+
It honors an explicit `user.signingkey` public-key value or path, can select a
|
|
167
|
+
single GitHub authentication-key match from multiple loaded identities, and
|
|
168
|
+
checks the selected key's GitHub signing registration when available. Signing
|
|
169
|
+
readiness warnings never fail setup.
|
package/docs/features/setup.md
CHANGED
|
@@ -20,6 +20,11 @@ interactive shell. It accepts:
|
|
|
20
20
|
--verbose
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Setup readiness runs before prompts or workspace state is written. A missing
|
|
24
|
+
Docker CLI fails immediately; a starting Docker daemon or discoverable Buildx
|
|
25
|
+
builder is polled once per second for up to 60 seconds. If this preflight fails,
|
|
26
|
+
setup leaves no workspace metadata, generated devcontainer config, or SSH key.
|
|
27
|
+
|
|
23
28
|
## Flow
|
|
24
29
|
|
|
25
30
|
1. Resolve the workspace to a real absolute path.
|
|
@@ -21,6 +21,11 @@ boxdown antigravity
|
|
|
21
21
|
Use `boxdown setup` when you want to prepare the devcontainer and SSH/app
|
|
22
22
|
integration without opening an interactive shell.
|
|
23
23
|
|
|
24
|
+
Start does not require a completed setup. It performs a fresh runtime-readiness
|
|
25
|
+
check, then writes workspace inventory metadata and creates the generated state
|
|
26
|
+
needed for the devcontainer. It does not install the setup-only SSH alias or
|
|
27
|
+
external application integrations.
|
|
28
|
+
|
|
24
29
|
`boxdown shell` remains supported as an alias for `boxdown start`, but `start`
|
|
25
30
|
is the canonical command used in help and documentation.
|
|
26
31
|
`boxdown cc` remains supported as an alias for `boxdown claude`, but `claude`
|
|
@@ -98,6 +103,11 @@ BOXDOWN_TTY_NORMALIZE=0 boxdown start
|
|
|
98
103
|
Use it when changing create-time settings such as image, features, mounts, or
|
|
99
104
|
Docker `runArgs`.
|
|
100
105
|
|
|
106
|
+
Existing containers created before Boxdown adopted runtime-mounted secrets
|
|
107
|
+
must be recreated once. After recreation, Boxdown-provided secrets remain
|
|
108
|
+
ordinary Bash-session environment variables but are absent from Docker
|
|
109
|
+
container configuration and `docker inspect`.
|
|
110
|
+
|
|
101
111
|
## Port Hint
|
|
102
112
|
|
|
103
113
|
The v1 asset config publishes container port `3000` with a dynamic host port.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Node SSH-Agent Proxy Signing Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Enable best-effort SSH commit signing for the non-root Boxdown user while preserving explicit user Git-signing preferences.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A root Node proxy forwards the raw mounted agent socket to a node-owned socket. The post-create hook starts and verifies that proxy before signing bootstrap. The bootstrap applies Boxdown's SSH default only when neither global nor local Git configuration expresses a signing preference.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node `net` Unix sockets, Bash lifecycle hooks, Node test runner.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Do not read, copy, mount, or log private keys.
|
|
14
|
+
- Signing and proxy failures must never block lifecycle commands or commits.
|
|
15
|
+
- Preserve explicit user `commit.gpgsign`, `gpg.format`, `user.signingkey`, and `gpg.program` settings.
|
|
16
|
+
- Existing containers require `--recreate`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### Task 1: Add the node-accessible SSH-agent proxy
|
|
21
|
+
|
|
22
|
+
**Files:**
|
|
23
|
+
|
|
24
|
+
- Create: `assets/devcontainer/utils/ssh-agent-proxy.mjs`
|
|
25
|
+
- Modify: `assets/devcontainer/hooks/post-create.sh`
|
|
26
|
+
- Test: `__tests__/app.test.ts`
|
|
27
|
+
|
|
28
|
+
- [ ] **Step 1: Write a failing proxy forwarding test**
|
|
29
|
+
|
|
30
|
+
Create a temporary raw Unix server, launch the proxy with a temporary target
|
|
31
|
+
socket and the current test user's UID/GID, send bytes through the target, and
|
|
32
|
+
assert the raw server receives and echoes them.
|
|
33
|
+
|
|
34
|
+
- [ ] **Step 2: Run the focused test to verify it fails**
|
|
35
|
+
|
|
36
|
+
Run: `fnm exec --using v24.15.0 -- node --import tsx --test --test-name-pattern='forwards node SSH-agent connections' __tests__/app.test.ts`
|
|
37
|
+
|
|
38
|
+
Expected: FAIL because the proxy asset does not exist.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 3: Implement the proxy and lifecycle startup helper**
|
|
41
|
+
|
|
42
|
+
Implement a Node `net.createServer()` bridge that unlinks stale target sockets,
|
|
43
|
+
listens with mode `0600`, changes ownership to the supplied UID/GID, and pipes
|
|
44
|
+
each client to the raw socket. Start it with `sudo` in post-create, wait for
|
|
45
|
+
the target socket, and run `ssh-add -L` through it before signing bootstrap.
|
|
46
|
+
|
|
47
|
+
- [ ] **Step 4: Run the focused test to verify it passes**
|
|
48
|
+
|
|
49
|
+
Run the Task 1 command. Expected: PASS.
|
|
50
|
+
|
|
51
|
+
### Task 2: Route generated configuration and diagnostics through the proxy
|
|
52
|
+
|
|
53
|
+
**Files:**
|
|
54
|
+
|
|
55
|
+
- Modify: `src/config.ts`
|
|
56
|
+
- Modify: `assets/devcontainer/utils/git-signing-bootstrap.sh`
|
|
57
|
+
- Test: `__tests__/app.test.ts`
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 1: Write failing generated-config and hook tests**
|
|
60
|
+
|
|
61
|
+
Assert enabled plans set `SSH_AUTH_SOCK` to `/run/boxdown/ssh-agent-node.sock`,
|
|
62
|
+
retain the raw source only in `BOXDOWN_GIT_SIGNING_SOURCE_SOCKET`, and disable
|
|
63
|
+
signing with `container-agent-proxy-unavailable` when proxy readiness fails.
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 2: Run the focused tests to verify they fail**
|
|
66
|
+
|
|
67
|
+
Run: `fnm exec --using v24.15.0 -- node --import tsx --test --test-name-pattern='node SSH-agent proxy|proxy-unavailable' __tests__/app.test.ts`
|
|
68
|
+
|
|
69
|
+
Expected: FAIL because generated configuration still exposes the raw socket.
|
|
70
|
+
|
|
71
|
+
- [ ] **Step 3: Implement proxy socket configuration and failure reporting**
|
|
72
|
+
|
|
73
|
+
Add constants or literal paths for raw and node-facing sockets, update generated
|
|
74
|
+
container environment, and teach the bootstrap to report the proxy reason
|
|
75
|
+
without replacing user configuration.
|
|
76
|
+
|
|
77
|
+
- [ ] **Step 4: Run the focused tests to verify they pass**
|
|
78
|
+
|
|
79
|
+
Run the Task 2 command. Expected: PASS.
|
|
80
|
+
|
|
81
|
+
### Task 3: Preserve explicit user signing configuration
|
|
82
|
+
|
|
83
|
+
**Files:**
|
|
84
|
+
|
|
85
|
+
- Modify: `assets/devcontainer/utils/git-signing-bootstrap.sh`
|
|
86
|
+
- Modify: `docs/features/commit-signing.md`
|
|
87
|
+
- Modify: `docs/superpowers/specs/2026-07-11-default-commit-signing-design.md`
|
|
88
|
+
- Test: `__tests__/app.test.ts`
|
|
89
|
+
|
|
90
|
+
- [ ] **Step 1: Write failing bootstrap tests**
|
|
91
|
+
|
|
92
|
+
Add separate tests for repository-local `commit.gpgsign=false`, global
|
|
93
|
+
non-SSH `gpg.format`, and an explicit SSH signing key. Assert the first two
|
|
94
|
+
remain unchanged; assert the SSH case maps only its public-key path to the
|
|
95
|
+
mounted snapshot and signs successfully.
|
|
96
|
+
|
|
97
|
+
- [ ] **Step 2: Run the focused tests to verify they fail**
|
|
98
|
+
|
|
99
|
+
Run: `fnm exec --using v24.15.0 -- node --import tsx --test --test-name-pattern='preserves explicit.*signing' __tests__/app.test.ts`
|
|
100
|
+
|
|
101
|
+
Expected: FAIL because the bootstrap currently overwrites global SSH settings.
|
|
102
|
+
|
|
103
|
+
- [ ] **Step 3: Implement explicit-preference detection**
|
|
104
|
+
|
|
105
|
+
Check local configuration first, then copied global configuration. Preserve an
|
|
106
|
+
explicit opt-out or non-SSH configuration; use Boxdown defaults only with no
|
|
107
|
+
explicit preference. For explicit SSH configuration, replace only an
|
|
108
|
+
unavailable public-key path with the selected mounted key.
|
|
109
|
+
|
|
110
|
+
- [ ] **Step 4: Run the focused tests to verify they pass**
|
|
111
|
+
|
|
112
|
+
Run the Task 3 command. Expected: PASS.
|
|
113
|
+
|
|
114
|
+
### Task 4: Document and verify
|
|
115
|
+
|
|
116
|
+
**Files:**
|
|
117
|
+
|
|
118
|
+
- Modify: `docs/features/commit-signing.md`
|
|
119
|
+
- Modify: `docs/superpowers/specs/2026-07-11-default-commit-signing-design.md`
|
|
120
|
+
- Create: `.changeset/<generated-name>.md`
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 1: Document the proxy, user-precedence policy, and `--recreate` requirement**
|
|
123
|
+
|
|
124
|
+
- [ ] **Step 2: Add a patch changeset**
|
|
125
|
+
|
|
126
|
+
- [ ] **Step 3: Run complete verification**
|
|
127
|
+
|
|
128
|
+
Run: `pnpm test`, `pnpm lint`, `pnpm build`, `bash -n assets/devcontainer/utils/git-signing-bootstrap.sh`, `node --check assets/devcontainer/utils/ssh-agent-proxy.mjs`, and `git diff --check`.
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 4: Commit**
|
|
131
|
+
|
|
132
|
+
Use: `git commit -m "fix: proxy SSH agent for commit signing"`
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Runtime Secret Environment Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Keep Boxdown-provided secrets available in Bash sessions without writing their values to the repository, persistent Boxdown state, Docker environment configuration, or workspace logs.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A private per-workspace runtime directory holds three secret files and is mounted read-only at `/run/boxdown/secrets`. A Bash bootstrap exports values for interactive, non-interactive, SSH, and Boxdown coding-agent Bash sessions. Docker configuration carries only paths; metadata inspection requests only image fields; logging structurally redacts known assignments.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js 24, TypeScript, Bash, `node:test`, Docker/Dev Containers configuration.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Do not print, log, serialize, or place a secret value in generated configuration, command arguments, metadata, or diagnostics.
|
|
14
|
+
- Fixed Boxdown secret names: `ANTHROPIC_API_KEY`, `SNYK_TOKEN`, and `OP_SERVICE_ACCOUNT_TOKEN`.
|
|
15
|
+
- Missing values and unavailable 1Password access are non-blocking; the affected variable is absent.
|
|
16
|
+
- `.env.development` is project-owned: Boxdown does not create, edit, delete, or migrate it.
|
|
17
|
+
- Runtime secret directories/files use `0700`/`0600` and are outside `workspaceDataDir`.
|
|
18
|
+
- Existing containers require `boxdown start --recreate`; do not add legacy cleanup or `doctor --fix-secrets`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## File Structure
|
|
23
|
+
|
|
24
|
+
| File | Responsibility |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `src/constants.ts`, `src/paths.ts` | Runtime secret paths and fixed secret-name constants. |
|
|
27
|
+
| `src/config.ts`, `assets/devcontainer/devcontainer.json` | Read-only mount and non-secret Bash bootstrap config. |
|
|
28
|
+
| `assets/devcontainer/hooks/initialize.sh` | Atomically create/remove runtime secret files. |
|
|
29
|
+
| `assets/devcontainer/utils/secret-env-bootstrap.sh` | Read fixed files and export present values without output. |
|
|
30
|
+
| `assets/devcontainer/hooks/post-create.sh`, `src/shell.ts` | Source bootstrap for interactive and Boxdown-launched Bash sessions. |
|
|
31
|
+
| `src/main.ts`, `src/purge.ts` | Remove workspace runtime state after down/purge. |
|
|
32
|
+
| `src/devcontainer.ts`, `src/logging.ts` | Narrow Docker inspection and structural redaction. |
|
|
33
|
+
| `src/doctor.ts`, docs, `__tests__/app.test.ts` | Safety checks, documentation, and regression tests. |
|
|
34
|
+
|
|
35
|
+
### Task 1: Define runtime secret state and remove workspace injection
|
|
36
|
+
|
|
37
|
+
**Files:**
|
|
38
|
+
|
|
39
|
+
- Modify: `src/constants.ts`, `src/paths.ts`, `src/config.ts`, `assets/devcontainer/devcontainer.json`, `assets/devcontainer/hooks/initialize.sh`, `assets/devcontainer/hooks/post-start.sh`, `__tests__/app.test.ts`
|
|
40
|
+
|
|
41
|
+
**Interfaces:**
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export const BOXDOWN_SECRET_ENV_NAMES = ['ANTHROPIC_API_KEY', 'SNYK_TOKEN', 'OP_SERVICE_ACCOUNT_TOKEN'] as const
|
|
45
|
+
export const BOXDOWN_CONTAINER_SECRET_ENV_DIR = '/run/boxdown/secrets'
|
|
46
|
+
export const BOXDOWN_CONTAINER_SECRET_ENV_BOOTSTRAP = '/opt/boxdown/devcontainer/utils/secret-env-bootstrap.sh'
|
|
47
|
+
export function defaultRuntimeRoot (env?: NodeJS.ProcessEnv): string
|
|
48
|
+
// WorkspaceContext adds runtimeRoot, workspaceRuntimeDir, workspaceSecretEnvDir.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- [ ] **Step 1: Write failing tests**
|
|
52
|
+
|
|
53
|
+
Create a context with `BOXDOWN_RUNTIME_HOME`. Assert that its secret directory starts below runtime root but not persistent workspace data. Assert generated config mounts that directory read-only at `/run/boxdown/secrets`, sets the non-secret bootstrap `BASH_ENV`, retains `NODE_ENV`, and serializes none of `--env-file`, `.env.development`, or the three secret names.
|
|
54
|
+
|
|
55
|
+
- [ ] **Step 2: Verify red**
|
|
56
|
+
|
|
57
|
+
Run `pnpm test -- --test-name-pattern "runtime secret|generated config"`. Expected: FAIL because the current config injects workspace-file and Docker-environment secrets.
|
|
58
|
+
|
|
59
|
+
- [ ] **Step 3: Implement paths and config**
|
|
60
|
+
|
|
61
|
+
Add the fixed constants. Resolve runtime root in this order: `BOXDOWN_RUNTIME_HOME`; then `join(XDG_RUNTIME_DIR, PACKAGE_NAME)`; then `join(tmpdir(), package-name plus UID)`. Add workspace-ID scoped runtime/secret paths. Append the read-only secret mount and `BASH_ENV`; remove the base `--env-file` pair and secret `containerEnv` values while preserving non-secret entries.
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 4: Implement host runtime-file refresh**
|
|
64
|
+
|
|
65
|
+
Pass only `BOXDOWN_SECRET_ENV_DIR` to `initializeCommand`. Set `umask 077`; create the directory at `0700`; write each nonempty file through `mktemp` plus `chmod 0600` plus atomic rename; remove a file when its host value is absent. Read the existing 1Password reference into a local variable and never echo it. Remove all `.env.development` preparation/upsert logic and post-start deletion.
|
|
66
|
+
|
|
67
|
+
- [ ] **Step 5: Verify green and commit**
|
|
68
|
+
|
|
69
|
+
Run `pnpm test -- --test-name-pattern "runtime secret|generated config|devcontainer git config hooks"` and `bash -n assets/devcontainer/hooks/initialize.sh assets/devcontainer/hooks/post-start.sh`; expect both to pass. Commit the listed sources and tests as `feat: mount runtime secret environment`.
|
|
70
|
+
|
|
71
|
+
### Task 2: Export secret files in supported Bash sessions and clean runtime state
|
|
72
|
+
|
|
73
|
+
**Files:**
|
|
74
|
+
|
|
75
|
+
- Create: `assets/devcontainer/utils/secret-env-bootstrap.sh`
|
|
76
|
+
- Modify: `assets/devcontainer/hooks/post-create.sh`, `src/shell.ts`, `src/main.ts`, `src/purge.ts`, `__tests__/app.test.ts`
|
|
77
|
+
|
|
78
|
+
**Interfaces:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Bootstrap reads /run/boxdown/secrets/<fixed-name> and exports only present values.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- [ ] **Step 1: Write failing tests**
|
|
85
|
+
|
|
86
|
+
Run the bootstrap against a temporary secret directory with sentinel values. Assert each present value is exported, an absent file remains unset, and stdout/stderr do not contain a sentinel. Assert interactive shell/agent scripts source the bootstrap before `exec`. Assert down/purge remove only workspace runtime state.
|
|
87
|
+
|
|
88
|
+
- [ ] **Step 2: Verify red**
|
|
89
|
+
|
|
90
|
+
Run `pnpm test -- --test-name-pattern "secret bootstrap|runtime secret cleanup|interactive shell setup"`. Expected: FAIL because no bootstrap or runtime cleanup exists.
|
|
91
|
+
|
|
92
|
+
- [ ] **Step 3: Implement bootstrap/session loading**
|
|
93
|
+
|
|
94
|
+
Create a no-output script that reads the three fixed files with `IFS= read -r`, exports only nonempty values, and never sources secret file contents as code. Add an idempotent source line to the node user's `.bashrc` in post-create. Add the same bootstrap source before final `exec` in both shell script builders; never add a value to `interactiveShellEnvArgs`.
|
|
95
|
+
|
|
96
|
+
- [ ] **Step 4: Implement safe runtime cleanup**
|
|
97
|
+
|
|
98
|
+
Add a path-contained helper that removes only `context.workspaceRuntimeDir`, accepts absence, and never removes `runtimeRoot`. Invoke it after successful down and during purge before persistent state removal.
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 5: Verify green and commit**
|
|
101
|
+
|
|
102
|
+
Run `pnpm test -- --test-name-pattern "secret bootstrap|runtime secret cleanup|interactive shell setup"` and `bash -n assets/devcontainer/utils/secret-env-bootstrap.sh assets/devcontainer/hooks/post-create.sh`; expect pass. Commit as `feat: export runtime secrets in Bash sessions`.
|
|
103
|
+
|
|
104
|
+
### Task 3: Replace full Docker inspection and harden logging
|
|
105
|
+
|
|
106
|
+
**Files:**
|
|
107
|
+
|
|
108
|
+
- Modify: `src/devcontainer.ts`, `src/logging.ts`, `__tests__/app.test.ts`
|
|
109
|
+
|
|
110
|
+
**Interfaces:**
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
export function parseDockerInspectImage (output: string, containerId: string): DockerImageInfo | undefined
|
|
114
|
+
export function redactKnownSecretEnvironmentAssignments (value: string): string
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- [ ] **Step 1: Write failing tests**
|
|
118
|
+
|
|
119
|
+
Test parsing two JSON-string lines representing image ID/name. Add a fake Docker complete-inspect fixture containing an OP sentinel, assert the lifecycle log omits it and inspect uses a narrow format. Test plain and Docker-JSON assignment forms for each known secret variable.
|
|
120
|
+
|
|
121
|
+
- [ ] **Step 2: Verify red**
|
|
122
|
+
|
|
123
|
+
Run `pnpm test -- --test-name-pattern "docker inspect|known secret|workspace logger"`. Expected: FAIL because inspect uses `{{json .}}` and logging is value-only.
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 3: Implement projection/redaction**
|
|
126
|
+
|
|
127
|
+
Use Docker format `{{json .Image}}` followed by `{{json .Config.Image}}`; parse exactly two scalar JSON lines and provide a container-ID-specific malformed-output error. Redact values after each fixed name before dynamic redactions and apply it through logger `#redact` for arguments, sections, errors, and child output while preserving the names.
|
|
128
|
+
|
|
129
|
+
- [ ] **Step 4: Verify green and commit**
|
|
130
|
+
|
|
131
|
+
Run `pnpm test -- --test-name-pattern "docker inspect|known secret|workspace logger"`; expect pass. Commit as `fix: keep container secrets out of workspace logs`.
|
|
132
|
+
|
|
133
|
+
### Task 4: Doctor coverage and documentation
|
|
134
|
+
|
|
135
|
+
**Files:**
|
|
136
|
+
|
|
137
|
+
- Modify: `src/doctor.ts`, `__tests__/app.test.ts`, `assets/devcontainer/README.md`, `docs/features/generated-config-and-state.md`, `docs/features/start-and-shell.md`
|
|
138
|
+
|
|
139
|
+
- [ ] **Step 1: Write failing tests**
|
|
140
|
+
|
|
141
|
+
Extend doctor fake-command tests to require a disposable runtime-secret mount probe and a warning when generated config contains env-file, `.env.development`, or a fixed secret name in `containerEnv`. Add existing-style static documentation assertions for session exposure and `--recreate`.
|
|
142
|
+
|
|
143
|
+
- [ ] **Step 2: Verify red**
|
|
144
|
+
|
|
145
|
+
Run `pnpm test -- --test-name-pattern "doctor|generated config|runtime secret"`. Expected: FAIL because doctor does not inspect those conditions.
|
|
146
|
+
|
|
147
|
+
- [ ] **Step 3: Implement non-blocking checks/docs**
|
|
148
|
+
|
|
149
|
+
Add `workspaceSecretEnvDir` to disposable mount probes. Add a `secret-environment-config` doctor result: missing generated config is warning, unsafe config is warning, safe config is ok; never read secret file contents. Update docs: values are ordinary Bash-session variables but not Docker config; `.env.development` is ignored; missing values are non-blocking; existing containers need recreate; legacy leaked values/logs require manual removal and rotation.
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 4: Verify green and commit**
|
|
152
|
+
|
|
153
|
+
Run `pnpm test -- --test-name-pattern "doctor|generated config|runtime secret"`; expect pass. Commit as `docs: describe runtime secret environment handling`.
|
|
154
|
+
|
|
155
|
+
### Task 5: Full verification
|
|
156
|
+
|
|
157
|
+
- [ ] **Step 1: Run the complete suite**
|
|
158
|
+
|
|
159
|
+
Run `pnpm test`, `pnpm lint`, `pnpm build`, `bash -n` for all changed hooks/utilities, `git diff --check`, and `git diff main...HEAD --check`. Expected: every command exits 0.
|
|
160
|
+
|
|
161
|
+
- [ ] **Step 2: Review secret-safety diff**
|
|
162
|
+
|
|
163
|
+
Run `git diff main...HEAD -- assets/devcontainer/devcontainer.json src/config.ts src/devcontainer.ts src/logging.ts`. Confirm no implementation path writes `.env.development`, uses `--env-file`, or contains literal token-like values.
|
|
164
|
+
|
|
165
|
+
- [ ] **Step 3: Commit a verification-only correction when required**
|
|
166
|
+
|
|
167
|
+
If verification requires a source correction, commit only that correction as `test: cover runtime secret environment regression`.
|
|
168
|
+
|
|
169
|
+
## Plan Self-Review
|
|
170
|
+
|
|
171
|
+
- Tasks 1–2 implement runtime-only state and compatible Bash-session exports.
|
|
172
|
+
- Task 3 removes the full-inspect log leak and adds a logging backstop.
|
|
173
|
+
- Task 4 adds doctor/documentation coverage without legacy cleanup.
|
|
174
|
+
- Task 5 runs full test, lint, build, Bash syntax, and diff checks.
|