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
package/src/doctor.ts CHANGED
@@ -1,7 +1,15 @@
1
- import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
2
- import { join } from 'node:path'
3
-
1
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import {
5
+ probeContainerRuntime,
6
+ type ContainerRuntimeCommandResult,
7
+ type ContainerRuntimeCommandRunner
8
+ } from './container-runtime.ts'
4
9
  import { resolveDevcontainerCli } from './devcontainer-cli.ts'
10
+ import { buildGeneratedDevcontainerConfig, type DevcontainerConfig } from './config.ts'
11
+ import { BOXDOWN_SECRET_ENV_NAMES } from './constants.ts'
12
+ import { resolveConfiguredSshSigningKey, selectGitSigningKey, type GitSigningReason } from './git-signing.ts'
5
13
  import type { WorkspaceContext } from './paths.ts'
6
14
  import { runBuffered } from './process.ts'
7
15
 
@@ -13,17 +21,13 @@ export interface DoctorCheck {
13
21
  message: string
14
22
  }
15
23
 
16
- export interface DoctorCommandResult {
17
- code: number
18
- stdout: string
19
- stderr: string
20
- }
21
-
22
- export type DoctorCommandRunner = (command: string, args: string[]) => Promise<DoctorCommandResult>
24
+ export type DoctorCommandResult = ContainerRuntimeCommandResult
25
+ export type DoctorCommandRunner = ContainerRuntimeCommandRunner
23
26
 
24
27
  export interface RunDoctorChecksOptions {
25
28
  includeOptional?: boolean
26
29
  includeDockerMountProbe?: boolean
30
+ containerRuntimeReady?: boolean
27
31
  runCommand?: DoctorCommandRunner
28
32
  }
29
33
 
@@ -40,6 +44,36 @@ function check (name: string, pass: boolean, okMessage: string, failMessage: str
40
44
  }
41
45
  }
42
46
 
47
+ function secretEnvironmentConfigCheck (context: WorkspaceContext): DoctorCheck {
48
+ let config: DevcontainerConfig
49
+
50
+ try {
51
+ config = existsSync(context.generatedConfigPath)
52
+ ? JSON.parse(readFileSync(context.generatedConfigPath, 'utf8')) as DevcontainerConfig
53
+ : buildGeneratedDevcontainerConfig(context)
54
+ } catch {
55
+ return {
56
+ name: 'secret-environment-config',
57
+ level: 'warn',
58
+ message: 'Generated config could not be checked for secret-safe environment handling'
59
+ }
60
+ }
61
+
62
+ const runArgs = Array.isArray(config.runArgs) ? config.runArgs : []
63
+ const containerEnv = config.containerEnv ?? {}
64
+ const unsafe = runArgs.includes('--env-file') ||
65
+ runArgs.some((arg) => arg.includes('.env.development')) ||
66
+ BOXDOWN_SECRET_ENV_NAMES.some((name) => Object.hasOwn(containerEnv, name))
67
+
68
+ return {
69
+ name: 'secret-environment-config',
70
+ level: unsafe ? 'warn' : 'ok',
71
+ message: unsafe
72
+ ? 'Generated config still exposes Boxdown secrets through Docker environment settings; recreate after upgrading Boxdown'
73
+ : 'Generated config uses runtime-mounted secrets without Docker environment values'
74
+ }
75
+ }
76
+
43
77
  async function runDoctorCommand (command: string, args: string[]): Promise<DoctorCommandResult> {
44
78
  return runBuffered(command, args, {
45
79
  mirrorStdout: false,
@@ -70,19 +104,77 @@ export async function runDoctorChecks (context: WorkspaceContext, options: RunDo
70
104
  ))
71
105
 
72
106
  const sshAgent = await runCommand('ssh-add', ['-L'])
73
- const identities = sshAgent.code === 0
74
- ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith('ssh-')).length
75
- : 0
107
+ const identityLines = sshAgent.code === 0
108
+ ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith('ssh-'))
109
+ : []
110
+ const identities = identityLines.length
111
+ let configuredKey: string | undefined
112
+ let configuredFailure: { reason: GitSigningReason, detail?: string } | undefined
113
+ const format = await runCommand('git', ['config', '--global', '--get', 'gpg.format'])
114
+ if (format.code === 0 && format.stdout.trim() === 'ssh') {
115
+ const signingKey = await runCommand('git', ['config', '--global', '--get', 'user.signingkey'])
116
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
117
+ const resolved = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
118
+ homeDir: dirname(context.hostGitconfigPath),
119
+ workspaceFolder: context.workspaceFolder
120
+ })
121
+ if (resolved.key === undefined) {
122
+ configuredFailure = {
123
+ reason: resolved.reason ?? 'configured-key-invalid',
124
+ detail: resolved.detail
125
+ }
126
+ } else {
127
+ configuredKey = resolved.key
128
+ }
129
+ }
130
+ }
131
+
132
+ const includeOptional = options.includeOptional ?? true
133
+ let ghAvailable = false
134
+ let ghAuth = false
135
+ let githubLogin: string | undefined
136
+ let githubAuthKeys: string[] | undefined
137
+ if (includeOptional) {
138
+ ghAvailable = await commandWorks(runCommand, 'gh', ['--version'])
139
+ if (ghAvailable) {
140
+ ghAuth = await commandWorks(runCommand, 'gh', ['auth', 'status', '--hostname', 'github.com'])
141
+ if (ghAuth && configuredKey === undefined && configuredFailure === undefined && identities > 1) {
142
+ const user = await runCommand('gh', ['api', 'user', '--jq', '.login'])
143
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined
144
+ if (githubLogin !== undefined) {
145
+ const authentication = await runCommand('gh', ['api', `users/${githubLogin}/keys`, '--paginate', '--jq', '.[].key'])
146
+ if (authentication.code === 0) githubAuthKeys = authentication.stdout.split(/\r?\n/)
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ const selected: { key?: string, reason?: GitSigningReason } = configuredFailure ?? selectGitSigningKey(identityLines, configuredKey, githubAuthKeys)
153
+ const selectedByConfiguration = selected.key !== undefined && configuredKey !== undefined
154
+ const selectedByGithub = selected.key !== undefined && configuredKey === undefined && identities > 1
155
+ const signingMessages: Record<GitSigningReason, string> = {
156
+ 'agent-unavailable': 'SSH agent is unavailable; Boxdown commits will remain unsigned',
157
+ 'no-identities': 'SSH agent has no identities; Boxdown commits will remain unsigned',
158
+ 'ambiguous-identities': 'SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned',
159
+ 'configured-key-unreadable': 'Configured SSH signing-key file could not be read; Boxdown commits will remain unsigned',
160
+ 'configured-key-invalid': 'Configured SSH signing key is not a valid public key; Boxdown commits will remain unsigned',
161
+ 'configured-key-not-loaded': 'Configured SSH signing key is not loaded in the agent; Boxdown commits will remain unsigned',
162
+ 'agent-socket-unavailable': 'Host SSH-agent socket is unavailable; Boxdown commits will remain unsigned',
163
+ 'docker-probe-image-unavailable': 'No local Docker image is available to probe commit-signing agent forwarding',
164
+ 'agent-mount-unavailable': 'Docker could not mount the host SSH-agent socket; Boxdown commits will remain unsigned'
165
+ }
76
166
  checks.push({
77
167
  name: 'git-signing-agent',
78
- level: sshAgent.code === 0 && identities === 1 ? 'ok' : 'warn',
168
+ level: sshAgent.code === 0 && selected.key !== undefined ? 'ok' : 'warn',
79
169
  message: sshAgent.code !== 0
80
- ? 'SSH agent is unavailable; Boxdown commits will remain unsigned'
81
- : identities === 0
82
- ? 'SSH agent has no identities; Boxdown commits will remain unsigned'
83
- : identities > 1
84
- ? 'SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned'
85
- : 'One SSH agent identity is available for Boxdown commit signing'
170
+ ? signingMessages['agent-unavailable']
171
+ : selected.key === undefined
172
+ ? signingMessages[selected.reason ?? 'ambiguous-identities']
173
+ : selectedByConfiguration
174
+ ? 'Configured SSH signing key is loaded in the agent'
175
+ : selectedByGithub
176
+ ? 'GitHub authentication keys identify one SSH agent identity for Boxdown commit signing'
177
+ : 'One SSH agent identity is available for Boxdown commit signing'
86
178
  })
87
179
 
88
180
  checks.push(check(
@@ -92,21 +184,46 @@ export async function runDoctorChecks (context: WorkspaceContext, options: RunDo
92
184
  'Packaged @devcontainers/cli is required but was not available'
93
185
  ))
94
186
 
95
- const dockerCliWorks = await commandWorks(runCommand, 'docker', ['--version'])
96
- checks.push(check(
97
- 'docker-cli',
98
- dockerCliWorks,
99
- 'Docker CLI is available',
100
- 'Docker CLI is required but was not available'
101
- ))
102
-
103
- const dockerDaemonWorks = await commandWorks(runCommand, 'docker', ['info'])
104
- checks.push(check(
105
- 'docker-daemon',
106
- dockerDaemonWorks,
107
- 'Docker daemon is reachable',
108
- 'Docker daemon is required but was not reachable'
109
- ))
187
+ let dockerCliWorks = options.containerRuntimeReady === true
188
+ let dockerDaemonWorks = options.containerRuntimeReady === true
189
+
190
+ if (options.containerRuntimeReady !== true) {
191
+ const runtime = await probeContainerRuntime(runCommand)
192
+ dockerCliWorks = runtime.state === 'ready' || runtime.failure.reason !== 'docker-cli-unavailable'
193
+ dockerDaemonWorks = runtime.state === 'ready' ||
194
+ (runtime.state === 'waiting' && runtime.failure.reason === 'buildx-builder-unavailable')
195
+
196
+ checks.push(check(
197
+ 'docker-cli',
198
+ dockerCliWorks,
199
+ 'Docker CLI is available',
200
+ 'Docker CLI is required but was not available'
201
+ ))
202
+ checks.push(check(
203
+ 'docker-daemon',
204
+ dockerDaemonWorks,
205
+ 'Docker daemon is reachable',
206
+ 'Docker daemon is required but was not reachable'
207
+ ))
208
+
209
+ if (!dockerCliWorks || !dockerDaemonWorks) {
210
+ checks.push({
211
+ name: 'docker-buildx',
212
+ level: 'warn',
213
+ message: 'Docker Buildx was not checked because the Docker runtime is unavailable'
214
+ })
215
+ } else if (runtime.state === 'ready' && runtime.mode === 'fallback') {
216
+ checks.push({ name: 'docker-buildx', level: 'warn', message: runtime.warnings[0] as string })
217
+ } else if (runtime.state === 'waiting') {
218
+ checks.push({
219
+ name: 'docker-buildx',
220
+ level: 'fail',
221
+ message: `Docker Buildx builder was not operational: ${runtime.failure.detail}`
222
+ })
223
+ } else {
224
+ checks.push({ name: 'docker-buildx', level: 'ok', message: 'Docker Buildx builder is operational' })
225
+ }
226
+ }
110
227
 
111
228
  checks.push(check(
112
229
  'ssh',
@@ -133,26 +250,29 @@ export async function runDoctorChecks (context: WorkspaceContext, options: RunDo
133
250
  `Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`
134
251
  ))
135
252
 
136
- if (options.includeOptional ?? true) {
137
- if (await commandWorks(runCommand, 'gh', ['--version'])) {
138
- const ghAuth = await commandWorks(runCommand, 'gh', ['auth', 'status', '--hostname', 'github.com'])
253
+ checks.push(secretEnvironmentConfigCheck(context))
254
+
255
+ if (includeOptional) {
256
+ if (ghAvailable) {
139
257
  checks.push({
140
258
  name: 'gh-auth',
141
259
  level: ghAuth ? 'ok' : 'warn',
142
260
  message: ghAuth ? 'GitHub CLI auth is available' : 'GitHub CLI is available but not authenticated'
143
261
  })
144
- if (ghAuth && identities === 1) {
145
- const user = await runCommand('gh', ['api', 'user', '--jq', '.login'])
146
- const signing = user.code === 0 && user.stdout.trim().length > 0
147
- ? await runCommand('gh', ['api', `users/${user.stdout.trim()}/ssh_signing_keys`, '--paginate', '--jq', '.[].key'])
262
+ if (ghAuth && selected.key !== undefined) {
263
+ if (githubLogin === undefined) {
264
+ const user = await runCommand('gh', ['api', 'user', '--jq', '.login'])
265
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined
266
+ }
267
+ const signing = githubLogin !== undefined
268
+ ? await runCommand('gh', ['api', `users/${githubLogin}/ssh_signing_keys`, '--paginate', '--jq', '.[].key'])
148
269
  : { code: 1, stdout: '', stderr: '' }
149
- const identity = sshAgent.stdout.split(/\r?\n/).find((line) => line.trim().startsWith('ssh-'))?.trim()
150
270
  checks.push({
151
271
  name: 'git-signing-github',
152
- level: signing.code === 0 && identity !== undefined && signing.stdout.includes(identity.split(/\s+/, 3).slice(0, 2).join(' ')) ? 'ok' : 'warn',
272
+ level: signing.code === 0 && signing.stdout.includes(selected.key) ? 'ok' : 'warn',
153
273
  message: signing.code !== 0
154
274
  ? 'GitHub SSH signing-key registration could not be checked'
155
- : signing.stdout.includes(identity?.split(/\s+/, 3).slice(0, 2).join(' ') ?? '')
275
+ : signing.stdout.includes(selected.key)
156
276
  ? 'Selected SSH key is registered with GitHub for commit signing'
157
277
  : 'Register the selected public key with GitHub as a signing key to receive Verified badges'
158
278
  })
@@ -215,10 +335,13 @@ async function checkDockerBindMounts (
215
335
 
216
336
  mkdirSync(context.workspaceDataDir, { recursive: true })
217
337
  const runtimeProbeDir = mkdtempSync(join(context.workspaceDataDir, 'doctor-mount-probe-'))
338
+ mkdirSync(context.workspaceSecretEnvDir, { recursive: true, mode: 0o700 })
339
+ chmodSync(context.workspaceSecretEnvDir, 0o700)
218
340
  const sources: DockerMountSource[] = [
219
341
  { label: 'workspace', path: context.workspaceFolder },
220
342
  { label: 'Boxdown devcontainer assets', path: context.assetsDevcontainerDir },
221
- { label: 'Boxdown runtime state', path: runtimeProbeDir }
343
+ { label: 'Boxdown runtime state', path: runtimeProbeDir },
344
+ { label: 'Boxdown runtime secret state', path: context.workspaceSecretEnvDir }
222
345
  ]
223
346
 
224
347
  try {
@@ -1,18 +1,64 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs'
2
- import { join } from 'node:path'
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { isAbsolute, join, resolve } from 'node:path'
3
3
 
4
+ import type { WorkspaceCommandLogger } from './logging.ts'
4
5
  import type { WorkspaceContext } from './paths.ts'
5
- import { runBuffered } from './process.ts'
6
+ import { runBuffered, type CommandResult } from './process.ts'
6
7
 
7
- export type GitSigningReason = 'agent-unavailable' | 'no-identities' | 'ambiguous-identities' | 'configured-key-not-loaded' | 'agent-mount-unavailable'
8
+ export type GitSigningReason = 'agent-unavailable' | 'no-identities' | 'ambiguous-identities' | 'configured-key-unreadable' | 'configured-key-invalid' | 'configured-key-not-loaded' | 'agent-socket-unavailable' | 'docker-probe-image-unavailable' | 'agent-mount-unavailable'
8
9
 
9
10
  export interface GitSigningPlan {
10
11
  enabled: boolean
11
12
  reason?: GitSigningReason
13
+ detail?: string
12
14
  publicKey?: string
13
15
  agentSocketSource?: string
14
16
  }
15
17
 
18
+ const GIT_SIGNING_REASON_MESSAGES: Record<GitSigningReason, string> = {
19
+ 'agent-unavailable': 'the host SSH agent is unavailable',
20
+ 'no-identities': 'the host SSH agent has no loaded identities',
21
+ 'ambiguous-identities': 'multiple SSH identities are loaded and no signing key could be selected safely',
22
+ 'configured-key-unreadable': 'the configured SSH signing-key file could not be read',
23
+ 'configured-key-invalid': 'the configured SSH signing key is not a valid public key',
24
+ 'configured-key-not-loaded': 'the configured SSH signing key is not loaded in the agent',
25
+ 'agent-socket-unavailable': 'the host SSH-agent socket is unavailable',
26
+ 'docker-probe-image-unavailable': 'no local Docker image is available to probe the SSH-agent mount',
27
+ 'agent-mount-unavailable': 'Docker could not mount the host SSH-agent socket'
28
+ }
29
+
30
+ function compactDiagnosticDetail (detail: string): string {
31
+ return detail
32
+ .trim()
33
+ .replace(/\s+/gu, ' ')
34
+ .slice(0, 2000)
35
+ .replace(/-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)/giu, '[redacted-private-key]')
36
+ .replace(/\b(?:ssh-[A-Za-z0-9-]+|ecdsa-sha2-[A-Za-z0-9-]+|sk-(?:ssh-[A-Za-z0-9-]+|ecdsa-sha2-[A-Za-z0-9-]+))\s+[A-Za-z0-9+/=]{16,}/gu, '[redacted-ssh-key]')
37
+ .replace(/\b(?:github_pat_|gh[pousr]_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{8,}/gu, '[redacted-token]')
38
+ .replace(/\b(?:Bearer\s+|token[=:]\s*)[A-Za-z0-9._~+/-]{8,}/giu, '[redacted-token]')
39
+ .slice(0, 300)
40
+ }
41
+
42
+ export function reportGitSigningPlan (
43
+ plan: GitSigningPlan,
44
+ options: {
45
+ logger?: Pick<WorkspaceCommandLogger, 'boxdown'>
46
+ quiet?: boolean
47
+ writeWarning?: (message: string) => void
48
+ } = {}
49
+ ): void {
50
+ if (plan.enabled) return
51
+
52
+ const reason = plan.reason ?? 'agent-unavailable'
53
+ const detail = plan.detail === undefined ? '' : ` detail=${compactDiagnosticDetail(plan.detail)}`
54
+ options.logger?.boxdown(`git-signing: enabled=false reason=${reason}${detail}\n`)
55
+
56
+ if (options.quiet !== true) {
57
+ const writeWarning = options.writeWarning ?? ((message: string) => process.stderr.write(message))
58
+ writeWarning(`boxdown: commit signing disabled: ${GIT_SIGNING_REASON_MESSAGES[reason]}; commits will remain unsigned.\n`)
59
+ }
60
+ }
61
+
16
62
  export function parseSshPublicKey (value: string): string | undefined {
17
63
  const [algorithm, key] = value.trim().split(/\s+/, 3)
18
64
  if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
@@ -22,11 +68,68 @@ export function parseSshPublicKey (value: string): string | undefined {
22
68
  return `${algorithm} ${key}`
23
69
  }
24
70
 
71
+ export interface ConfiguredSshSigningKeyResult {
72
+ key?: string
73
+ reason?: Extract<GitSigningReason, 'configured-key-unreadable' | 'configured-key-invalid'>
74
+ detail?: string
75
+ }
76
+
77
+ export function resolveConfiguredSshSigningKey (
78
+ value: string,
79
+ options: { homeDir?: string, workspaceFolder: string }
80
+ ): ConfiguredSshSigningKeyResult {
81
+ const inlineValue = value.startsWith('key::') ? value.slice('key::'.length) : value
82
+ const inlineKey = parseSshPublicKey(inlineValue)
83
+ if (inlineKey !== undefined) {
84
+ return { key: inlineKey }
85
+ }
86
+ if (value.startsWith('key::')) {
87
+ return {
88
+ reason: 'configured-key-invalid',
89
+ detail: 'configured inline value is not a valid SSH public key'
90
+ }
91
+ }
92
+
93
+ let keyPath: string
94
+ if (value.startsWith('~/')) {
95
+ if (options.homeDir === undefined) {
96
+ return {
97
+ reason: 'configured-key-unreadable',
98
+ detail: 'configured public-key file could not be read'
99
+ }
100
+ }
101
+ keyPath = join(options.homeDir, value.slice(2))
102
+ } else {
103
+ keyPath = isAbsolute(value) ? value : resolve(options.workspaceFolder, value)
104
+ }
105
+
106
+ let publicKeyText: string
107
+ try {
108
+ publicKeyText = readFileSync(keyPath, 'utf8')
109
+ } catch {
110
+ return {
111
+ reason: 'configured-key-unreadable',
112
+ detail: 'configured public-key file could not be read'
113
+ }
114
+ }
115
+
116
+ const publicKey = parseSshPublicKey(publicKeyText.split(/\r?\n/u).find((line) => line.trim().length > 0) ?? '')
117
+ return publicKey === undefined
118
+ ? {
119
+ reason: 'configured-key-invalid',
120
+ detail: 'configured public-key file does not contain a valid SSH public key'
121
+ }
122
+ : { key: publicKey }
123
+ }
124
+
25
125
  export function selectGitSigningKey (identities: string[], configuredKey?: string, githubKeys?: string[]): { key?: string, reason?: GitSigningReason } {
26
126
  const keys = identities.map(parseSshPublicKey).filter((key): key is string => key !== undefined)
27
127
  if (keys.length === 0) return { reason: 'no-identities' }
28
128
 
29
129
  const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey)
130
+ if (configuredKey !== undefined && configured === undefined) {
131
+ return { reason: 'configured-key-invalid' }
132
+ }
30
133
  if (configured !== undefined) {
31
134
  return keys.includes(configured) ? { key: configured } : { reason: 'configured-key-not-loaded' }
32
135
  }
@@ -43,44 +146,121 @@ export function writeGitSigningPublicKey (context: WorkspaceContext, key: string
43
146
  writeFileSync(join(context.gitSigningStateDir, 'signing-key.pub'), `${parseSshPublicKey(key) ?? key}\n`, { mode: 0o644 })
44
147
  }
45
148
 
46
- async function dockerCanMountAgent (source: string): Promise<boolean> {
47
- const images = await runBuffered('docker', ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}'], { mirrorStdout: false, mirrorStderr: false })
149
+ type GitSigningCommandRunner = (command: string, args: string[]) => Promise<CommandResult>
150
+
151
+ interface ResolveGitSigningPlanOptions {
152
+ env?: NodeJS.ProcessEnv
153
+ platform?: NodeJS.Platform
154
+ runCommand?: GitSigningCommandRunner
155
+ }
156
+
157
+ type AgentMountProbeResult =
158
+ | { ok: true }
159
+ | { ok: false, reason: Extract<GitSigningReason, 'docker-probe-image-unavailable' | 'agent-mount-unavailable'>, detail: string }
160
+
161
+ async function runGitSigningCommand (command: string, args: string[]): Promise<CommandResult> {
162
+ return runBuffered(command, args, { mirrorStdout: false, mirrorStderr: false })
163
+ }
164
+
165
+ function failedCommandDetail (label: string, result: CommandResult): string {
166
+ const stderr = compactDiagnosticDetail(result.stderr)
167
+ return stderr.length === 0
168
+ ? `${label} failed with exit code ${result.code}`
169
+ : `${label} failed with exit code ${result.code}: ${stderr}`
170
+ }
171
+
172
+ async function probeDockerAgentMount (source: string, runCommand: GitSigningCommandRunner): Promise<AgentMountProbeResult> {
173
+ const images = await runCommand('docker', ['image', 'ls', '--format', '{{.Repository}}:{{.Tag}}'])
48
174
  const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== '<none>:<none>')
49
- if (images.code !== 0 || image === undefined) return false
175
+ if (images.code !== 0) {
176
+ return {
177
+ ok: false,
178
+ reason: 'agent-mount-unavailable',
179
+ detail: failedCommandDetail('Docker image listing', images)
180
+ }
181
+ }
182
+ if (image === undefined) {
183
+ return {
184
+ ok: false,
185
+ reason: 'docker-probe-image-unavailable',
186
+ detail: 'no tagged local Docker image was found'
187
+ }
188
+ }
50
189
 
51
- const created = await runBuffered('docker', ['create', '--pull=never', '--entrypoint', '/bin/true', '--mount', `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`, image], { mirrorStdout: false, mirrorStderr: false })
190
+ const created = await runCommand('docker', ['create', '--pull=never', '--entrypoint', '/bin/true', '--mount', `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`, image])
52
191
  const containerId = created.stdout.trim().split(/\r?\n/)[0]
53
- if (created.code !== 0 || containerId === undefined || containerId.length === 0) return false
54
- await runBuffered('docker', ['rm', '-f', containerId], { mirrorStdout: false, mirrorStderr: false })
55
- return true
192
+ if (created.code !== 0 || containerId === undefined || containerId.length === 0) {
193
+ return {
194
+ ok: false,
195
+ reason: 'agent-mount-unavailable',
196
+ detail: failedCommandDetail('Docker SSH-agent mount probe', created)
197
+ }
198
+ }
199
+ await runCommand('docker', ['rm', '-f', containerId])
200
+ return { ok: true }
56
201
  }
57
202
 
58
- export async function resolveGitSigningPlan (context: WorkspaceContext): Promise<GitSigningPlan> {
59
- const result = await runBuffered('ssh-add', ['-L'], { mirrorStdout: false, mirrorStderr: false })
60
- if (result.code !== 0) return { enabled: false, reason: 'agent-unavailable' }
203
+ export async function resolveGitSigningPlan (context: WorkspaceContext, options: ResolveGitSigningPlanOptions = {}): Promise<GitSigningPlan> {
204
+ const runCommand = options.runCommand ?? runGitSigningCommand
205
+ const result = await runCommand('ssh-add', ['-L'])
206
+ if (result.code !== 0) {
207
+ return {
208
+ enabled: false,
209
+ reason: 'agent-unavailable',
210
+ detail: failedCommandDetail('ssh-add -L', result)
211
+ }
212
+ }
61
213
 
62
214
  const identities = result.stdout.split(/\r?\n/)
63
- const format = await runBuffered('git', ['config', '--global', '--get', 'gpg.format'], { mirrorStdout: false, mirrorStderr: false })
64
- const configuredKey = format.code === 0 && format.stdout.trim() === 'ssh'
65
- ? (await runBuffered('git', ['config', '--global', '--get', 'user.signingkey'], { mirrorStdout: false, mirrorStderr: false })).stdout.trim()
66
- : undefined
215
+ const format = await runCommand('git', ['config', '--global', '--get', 'gpg.format'])
216
+ let configuredKey: string | undefined
217
+ if (format.code === 0 && format.stdout.trim() === 'ssh') {
218
+ const signingKey = await runCommand('git', ['config', '--global', '--get', 'user.signingkey'])
219
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
220
+ const resolvedKey = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
221
+ homeDir: options.env?.HOME ?? process.env.HOME,
222
+ workspaceFolder: context.workspaceFolder
223
+ })
224
+ if (resolvedKey.key === undefined) {
225
+ return {
226
+ enabled: false,
227
+ reason: resolvedKey.reason ?? 'configured-key-invalid',
228
+ detail: resolvedKey.detail
229
+ }
230
+ }
231
+ configuredKey = resolvedKey.key
232
+ }
233
+ }
67
234
  let githubKeys: string[] | undefined
68
- if (identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
69
- const user = await runBuffered('gh', ['api', 'user', '--jq', '.login'], { mirrorStdout: false, mirrorStderr: false })
235
+ if (configuredKey === undefined && identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
236
+ const user = await runCommand('gh', ['api', 'user', '--jq', '.login'])
70
237
  const login = user.stdout.trim()
71
238
  if (user.code === 0 && login.length > 0) {
72
- const github = await runBuffered('gh', ['api', `users/${login}/keys`, '--paginate', '--jq', '.[].key'], { mirrorStdout: false, mirrorStderr: false })
239
+ const github = await runCommand('gh', ['api', `users/${login}/keys`, '--paginate', '--jq', '.[].key'])
73
240
  if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/)
74
241
  }
75
242
  }
76
243
  const selected = selectGitSigningKey(identities, configuredKey, githubKeys)
77
244
  if (selected.key === undefined) return { enabled: false, reason: selected.reason }
78
245
 
79
- const agentSocketSource = process.platform === 'darwin'
246
+ const agentSocketSource = (options.platform ?? process.platform) === 'darwin'
80
247
  ? '/run/host-services/ssh-auth.sock'
81
- : process.env.SSH_AUTH_SOCK
82
- if (agentSocketSource === undefined || agentSocketSource.length === 0) return { enabled: false, reason: 'agent-mount-unavailable' }
83
- if (!await dockerCanMountAgent(agentSocketSource)) return { enabled: false, reason: 'agent-mount-unavailable' }
248
+ : options.env?.SSH_AUTH_SOCK ?? process.env.SSH_AUTH_SOCK
249
+ if (agentSocketSource === undefined || agentSocketSource.length === 0) {
250
+ return {
251
+ enabled: false,
252
+ reason: 'agent-socket-unavailable',
253
+ detail: 'SSH_AUTH_SOCK is not set'
254
+ }
255
+ }
256
+ const mountProbe = await probeDockerAgentMount(agentSocketSource, runCommand)
257
+ if (!mountProbe.ok) {
258
+ return {
259
+ enabled: false,
260
+ reason: mountProbe.reason,
261
+ detail: mountProbe.detail
262
+ }
263
+ }
84
264
 
85
265
  writeGitSigningPublicKey(context, selected.key)
86
266
  return { enabled: true, publicKey: selected.key, agentSocketSource }
package/src/logging.ts CHANGED
@@ -24,6 +24,13 @@ function argvText (command: string, args: string[]): string {
24
24
  return JSON.stringify([command, ...args])
25
25
  }
26
26
 
27
+ export function redactKnownSecretEnvironmentAssignments (value: string): string {
28
+ return value
29
+ .replace(/ANTHROPIC_API_KEY=[^\s,"\]}]+/gu, 'ANTHROPIC_API_KEY=[redacted]')
30
+ .replace(/SNYK_TOKEN=[^\s,"\]}]+/gu, 'SNYK_TOKEN=[redacted]')
31
+ .replace(/OP_SERVICE_ACCOUNT_TOKEN=[^\s,"\]}]+/gu, 'OP_SERVICE_ACCOUNT_TOKEN=[redacted]')
32
+ }
33
+
27
34
  export class WorkspaceCommandLogger {
28
35
  readonly logPath: string
29
36
  readonly workspaceFolder: string
@@ -89,7 +96,10 @@ export class WorkspaceCommandLogger {
89
96
  }
90
97
 
91
98
  #redact (value: string): string {
92
- return this.#redactions.reduce((current, redaction) => current.replaceAll(redaction, '[redacted]'), value)
99
+ return this.#redactions.reduce(
100
+ (current, redaction) => current.replaceAll(redaction, '[redacted]'),
101
+ redactKnownSecretEnvironmentAssignments(value)
102
+ )
93
103
  }
94
104
 
95
105
  #stream (stream: LogStreamName, chunk: Buffer | string): void {