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.
Files changed (53) hide show
  1. package/README.md +10 -0
  2. package/assets/devcontainer/README.md +29 -6
  3. package/assets/devcontainer/devcontainer.json +4 -9
  4. package/assets/devcontainer/hooks/initialize.sh +49 -27
  5. package/assets/devcontainer/hooks/post-create.sh +23 -1
  6. package/assets/devcontainer/hooks/post-start.sh +0 -10
  7. package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
  8. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  9. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  10. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  11. package/dist/bin/cli.cjs +1 -1
  12. package/dist/bin/cli.mjs +1 -1
  13. package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
  14. package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
  15. package/dist/main-O__JaiXe.mjs.map +1 -0
  16. package/dist/main.cjs +4 -1
  17. package/dist/main.d.cts +98 -26
  18. package/dist/main.d.cts.map +1 -1
  19. package/dist/main.d.mts +98 -26
  20. package/dist/main.d.mts.map +1 -1
  21. package/dist/main.mjs +2 -2
  22. package/docs/features/commit-signing.md +71 -0
  23. package/docs/features/generated-config-and-state.md +19 -4
  24. package/docs/features/github-auth-refresh.md +3 -2
  25. package/docs/features/lifecycle.md +19 -0
  26. package/docs/features/setup.md +5 -0
  27. package/docs/features/start-and-shell.md +10 -0
  28. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  29. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  30. package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
  31. package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
  32. package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
  33. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
  34. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  35. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  36. package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
  37. package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
  38. package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
  39. package/package.json +1 -1
  40. package/src/config.ts +14 -2
  41. package/src/constants.ts +7 -0
  42. package/src/container-runtime.ts +295 -0
  43. package/src/devcontainer.ts +57 -30
  44. package/src/doctor.ts +169 -46
  45. package/src/git-signing.ts +205 -25
  46. package/src/logging.ts +11 -1
  47. package/src/main.ts +97 -12
  48. package/src/paths.ts +21 -1
  49. package/src/process.ts +50 -10
  50. package/src/progress.ts +63 -8
  51. package/src/purge.ts +8 -0
  52. package/src/shell.ts +6 -0
  53. 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 directory exists, and host
108
- `~/.codex/auth.json` read-only mount when that file exists.
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. Use `boxdown start --recreate` after
116
- creating or removing host `~/.agents` or `~/.codex/auth.json` so Docker
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 present, is a 1Password service account token.
76
- It is not a GitHub token and is not used by `gh` or GitHub Git operations.
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.
@@ -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.