boxdown 1.0.0 → 1.2.1

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 (76) hide show
  1. package/README.md +135 -12
  2. package/assets/devcontainer/README.md +65 -21
  3. package/assets/devcontainer/devcontainer.json +27 -15
  4. package/assets/devcontainer/hooks/initialize.sh +76 -22
  5. package/assets/devcontainer/hooks/post-create.sh +70 -12
  6. package/assets/devcontainer/hooks/post-start.sh +20 -13
  7. package/assets/devcontainer/ssh-config-install.sh +12 -3
  8. package/assets/devcontainer/start.sh +721 -44
  9. package/assets/devcontainer/utils/coding-agent-cli-update.sh +267 -3
  10. package/assets/devcontainer/utils/deps-install.sh +68 -0
  11. package/assets/devcontainer/utils/git-config-bootstrap.sh +128 -0
  12. package/assets/devcontainer/utils/git-signing-bootstrap.sh +109 -0
  13. package/assets/devcontainer/utils/python-bootstrap.sh +69 -0
  14. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  15. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  16. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  17. package/assets/devcontainer/utils/ssh-bootstrap.sh +9 -0
  18. package/dist/bin/cli.cjs +1 -1
  19. package/dist/bin/cli.mjs +1 -1
  20. package/dist/main-BDgyf2t5.cjs +5758 -0
  21. package/dist/main-J4_2Up3o.mjs +5718 -0
  22. package/dist/main-J4_2Up3o.mjs.map +1 -0
  23. package/dist/main.cjs +5 -2
  24. package/dist/main.d.cts +501 -4
  25. package/dist/main.d.cts.map +1 -1
  26. package/dist/main.d.mts +501 -4
  27. package/dist/main.d.mts.map +1 -1
  28. package/dist/main.mjs +2 -2
  29. package/docs/README.md +1 -0
  30. package/docs/architecture.md +32 -0
  31. package/docs/development.md +13 -6
  32. package/docs/features/README.md +2 -0
  33. package/docs/features/commit-signing.md +94 -0
  34. package/docs/features/generated-config-and-state.md +73 -5
  35. package/docs/features/github-auth-refresh.md +15 -2
  36. package/docs/features/lifecycle.md +103 -11
  37. package/docs/features/setup.md +66 -0
  38. package/docs/features/ssh-config-and-proxy.md +228 -7
  39. package/docs/features/start-and-shell.md +45 -5
  40. package/docs/superpowers/plans/2026-07-11-default-commit-signing.md +110 -0
  41. package/docs/superpowers/plans/2026-07-11-progress-ci-regression.md +125 -0
  42. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  43. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  44. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +416 -0
  45. package/docs/superpowers/specs/2026-07-11-progress-ci-regression-design.md +43 -0
  46. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  47. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  48. package/docs/testing.md +35 -2
  49. package/docs/todo.md +2 -2
  50. package/package.json +1 -1
  51. package/src/claude-app-config.ts +304 -0
  52. package/src/cli-style.ts +43 -0
  53. package/src/codex-app-config.ts +656 -0
  54. package/src/config.ts +80 -10
  55. package/src/constants.ts +12 -0
  56. package/src/devcontainer.ts +511 -64
  57. package/src/doctor.ts +292 -30
  58. package/src/git-signing.ts +267 -0
  59. package/src/interactive-prompts.ts +692 -0
  60. package/src/list.ts +16 -2
  61. package/src/logging.ts +164 -0
  62. package/src/main.ts +1214 -63
  63. package/src/metadata.ts +52 -3
  64. package/src/package-info.ts +18 -0
  65. package/src/paths.ts +71 -11
  66. package/src/process.ts +80 -2
  67. package/src/progress.ts +593 -0
  68. package/src/purge.ts +216 -0
  69. package/src/shell.ts +25 -0
  70. package/src/ssh-config.ts +134 -16
  71. package/src/ssh-install-targets.ts +111 -0
  72. package/src/ssh-key.ts +53 -13
  73. package/src/status.ts +5 -0
  74. package/dist/main-BuEptwlL.cjs +0 -1707
  75. package/dist/main-ZFTrSVgt.mjs +0 -1685
  76. package/dist/main-ZFTrSVgt.mjs.map +0 -1
@@ -0,0 +1,416 @@
1
+ # Default Commit Signing Design
2
+
3
+ ## Summary
4
+
5
+ Boxdown will attempt to enable SSH commit signing by default in every newly
6
+ created environment. It will forward the host SSH agent into the container,
7
+ select a signing identity without user interaction when that choice is
8
+ unambiguous, and configure container-global Git settings for SSH signing.
9
+
10
+ Signing is best-effort. If Boxdown cannot select or use a signing identity, it
11
+ will warn, disable signing in the container-global Git configuration, and allow
12
+ unsigned commits. Signing readiness will never make `boxdown setup` fail.
13
+
14
+ This design applies only to newly created Boxdown environments. It does not
15
+ migrate existing containers or repair repository-local Git settings written by
16
+ older Boxdown versions.
17
+
18
+ ## Goals
19
+
20
+ - Sign commits by default without a separate Boxdown signing setup command.
21
+ - Keep private signing keys on the host and expose only an agent-backed signing
22
+ capability to the container.
23
+ - Provide the same signing capability through `boxdown setup`, `start`,
24
+ `codex`, `claude`, `opencode`, `antigravity`, SSH proxy, and tunnel flows.
25
+ - Continue allowing commits when signing cannot be configured or validated.
26
+ - Detect signing problems in `boxdown doctor` and the setup preflight without
27
+ making signing readiness a setup prerequisite.
28
+ - Explain the difference between a cryptographically signed commit and a
29
+ commit that GitHub displays as Verified.
30
+
31
+ ## Non-goals
32
+
33
+ - Migrating existing Boxdown environments or legacy `.git/config` values.
34
+ - Copying a private GPG or SSH key into a container or Boxdown state.
35
+ - Automatically changing the user's GitHub account or registering signing
36
+ keys without explicit user action.
37
+ - Guaranteeing that a host agent remains available after an interactive
38
+ session has started.
39
+ - Intercepting `git commit` and automatically retrying it with
40
+ `--no-gpg-sign`.
41
+
42
+ ## Approaches Considered
43
+
44
+ ### SSH signing through the Docker agent bridge
45
+
46
+ This is the selected approach. Docker Desktop exposes the host SSH agent to a
47
+ container through `/run/host-services/ssh-auth.sock`, while native Linux Docker
48
+ can bind-mount the host path in `SSH_AUTH_SOCK`. Git supports signing through
49
+ that agent with `gpg.format=ssh`.
50
+
51
+ This approach works for both SSH sessions and Boxdown's primary
52
+ `devcontainer exec` execution path. It does not copy private key material.
53
+
54
+ ### GPG-agent forwarding over SSH
55
+
56
+ GPG-agent forwarding was validated successfully, but the forwarded socket
57
+ exists only during an SSH connection. Boxdown launches shells and coding-agent
58
+ CLIs through `devcontainer exec`, so GPG forwarding would require an additional
59
+ long-lived host proxy and remote socket lifecycle management. It is not the
60
+ default design.
61
+
62
+ ### Container-owned signing key
63
+
64
+ A key persisted in Boxdown state would remain available independently of the
65
+ host agent, but the container could read and copy the private key. Every new
66
+ key would also require GitHub registration. This weakens Boxdown's isolation
67
+ boundary and is rejected.
68
+
69
+ ## Signing Policy
70
+
71
+ Signing is default-on but best-effort:
72
+
73
+ 1. Boxdown attempts to resolve one host SSH signing identity.
74
+ 2. Boxdown attempts to expose the host agent to the container.
75
+ 3. The container verifies that the selected identity is loaded and can create
76
+ a disposable signed commit.
77
+ 4. If all checks pass, container-global `commit.gpgsign` is set to `true`.
78
+ 5. If any check fails, container-global `commit.gpgsign` is set to `false` and
79
+ Boxdown reports a warning.
80
+
81
+ Boxdown must never select the first of multiple ambiguous candidates. If it
82
+ cannot identify exactly one intended key, signing is disabled and commits
83
+ continue unsigned. This behavior must be documented in the README, signing
84
+ feature documentation, `boxdown doctor` output, and setup warnings.
85
+
86
+ Boxdown will no longer write `commit.gpgsign` into the repository-local
87
+ `.git/config`. Repository-local configuration is shared with the host and is
88
+ not container-owned state.
89
+
90
+ An explicit repository-local `commit.gpgsign=true` remains the user's policy
91
+ and can override Boxdown's container-global fallback. Boxdown does not rewrite
92
+ explicit repository policy to guarantee unsigned fallback.
93
+
94
+ ## Host Signing Resolution
95
+
96
+ A new host-side module will produce a typed signing plan before generated
97
+ devcontainer configuration is written. The plan records whether signing is
98
+ enabled, the selected public key, its fingerprint, the agent mount source, and
99
+ a stable reason when signing is unavailable.
100
+
101
+ Public keys are compared by algorithm and base64 key data. Comments are not
102
+ part of identity matching.
103
+
104
+ Boxdown resolves a signing identity in this order:
105
+
106
+ 1. If the host Git configuration uses `gpg.format=ssh`, resolve its
107
+ `user.signingKey`. Select it only if the corresponding public key is loaded
108
+ in the host agent.
109
+ 2. Otherwise, when authenticated GitHub CLI access and the network are
110
+ available, intersect the loaded agent identities with the current GitHub
111
+ user's SSH authentication keys. Select the key only when exactly one loaded
112
+ identity matches.
113
+ 3. Otherwise, select the loaded identity only when the agent contains exactly
114
+ one identity.
115
+ 4. With zero or multiple unresolved identities, produce a disabled signing
116
+ plan and a warning reason. Do not guess.
117
+
118
+ `user.signingKey` accepts an inline SSH public key, Git's `key::` public-key
119
+ form, an absolute path, a `~/` path, or a path relative to the workspace.
120
+ Boxdown reads public-key files only. An explicitly configured key is
121
+ authoritative: unreadable, malformed, or unloaded configured keys disable
122
+ signing and never fall through to GitHub or single-identity selection.
123
+
124
+ Failure to execute `ssh-add`, query GitHub, parse a public key, or resolve a
125
+ single identity is non-fatal.
126
+
127
+ The selected public key is written outside the repository under:
128
+
129
+ ```text
130
+ ~/.local/share/boxdown/workspaces/<workspace-id>/git-signing/signing-key.pub
131
+ ```
132
+
133
+ The signing state directory contains public material only. It is mounted
134
+ read-only at `/opt/boxdown/state/git-signing`.
135
+
136
+ ## Agent Socket Resolution
137
+
138
+ The generated configuration uses the fixed container path:
139
+
140
+ ```text
141
+ /run/boxdown/ssh-agent.sock
142
+ ```
143
+
144
+ and sets:
145
+
146
+ ```text
147
+ SSH_AUTH_SOCK=/run/boxdown/ssh-agent.sock
148
+ ```
149
+
150
+ On Docker Desktop for macOS and Linux, the mount source is Docker Desktop's
151
+ host-service socket:
152
+
153
+ ```text
154
+ /run/host-services/ssh-auth.sock
155
+ ```
156
+
157
+ On native Linux Docker, the source is the host value of `SSH_AUTH_SOCK`.
158
+
159
+ Boxdown probes a candidate source through Docker before adding it to generated
160
+ configuration. If the probe fails, the mount and `SSH_AUTH_SOCK` container
161
+ environment value are omitted and the signing plan is disabled. This prevents
162
+ an unavailable bind source from blocking container creation.
163
+
164
+ Because mounts are create-time state, an environment created while the agent
165
+ bridge is unavailable remains unsigned until it is recreated. Boxdown warns
166
+ about this limitation; it does not mutate existing container mounts.
167
+
168
+ ## Generated Devcontainer Configuration
169
+
170
+ When signing is enabled, the generated configuration includes mounts
171
+ equivalent to:
172
+
173
+ ```json
174
+ {
175
+ "mounts": [
176
+ "type=bind,source=/run/host-services/ssh-auth.sock,target=/run/boxdown/ssh-agent.sock",
177
+ "type=bind,source=<workspace-signing-state>,target=/opt/boxdown/state/git-signing,readonly"
178
+ ],
179
+ "containerEnv": {
180
+ "SSH_AUTH_SOCK": "/run/boxdown/ssh-agent.sock"
181
+ }
182
+ }
183
+ ```
184
+
185
+ Native Linux substitutes the resolved host agent socket for the first source.
186
+ The signing mount and environment value are absent from a disabled plan.
187
+
188
+ Generated configuration accepts the resolved signing plan as input rather
189
+ than discovering host or Docker state inside the pure configuration builder.
190
+ This keeps mount generation deterministic and unit-testable.
191
+
192
+ ## Container Git Configuration
193
+
194
+ Container Git bootstrap will stop unconditionally disabling commit signing.
195
+ When the selected identity and socket validate, it will configure the writable
196
+ container-global Git configuration with:
197
+
198
+ ```ini
199
+ [gpg]
200
+ format = ssh
201
+ [user]
202
+ signingKey = /opt/boxdown/state/git-signing/signing-key.pub
203
+ [commit]
204
+ gpgSign = true
205
+ ```
206
+
207
+ The bootstrap removes incompatible copied host settings such as
208
+ `gpg.program=/opt/homebrew/bin/gpg`. It does not need a global
209
+ `gpg.ssh.allowedSignersFile` to create signatures. A disposable repository may
210
+ use a temporary allowed-signers file when validating its own signature.
211
+
212
+ When validation fails, the container-global configuration uses:
213
+
214
+ ```ini
215
+ [commit]
216
+ gpgSign = false
217
+ ```
218
+
219
+ and removes Boxdown-owned SSH signing values that would otherwise leave an
220
+ inconsistent configuration.
221
+
222
+ Tag-signing behavior is not changed beyond removing the current unconditional
223
+ `tag.gpgsign=false`. When a copied host configuration explicitly enables tag
224
+ signing, the selected SSH signing configuration can satisfy it. Commit signing
225
+ is the feature guaranteed by this design.
226
+
227
+ ## Lifecycle Consistency
228
+
229
+ The Docker agent mount makes the host agent available to all container
230
+ processes. It therefore covers both SSH access and the `devcontainer exec`
231
+ path used by `boxdown start` and coding-agent commands.
232
+
233
+ A focused container script owns signing configuration and validation. A shared
234
+ `ensureContainerGitSigning` lifecycle helper invokes it after a container is
235
+ started or reused and before control is handed to a shell, coding agent, SSH
236
+ proxy, tunnel, or setup completion.
237
+
238
+ All entry points use the same sequence:
239
+
240
+ 1. Resolve the host signing plan.
241
+ 2. Write signing-aware generated configuration.
242
+ 3. Start or reuse the container.
243
+ 4. Refresh container signing readiness.
244
+ 5. Continue normally whether signing is enabled or disabled.
245
+
246
+ The signing refresh performs a disposable signed-commit test. A failed test
247
+ changes the container-global fallback to unsigned and reports a warning. The
248
+ test does not write to the user's repository.
249
+
250
+ If the host agent or selected key disappears after an interactive process has
251
+ started, a later `git commit` can still fail. Boxdown will document this narrow
252
+ runtime limitation instead of replacing or wrapping the Git executable.
253
+
254
+ ## Doctor and Setup Preflight
255
+
256
+ Signing readiness is optional. Every signing-related doctor result has `pass`
257
+ or `warn` severity and never contributes a required setup failure.
258
+
259
+ Host and preliminary checks cover:
260
+
261
+ 1. The host SSH agent can be queried.
262
+ 2. A signing identity can be selected unambiguously.
263
+ 3. Docker can expose the candidate agent socket to a container.
264
+ 4. When GitHub CLI and the network are available, the selected key is
265
+ registered as a GitHub SSH signing key.
266
+
267
+ Container checks cover:
268
+
269
+ 1. `SSH_AUTH_SOCK` is reachable.
270
+ 2. The selected public key is present in `ssh-add -L`.
271
+ 3. A disposable Git repository can create a signed commit.
272
+ 4. The disposable commit contains a valid SSH signature from the selected
273
+ fingerprint.
274
+
275
+ `boxdown setup` runs relevant host checks during its existing preliminary
276
+ doctor stage and prints warnings before container creation. After the
277
+ container starts, the shared signing refresh supplies the full container
278
+ validation.
279
+
280
+ `boxdown doctor` runs the host checks on every invocation. It runs full
281
+ container validation in an existing Boxdown container. When no Boxdown
282
+ container exists, it may use an already available local Boxdown image for a
283
+ disposable probe; it does not pull an image solely for signing diagnostics.
284
+ If a full probe is unavailable, doctor reports that limitation as a warning.
285
+
286
+ ## GitHub Verification
287
+
288
+ A commit is cryptographically signed when Git creates and validates its SSH
289
+ signature. GitHub displays that commit as Verified only when the corresponding
290
+ public key is registered in the user's GitHub account as an SSH signing key.
291
+
292
+ GitHub treats authentication and signing registrations separately. A public
293
+ key already registered for SSH authentication must be uploaded a second time
294
+ with type `signing`.
295
+
296
+ Boxdown checks the selected key against `user/ssh_signing_keys` when possible.
297
+ If it is absent, Boxdown keeps local signing enabled and warns that GitHub will
298
+ not display Verified until the user completes the one-time registration. The
299
+ warning includes an actionable command such as:
300
+
301
+ ```bash
302
+ gh ssh-key add <public-key-file> --type signing --title "Boxdown commit signing"
303
+ ```
304
+
305
+ Boxdown does not execute this account mutation automatically. GitHub or
306
+ network unavailability never disables a locally usable signing identity after
307
+ that identity has already been selected.
308
+
309
+ ## Security Model
310
+
311
+ The agent bridge does not expose private key bytes, but any process in the
312
+ container that can access the socket can request operations from every identity
313
+ exposed by that host agent. Enabling signing therefore grants the container a
314
+ signing oracle and potentially SSH authentication capability for the lifetime
315
+ of the mount.
316
+
317
+ Documentation will recommend a dedicated signing identity or an agent that
318
+ requires confirmation for sensitive keys. The capability is default-on by
319
+ product decision, but doctor output and security documentation must describe
320
+ the exposure accurately.
321
+
322
+ ## Error Reporting
323
+
324
+ Signing warnings use stable reasons so CLI and JSON doctor output remain
325
+ testable. At minimum, reasons distinguish:
326
+
327
+ - host agent unavailable;
328
+ - no loaded identities;
329
+ - ambiguous loaded identities;
330
+ - configured host SSH signing key unreadable or malformed;
331
+ - configured host SSH signing key not loaded;
332
+ - host agent socket unavailable;
333
+ - no local Docker image available for the agent mount probe;
334
+ - Docker agent bridge unavailable;
335
+ - public-key snapshot failure;
336
+ - selected identity missing in the container;
337
+ - disposable signing failure;
338
+ - GitHub signing registration missing;
339
+ - GitHub registration check unavailable.
340
+
341
+ Human-readable warnings state that commits will remain unsigned but are not
342
+ blocked. A missing GitHub registration warning instead states that commits are
343
+ signed locally but will not appear Verified on GitHub.
344
+
345
+ User-facing lifecycle commands print the concise warning. The workspace
346
+ command log additionally records the stable reason code and sanitized detail,
347
+ without public-key material or secrets. Internal SSH proxy startup remains
348
+ log-only. Generated configuration passes the stable host reason into container
349
+ bootstrap so later lifecycle warnings preserve the original diagnosis.
350
+
351
+ Docker Desktop may expose the mounted host-agent socket only to root. Boxdown
352
+ therefore relays it through a root-owned Unix-socket proxy to a node-owned
353
+ socket. Explicit user Git-signing preferences remain authoritative; Boxdown
354
+ uses SSH signing only as the default when no preference exists.
355
+
356
+ ## Testing Strategy
357
+
358
+ ### Unit tests
359
+
360
+ - Resolve a host-configured SSH signing key that is loaded in the agent.
361
+ - Resolve exactly one loaded key that matches GitHub authentication keys.
362
+ - Resolve a single loaded key without GitHub access.
363
+ - Disable signing for zero keys, multiple ambiguous keys, malformed keys, and
364
+ command or API failures.
365
+ - Compare public keys without considering comments.
366
+ - Generate Docker Desktop and native Linux socket mounts correctly.
367
+ - Omit signing mounts and environment values for disabled plans.
368
+ - Generate the signing public-key state mount as read-only.
369
+ - Configure SSH signing after a successful container probe.
370
+ - Fall back to unsigned global configuration after every validation failure.
371
+ - Prove that post-create no longer writes repository-local
372
+ `commit.gpgsign`.
373
+ - Report all signing preflight failures as warnings rather than failures.
374
+ - Exercise `setup`, `start`, coding-agent, SSH proxy, and tunnel paths through
375
+ the shared signing lifecycle.
376
+
377
+ ### Shell-script tests
378
+
379
+ Container signing bootstrap tests use temporary Git homes, repositories, fake
380
+ agent output, and disposable public keys. They verify both the enabled Git
381
+ configuration and the non-blocking unsigned fallback.
382
+
383
+ ### Docker integration test
384
+
385
+ A gated manual or integration test mounts a real agent bridge into a disposable
386
+ container, creates an empty signed commit, and verifies the signature and
387
+ fingerprint. It is skipped when Docker or a usable agent identity is absent.
388
+ It does not push a commit or change a GitHub account.
389
+
390
+ ### Manual acceptance
391
+
392
+ Create a new Boxdown environment and verify signed commits from:
393
+
394
+ - `boxdown start`;
395
+ - `boxdown codex`;
396
+ - an SSH session through the generated Boxdown alias.
397
+
398
+ Push a test branch only when explicitly authorized, then verify GitHub's badge
399
+ before and after registering the selected public key as a signing key.
400
+
401
+ ## Documentation
402
+
403
+ Implementation updates will include:
404
+
405
+ - a dedicated commit-signing feature document;
406
+ - README behavior and security notes;
407
+ - setup and doctor documentation;
408
+ - architecture documentation for signing state, generated mounts, and trust
409
+ boundaries;
410
+ - troubleshooting for ambiguity, absent agents, Docker mount failures,
411
+ unsigned fallback, and missing GitHub verification;
412
+ - a changeset describing default-on best-effort signing.
413
+
414
+ The documentation will explicitly state that multiple ambiguous agent keys
415
+ disable signing rather than causing Boxdown to guess, and that this fallback
416
+ allows unsigned commits.
@@ -0,0 +1,43 @@
1
+ # Progress CI Regression Design
2
+
3
+ ## Goal
4
+
5
+ Restore the test suite by fixing progress checklist cleanup in every output mode
6
+ and updating stale spinner-label assertions without changing user-facing output.
7
+
8
+ ## Root Cause
9
+
10
+ `ProgressReporter.end()` stops timers before checking the output mode, but it
11
+ clears checklist state only after the interactive-only rendering guards. In
12
+ `none` and `verbose` modes, `isChecklistActive()` therefore remains true after
13
+ the reporter ends.
14
+
15
+ The SSH spinner labels remain present and are used when no checklist is active.
16
+ Their source expressions became conditional when checklist-owned output was
17
+ introduced, so the source-text test's direct-property regular expressions no
18
+ longer match them.
19
+
20
+ ## Design
21
+
22
+ Move internal checklist cleanup ahead of the output-mode and section-rendering
23
+ guards in `ProgressReporter.end()`. Ending a reporter will always clear its
24
+ steps and rendered-line count, while only interactive reporters with an open
25
+ section will write the visual terminator.
26
+
27
+ Keep the existing source-presence test because it guards friendly spinner copy,
28
+ but make the two SSH assertions check for the label text rather than requiring
29
+ a particular property-expression shape. Existing runtime tests continue to
30
+ verify that checklist mode suppresses the standalone spinners.
31
+
32
+ ## Scope
33
+
34
+ - Modify `src/progress.ts` only to make state cleanup mode-independent.
35
+ - Modify the two SSH label assertions in `__tests__/app.test.ts`.
36
+ - Do not change progress APIs, spinner copy, output formatting, or command flow.
37
+
38
+ ## Verification
39
+
40
+ Use the two CI failures as the red phase, then run the targeted progress tests,
41
+ the full test suite, lint, and build. Verification must use a supported Node 24
42
+ runtime because the current host Node 26 runtime is incompatible with one of
43
+ the installed CommonJS test dependencies.
@@ -0,0 +1,63 @@
1
+ # Node SSH-Agent Proxy for Commit Signing
2
+
3
+ ## Goal
4
+
5
+ Make Boxdown's default SSH commit signing usable by the container's `node`
6
+ user when Docker Desktop mounts the host agent socket as `root:root` with mode
7
+ `0660`. Signing must remain best-effort and never expose a private key.
8
+
9
+ ## Root Cause
10
+
11
+ The host preflight and the raw socket mount both succeed, but lifecycle hooks
12
+ run as `node`. In the reproduced environment, `ssh-add -L` succeeds as root
13
+ and fails as `node` with `Permission denied`. The signing bootstrap therefore
14
+ sets `commit.gpgsign=false` and reports `container-agent-unavailable`.
15
+
16
+ ## Architecture
17
+
18
+ Boxdown continues to bind-mount the host-facing agent socket at
19
+ `/run/boxdown/ssh-agent.sock`. A small Node Unix-socket proxy runs as root and
20
+ forwards each connection from that raw socket to
21
+ `/run/boxdown/ssh-agent-node.sock`. The proxy creates the latter socket with
22
+ ownership and permissions suitable for `node`. Generated configuration sets
23
+ `SSH_AUTH_SOCK` to the node-facing socket and exposes the raw path only through
24
+ `BOXDOWN_GIT_SIGNING_SOURCE_SOCKET` for the proxy.
25
+
26
+ The post-create hook starts the proxy with passwordless `sudo`, waits briefly
27
+ for its socket, verifies `ssh-add -L` as `node`, then invokes the existing
28
+ signing bootstrap. A proxy failure is non-blocking and has its own stable
29
+ reason code.
30
+
31
+ ## Git Configuration Precedence
32
+
33
+ **Decision:** Boxdown does not make its SSH configuration authoritative over
34
+ explicit user Git signing preferences. This is intentional because the
35
+ container begins from a writable copy of the user's Git configuration.
36
+
37
+ - With no explicit global or repository-local signing preference, Boxdown
38
+ configures SSH signing as the default.
39
+ - With an explicit SSH preference, Boxdown preserves the selected identity;
40
+ it only translates an inaccessible host public-key path to the mounted
41
+ public-key snapshot in the container.
42
+ - With `commit.gpgsign=false`, a non-SSH `gpg.format`, or an explicit
43
+ `gpg.program`, Boxdown preserves that preference and does not replace it.
44
+
45
+ Repository-local settings retain Git's normal precedence. In particular, a
46
+ local `commit.gpgsign=false` keeps commits unsigned even when agent forwarding
47
+ is healthy. A future product decision may deliberately reverse this policy and
48
+ make Boxdown authoritative; that would be a behavior change, not a bug fix.
49
+
50
+ ## Failure Handling
51
+
52
+ The proxy never reads, copies, or logs private-key material. It reports
53
+ `container-agent-proxy-unavailable` when `sudo`, Node, proxy readiness, or the
54
+ node-user agent probe fails. The signing bootstrap remains non-blocking and
55
+ preserves explicit user configuration rather than setting a contradictory
56
+ global value.
57
+
58
+ ## Validation
59
+
60
+ Tests cover forwarding a real Unix-socket request through the proxy, a proxy
61
+ startup failure, generated socket environment values, successful default SSH
62
+ signing, and preservation of global and local user preferences. Documentation
63
+ will require `--recreate` after upgrade.