boxdown 1.2.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 (42) hide show
  1. package/assets/devcontainer/README.md +17 -5
  2. package/assets/devcontainer/devcontainer.json +3 -8
  3. package/assets/devcontainer/hooks/initialize.sh +49 -27
  4. package/assets/devcontainer/hooks/post-create.sh +11 -0
  5. package/assets/devcontainer/hooks/post-start.sh +0 -10
  6. package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
  7. package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
  8. package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
  9. package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
  10. package/dist/bin/cli.cjs +1 -1
  11. package/dist/bin/cli.mjs +1 -1
  12. package/dist/{main-Df4E8ARj.cjs → main-BDgyf2t5.cjs} +459 -199
  13. package/dist/{main-XMBsKjIK.mjs → main-J4_2Up3o.mjs} +461 -201
  14. package/dist/main-J4_2Up3o.mjs.map +1 -0
  15. package/dist/main.cjs +1 -1
  16. package/dist/main.d.cts +6 -0
  17. package/dist/main.d.cts.map +1 -1
  18. package/dist/main.d.mts +6 -0
  19. package/dist/main.d.mts.map +1 -1
  20. package/dist/main.mjs +1 -1
  21. package/docs/features/commit-signing.md +71 -0
  22. package/docs/features/generated-config-and-state.md +19 -4
  23. package/docs/features/github-auth-refresh.md +3 -2
  24. package/docs/features/lifecycle.md +6 -0
  25. package/docs/features/start-and-shell.md +5 -0
  26. package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
  27. package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
  28. package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
  29. package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
  30. package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
  31. package/package.json +1 -1
  32. package/src/config.ts +14 -2
  33. package/src/constants.ts +7 -0
  34. package/src/devcontainer.ts +37 -26
  35. package/src/doctor.ts +120 -23
  36. package/src/git-signing.ts +205 -25
  37. package/src/logging.ts +11 -1
  38. package/src/main.ts +2 -1
  39. package/src/paths.ts +21 -1
  40. package/src/purge.ts +8 -0
  41. package/src/shell.ts +6 -0
  42. package/dist/main-XMBsKjIK.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- import { homedir } from "node:os";
4
+ import { homedir, tmpdir } from "node:os";
5
5
  import { basename, delimiter, dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
6
6
  import { spawn } from "node:child_process";
7
7
  import { createInterface } from "node:readline";
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
10
10
  //#region src/claude-app-config.ts
11
11
  const CLAUDE_APP_SUPPORT_DIRNAME = "Claude";
12
12
  const CLAUDE_SSH_CONFIGS_FILENAME = "ssh_configs.json";
13
- function isRecord$2(value) {
13
+ function isRecord$1(value) {
14
14
  return value !== null && typeof value === "object" && !Array.isArray(value);
15
15
  }
16
16
  function parseRequiredString(value, key) {
@@ -43,7 +43,7 @@ function parseStringArray(value, key) {
43
43
  });
44
44
  }
45
45
  function parseClaudeSshConfigEntry(value) {
46
- if (!isRecord$2(value)) {
46
+ if (!isRecord$1(value)) {
47
47
  throw new Error("Invalid Claude SSH config: configs entries must be objects");
48
48
  }
49
49
  const source = parseOptionalString(value.source, "source");
@@ -61,7 +61,7 @@ function parseClaudeSshConfigEntry(value) {
61
61
  return parsed;
62
62
  }
63
63
  function parseClaudeSshConfigs(value) {
64
- if (!isRecord$2(value)) {
64
+ if (!isRecord$1(value)) {
65
65
  throw new Error("Invalid Claude SSH config: top-level value must be an object");
66
66
  }
67
67
  const configs = value.configs;
@@ -217,7 +217,7 @@ const CODEX_APP_CONFIG_VERSION = 1;
217
217
  const CODEX_APP_CONFIG_DIRNAME = "codex-app";
218
218
  const CODEX_APP_CONFIG_FILENAME = "config.json";
219
219
  const CODEX_GLOBAL_STATE_FILENAME = ".codex-global-state.json";
220
- function isRecord$1(value) {
220
+ function isRecord(value) {
221
221
  return value !== null && typeof value === "object" && !Array.isArray(value);
222
222
  }
223
223
  function parseOptionalNonnegativeInteger(value, key) {
@@ -239,7 +239,7 @@ function parseOptionalLabel(value) {
239
239
  return value;
240
240
  }
241
241
  function parseProjectConfig(value) {
242
- if (!isRecord$1(value)) {
242
+ if (!isRecord(value)) {
243
243
  throw new Error("Invalid Codex app config: project entries must be objects");
244
244
  }
245
245
  const remotePath = value.remotePath;
@@ -253,7 +253,7 @@ function parseProjectConfig(value) {
253
253
  };
254
254
  }
255
255
  function parseRemoteConnectionConfig(value) {
256
- if (!isRecord$1(value)) {
256
+ if (!isRecord(value)) {
257
257
  throw new Error("Invalid Codex app config: remoteConnections entries must be objects");
258
258
  }
259
259
  const sshAlias = value.sshAlias;
@@ -270,7 +270,7 @@ function parseRemoteConnectionConfig(value) {
270
270
  };
271
271
  }
272
272
  function parseCodexAppConfig(value) {
273
- if (!isRecord$1(value)) {
273
+ if (!isRecord(value)) {
274
274
  throw new Error("Invalid Codex app config: top-level value must be an object");
275
275
  }
276
276
  const version = value.version;
@@ -397,7 +397,7 @@ function cloneJsonObject(value) {
397
397
  return JSON.parse(JSON.stringify(value));
398
398
  }
399
399
  function removeObjectKey(value, key) {
400
- if (isRecord$1(value)) {
400
+ if (isRecord(value)) {
401
401
  delete value[key];
402
402
  }
403
403
  }
@@ -417,7 +417,7 @@ function cleanupCodexStateContainer(container, hostId, remotePaths) {
417
417
  const remoteProjects = container["remote-projects"];
418
418
  if (Array.isArray(remoteProjects)) {
419
419
  container["remote-projects"] = remoteProjects.filter((project) => {
420
- if (!isRecord$1(project)) {
420
+ if (!isRecord(project)) {
421
421
  return true;
422
422
  }
423
423
  const matches = project.hostId === hostId && typeof project.remotePath === "string" && remotePaths.has(normalizeRemotePath(project.remotePath));
@@ -429,7 +429,7 @@ function cleanupCodexStateContainer(container, hostId, remotePaths) {
429
429
  }
430
430
  const managedConnections = container["codex-managed-remote-connections"];
431
431
  if (Array.isArray(managedConnections)) {
432
- container["codex-managed-remote-connections"] = managedConnections.filter((connection) => !isRecord$1(connection) || connection.hostId !== hostId);
432
+ container["codex-managed-remote-connections"] = managedConnections.filter((connection) => !isRecord(connection) || connection.hostId !== hostId);
433
433
  }
434
434
  for (const key of [
435
435
  "remote-connection-analytics-id-by-host-id",
@@ -453,7 +453,7 @@ function cleanupProjectReferences(state, atomState, removedProjectIds) {
453
453
  for (const projectId of removedProjectIds) {
454
454
  removeObjectKey(state["sidebar-collapsed-groups"], projectId);
455
455
  }
456
- if (isRecord$1(atomState)) {
456
+ if (isRecord(atomState)) {
457
457
  cleanupStringArrayField(atomState, "project-order", removedProjectIds);
458
458
  for (const projectId of removedProjectIds) {
459
459
  removeObjectKey(atomState["sidebar-collapsed-groups"], projectId);
@@ -466,7 +466,7 @@ function removeCodexGlobalStateProject(state, entry, options = {}) {
466
466
  const nextState = cloneJsonObject(state);
467
467
  const removedProjectIds = cleanupCodexStateContainer(nextState, hostId, remotePaths);
468
468
  const atomState = nextState["electron-persisted-atom-state"];
469
- if (isRecord$1(atomState)) {
469
+ if (isRecord(atomState)) {
470
470
  for (const projectId of cleanupCodexStateContainer(atomState, hostId, remotePaths)) {
471
471
  removedProjectIds.add(projectId);
472
472
  }
@@ -482,7 +482,7 @@ function normalizeCodexStateContainer(container, hostId, canonicalRemotePath, le
482
482
  }
483
483
  let matchedProject = false;
484
484
  container["remote-projects"] = remoteProjects.flatMap((project) => {
485
- if (!isRecord$1(project) || project.hostId !== hostId || typeof project.remotePath !== "string") {
485
+ if (!isRecord(project) || project.hostId !== hostId || typeof project.remotePath !== "string") {
486
486
  return [project];
487
487
  }
488
488
  const remotePath = normalizeRemotePath(project.remotePath);
@@ -510,7 +510,7 @@ function normalizeCodexGlobalStateProject(state, entry, options = {}) {
510
510
  const nextState = cloneJsonObject(state);
511
511
  const removedProjectIds = normalizeCodexStateContainer(nextState, hostId, canonicalRemotePath, legacyRemotePaths);
512
512
  const atomState = nextState["electron-persisted-atom-state"];
513
- if (isRecord$1(atomState)) {
513
+ if (isRecord(atomState)) {
514
514
  for (const projectId of normalizeCodexStateContainer(atomState, hostId, canonicalRemotePath, legacyRemotePaths)) {
515
515
  removedProjectIds.add(projectId);
516
516
  }
@@ -608,7 +608,7 @@ function uninstallCodexGlobalStateProject(entry, options = {}) {
608
608
  }
609
609
  const existingJson = readFileSync(statePath, "utf8");
610
610
  const existingState = JSON.parse(existingJson);
611
- if (!isRecord$1(existingState)) {
611
+ if (!isRecord(existingState)) {
612
612
  throw new Error(`Invalid Codex global state JSON: ${statePath}`);
613
613
  }
614
614
  const nextState = removeCodexGlobalStateProject(existingState, entry, { additionalRemotePaths: options.additionalRemotePaths });
@@ -638,7 +638,7 @@ function installCodexGlobalStateProject(entry, options = {}) {
638
638
  }
639
639
  const existingJson = readFileSync(statePath, "utf8");
640
640
  const existingState = JSON.parse(existingJson);
641
- if (!isRecord$1(existingState)) {
641
+ if (!isRecord(existingState)) {
642
642
  throw new Error(`Invalid Codex global state JSON: ${statePath}`);
643
643
  }
644
644
  const nextState = normalizeCodexGlobalStateProject(existingState, entry, { legacyRemotePaths: options.legacyRemotePaths });
@@ -691,6 +691,13 @@ const BOXDOWN_CONTAINER_AGENTS_DIR = "/home/node/.agents";
691
691
  const BOXDOWN_CONTAINER_GITCONFIG_PATH = "/home/node/.gitconfig";
692
692
  const BOXDOWN_CONTAINER_CODEX_DIR = "/home/node/.codex";
693
693
  const BOXDOWN_CONTAINER_CODEX_AUTH_PATH = `${BOXDOWN_CONTAINER_CODEX_DIR}/auth.json`;
694
+ const BOXDOWN_CONTAINER_SECRET_ENV_DIR = "/run/boxdown/secrets";
695
+ const BOXDOWN_CONTAINER_SECRET_ENV_BOOTSTRAP = `${BOXDOWN_CONTAINER_DEVCONTAINER_DIR}/utils/secret-env-bootstrap.sh`;
696
+ const BOXDOWN_SECRET_ENV_NAMES = [
697
+ "ANTHROPIC_API_KEY",
698
+ "SNYK_TOKEN",
699
+ "OP_SERVICE_ACCOUNT_TOKEN"
700
+ ];
694
701
  const PACKAGE_NAME = "boxdown";
695
702
 
696
703
  //#endregion
@@ -844,10 +851,14 @@ function interactiveTermSetupScript() {
844
851
  function interactiveAgentPathScript() {
845
852
  return ["codex_home=\"${CODEX_HOME:-${HOME}/.codex}\"", "export PATH=\"${HOME}/.local/bin:${HOME}/.opencode/bin:${codex_home}/packages/standalone/current/bin:${PATH}\""].join("\n");
846
853
  }
854
+ function runtimeSecretEnvironmentScript() {
855
+ return "source \"${BASH_ENV:-/opt/boxdown/devcontainer/utils/secret-env-bootstrap.sh}\"";
856
+ }
847
857
  function interactiveShellScript() {
848
858
  return [
849
859
  interactiveTermSetupScript(),
850
860
  interactiveTtySetupScript(),
861
+ runtimeSecretEnvironmentScript(),
851
862
  "exec bash -i"
852
863
  ].join("\n");
853
864
  }
@@ -856,6 +867,7 @@ function interactiveCommandScript() {
856
867
  interactiveTermSetupScript(),
857
868
  interactiveTtySetupScript(),
858
869
  interactiveAgentPathScript(),
870
+ runtimeSecretEnvironmentScript(),
859
871
  "exec \"$@\""
860
872
  ].join("\n");
861
873
  }
@@ -892,7 +904,8 @@ function buildGeneratedDevcontainerConfig(context, signing) {
892
904
  const boxdownMounts = [
893
905
  `type=bind,source=${context.assetsDevcontainerDir},target=${BOXDOWN_CONTAINER_DEVCONTAINER_DIR},readonly`,
894
906
  `type=bind,source=${context.sshPublicKeyRuntimeDir},target=${BOXDOWN_CONTAINER_SSH_DIR},readonly`,
895
- `type=bind,source=${context.hostGitconfigSnapshotDir},target=${BOXDOWN_CONTAINER_HOST_GITCONFIG_DIR},readonly`
907
+ `type=bind,source=${context.hostGitconfigSnapshotDir},target=${BOXDOWN_CONTAINER_HOST_GITCONFIG_DIR},readonly`,
908
+ `type=bind,source=${context.workspaceSecretEnvDir},target=${BOXDOWN_CONTAINER_SECRET_ENV_DIR},readonly`
896
909
  ];
897
910
  if (directoryExists(context.hostAgentsDir) && !hasMountTarget(mounts, "/home/node/.agents")) {
898
911
  boxdownMounts.push(`type=bind,source=${context.hostAgentsDir},target=${BOXDOWN_CONTAINER_AGENTS_DIR},readonly`);
@@ -912,6 +925,7 @@ function buildGeneratedDevcontainerConfig(context, signing) {
912
925
  `BOXDOWN_WORKSPACE_FOLDER=${shellQuote(context.workspaceFolder)}`,
913
926
  `BOXDOWN_HOST_GITCONFIG_PATH=${shellQuote(context.hostGitconfigPath)}`,
914
927
  `BOXDOWN_HOST_GITCONFIG_SNAPSHOT_PATH=${shellQuote(context.hostGitconfigSnapshotPath)}`,
928
+ `BOXDOWN_SECRET_ENV_DIR=${shellQuote(context.workspaceSecretEnvDir)}`,
915
929
  `BOXDOWN_PROGRESS=${shellQuote("${localEnv:BOXDOWN_PROGRESS}")}`,
916
930
  `BOXDOWN_VERBOSE=${shellQuote("${localEnv:BOXDOWN_VERBOSE}")}`,
917
931
  "bash",
@@ -933,10 +947,16 @@ function buildGeneratedDevcontainerConfig(context, signing) {
933
947
  ...baseConfig.containerEnv ?? {},
934
948
  BOXDOWN_CONTAINER_WORKSPACE_FOLDER: "/workspaces/${localWorkspaceFolderBasename}",
935
949
  BOXDOWN_WORKSPACE_BASENAME: "${localWorkspaceFolderBasename}",
950
+ BOXDOWN_SECRET_ENV_DIR: BOXDOWN_CONTAINER_SECRET_ENV_DIR,
951
+ BASH_ENV: BOXDOWN_CONTAINER_SECRET_ENV_BOOTSTRAP,
936
952
  DEVCONTAINER_SSH_PUBLIC_KEY_FILE: BOXDOWN_CONTAINER_SSH_PUBLIC_KEY_PATH,
937
953
  BOXDOWN_GIT_SIGNING_ENABLED: signing?.enabled === true ? "1" : "0",
938
954
  BOXDOWN_GIT_SIGNING_KEY_PATH: "/opt/boxdown/state/git-signing/signing-key.pub",
939
- ...signing?.enabled === true ? { SSH_AUTH_SOCK: "/run/boxdown/ssh-agent.sock" } : {}
955
+ ...signing?.enabled === false && signing.reason !== undefined ? { BOXDOWN_GIT_SIGNING_REASON: signing.reason } : {},
956
+ ...signing?.enabled === true ? {
957
+ BOXDOWN_GIT_SIGNING_SOURCE_SOCKET: "/run/boxdown/ssh-agent.sock",
958
+ SSH_AUTH_SOCK: "/run/boxdown/ssh-agent-node.sock"
959
+ } : {}
940
960
  }
941
961
  };
942
962
  }
@@ -1116,6 +1136,246 @@ function runInteractive(command, args, options = {}) {
1116
1136
  });
1117
1137
  }
1118
1138
 
1139
+ //#endregion
1140
+ //#region src/git-signing.ts
1141
+ const GIT_SIGNING_REASON_MESSAGES = {
1142
+ "agent-unavailable": "the host SSH agent is unavailable",
1143
+ "no-identities": "the host SSH agent has no loaded identities",
1144
+ "ambiguous-identities": "multiple SSH identities are loaded and no signing key could be selected safely",
1145
+ "configured-key-unreadable": "the configured SSH signing-key file could not be read",
1146
+ "configured-key-invalid": "the configured SSH signing key is not a valid public key",
1147
+ "configured-key-not-loaded": "the configured SSH signing key is not loaded in the agent",
1148
+ "agent-socket-unavailable": "the host SSH-agent socket is unavailable",
1149
+ "docker-probe-image-unavailable": "no local Docker image is available to probe the SSH-agent mount",
1150
+ "agent-mount-unavailable": "Docker could not mount the host SSH-agent socket"
1151
+ };
1152
+ function compactDiagnosticDetail(detail) {
1153
+ return detail.trim().replace(/\s+/gu, " ").slice(0, 2e3).replace(/-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)/giu, "[redacted-private-key]").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]").replace(/\b(?:github_pat_|gh[pousr]_|glpat-|xox[baprs]-)[A-Za-z0-9_-]{8,}/gu, "[redacted-token]").replace(/\b(?:Bearer\s+|token[=:]\s*)[A-Za-z0-9._~+/-]{8,}/giu, "[redacted-token]").slice(0, 300);
1154
+ }
1155
+ function reportGitSigningPlan(plan, options = {}) {
1156
+ if (plan.enabled) return;
1157
+ const reason = plan.reason ?? "agent-unavailable";
1158
+ const detail = plan.detail === undefined ? "" : ` detail=${compactDiagnosticDetail(plan.detail)}`;
1159
+ options.logger?.boxdown(`git-signing: enabled=false reason=${reason}${detail}\n`);
1160
+ if (options.quiet !== true) {
1161
+ const writeWarning = options.writeWarning ?? ((message) => process.stderr.write(message));
1162
+ writeWarning(`boxdown: commit signing disabled: ${GIT_SIGNING_REASON_MESSAGES[reason]}; commits will remain unsigned.\n`);
1163
+ }
1164
+ }
1165
+ function parseSshPublicKey(value) {
1166
+ const [algorithm, key] = value.trim().split(/\s+/, 3);
1167
+ if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
1168
+ return undefined;
1169
+ }
1170
+ return `${algorithm} ${key}`;
1171
+ }
1172
+ function resolveConfiguredSshSigningKey(value, options) {
1173
+ const inlineValue = value.startsWith("key::") ? value.slice("key::".length) : value;
1174
+ const inlineKey = parseSshPublicKey(inlineValue);
1175
+ if (inlineKey !== undefined) {
1176
+ return { key: inlineKey };
1177
+ }
1178
+ if (value.startsWith("key::")) {
1179
+ return {
1180
+ reason: "configured-key-invalid",
1181
+ detail: "configured inline value is not a valid SSH public key"
1182
+ };
1183
+ }
1184
+ let keyPath;
1185
+ if (value.startsWith("~/")) {
1186
+ if (options.homeDir === undefined) {
1187
+ return {
1188
+ reason: "configured-key-unreadable",
1189
+ detail: "configured public-key file could not be read"
1190
+ };
1191
+ }
1192
+ keyPath = join(options.homeDir, value.slice(2));
1193
+ } else {
1194
+ keyPath = isAbsolute(value) ? value : resolve(options.workspaceFolder, value);
1195
+ }
1196
+ let publicKeyText;
1197
+ try {
1198
+ publicKeyText = readFileSync(keyPath, "utf8");
1199
+ } catch {
1200
+ return {
1201
+ reason: "configured-key-unreadable",
1202
+ detail: "configured public-key file could not be read"
1203
+ };
1204
+ }
1205
+ const publicKey = parseSshPublicKey(publicKeyText.split(/\r?\n/u).find((line) => line.trim().length > 0) ?? "");
1206
+ return publicKey === undefined ? {
1207
+ reason: "configured-key-invalid",
1208
+ detail: "configured public-key file does not contain a valid SSH public key"
1209
+ } : { key: publicKey };
1210
+ }
1211
+ function selectGitSigningKey(identities, configuredKey, githubKeys) {
1212
+ const keys = identities.map(parseSshPublicKey).filter((key) => key !== undefined);
1213
+ if (keys.length === 0) return { reason: "no-identities" };
1214
+ const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey);
1215
+ if (configuredKey !== undefined && configured === undefined) {
1216
+ return { reason: "configured-key-invalid" };
1217
+ }
1218
+ if (configured !== undefined) {
1219
+ return keys.includes(configured) ? { key: configured } : { reason: "configured-key-not-loaded" };
1220
+ }
1221
+ const github = new Set((githubKeys ?? []).map(parseSshPublicKey).filter((key) => key !== undefined));
1222
+ const matches = keys.filter((key) => github.has(key));
1223
+ if (matches.length === 1) return { key: matches[0] };
1224
+ if (keys.length === 1) return { key: keys[0] };
1225
+ return { reason: "ambiguous-identities" };
1226
+ }
1227
+ function writeGitSigningPublicKey(context, key) {
1228
+ mkdirSync(context.gitSigningStateDir, {
1229
+ recursive: true,
1230
+ mode: 448
1231
+ });
1232
+ writeFileSync(join(context.gitSigningStateDir, "signing-key.pub"), `${parseSshPublicKey(key) ?? key}\n`, { mode: 420 });
1233
+ }
1234
+ async function runGitSigningCommand(command, args) {
1235
+ return runBuffered(command, args, {
1236
+ mirrorStdout: false,
1237
+ mirrorStderr: false
1238
+ });
1239
+ }
1240
+ function failedCommandDetail(label, result) {
1241
+ const stderr = compactDiagnosticDetail(result.stderr);
1242
+ return stderr.length === 0 ? `${label} failed with exit code ${result.code}` : `${label} failed with exit code ${result.code}: ${stderr}`;
1243
+ }
1244
+ async function probeDockerAgentMount(source, runCommand) {
1245
+ const images = await runCommand("docker", [
1246
+ "image",
1247
+ "ls",
1248
+ "--format",
1249
+ "{{.Repository}}:{{.Tag}}"
1250
+ ]);
1251
+ const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== "<none>:<none>");
1252
+ if (images.code !== 0) {
1253
+ return {
1254
+ ok: false,
1255
+ reason: "agent-mount-unavailable",
1256
+ detail: failedCommandDetail("Docker image listing", images)
1257
+ };
1258
+ }
1259
+ if (image === undefined) {
1260
+ return {
1261
+ ok: false,
1262
+ reason: "docker-probe-image-unavailable",
1263
+ detail: "no tagged local Docker image was found"
1264
+ };
1265
+ }
1266
+ const created = await runCommand("docker", [
1267
+ "create",
1268
+ "--pull=never",
1269
+ "--entrypoint",
1270
+ "/bin/true",
1271
+ "--mount",
1272
+ `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`,
1273
+ image
1274
+ ]);
1275
+ const containerId = created.stdout.trim().split(/\r?\n/)[0];
1276
+ if (created.code !== 0 || containerId === undefined || containerId.length === 0) {
1277
+ return {
1278
+ ok: false,
1279
+ reason: "agent-mount-unavailable",
1280
+ detail: failedCommandDetail("Docker SSH-agent mount probe", created)
1281
+ };
1282
+ }
1283
+ await runCommand("docker", [
1284
+ "rm",
1285
+ "-f",
1286
+ containerId
1287
+ ]);
1288
+ return { ok: true };
1289
+ }
1290
+ async function resolveGitSigningPlan(context, options = {}) {
1291
+ const runCommand = options.runCommand ?? runGitSigningCommand;
1292
+ const result = await runCommand("ssh-add", ["-L"]);
1293
+ if (result.code !== 0) {
1294
+ return {
1295
+ enabled: false,
1296
+ reason: "agent-unavailable",
1297
+ detail: failedCommandDetail("ssh-add -L", result)
1298
+ };
1299
+ }
1300
+ const identities = result.stdout.split(/\r?\n/);
1301
+ const format = await runCommand("git", [
1302
+ "config",
1303
+ "--global",
1304
+ "--get",
1305
+ "gpg.format"
1306
+ ]);
1307
+ let configuredKey;
1308
+ if (format.code === 0 && format.stdout.trim() === "ssh") {
1309
+ const signingKey = await runCommand("git", [
1310
+ "config",
1311
+ "--global",
1312
+ "--get",
1313
+ "user.signingkey"
1314
+ ]);
1315
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
1316
+ const resolvedKey = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
1317
+ homeDir: options.env?.HOME ?? process.env.HOME,
1318
+ workspaceFolder: context.workspaceFolder
1319
+ });
1320
+ if (resolvedKey.key === undefined) {
1321
+ return {
1322
+ enabled: false,
1323
+ reason: resolvedKey.reason ?? "configured-key-invalid",
1324
+ detail: resolvedKey.detail
1325
+ };
1326
+ }
1327
+ configuredKey = resolvedKey.key;
1328
+ }
1329
+ }
1330
+ let githubKeys;
1331
+ if (configuredKey === undefined && identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
1332
+ const user = await runCommand("gh", [
1333
+ "api",
1334
+ "user",
1335
+ "--jq",
1336
+ ".login"
1337
+ ]);
1338
+ const login = user.stdout.trim();
1339
+ if (user.code === 0 && login.length > 0) {
1340
+ const github = await runCommand("gh", [
1341
+ "api",
1342
+ `users/${login}/keys`,
1343
+ "--paginate",
1344
+ "--jq",
1345
+ ".[].key"
1346
+ ]);
1347
+ if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/);
1348
+ }
1349
+ }
1350
+ const selected = selectGitSigningKey(identities, configuredKey, githubKeys);
1351
+ if (selected.key === undefined) return {
1352
+ enabled: false,
1353
+ reason: selected.reason
1354
+ };
1355
+ const agentSocketSource = (options.platform ?? process.platform) === "darwin" ? "/run/host-services/ssh-auth.sock" : options.env?.SSH_AUTH_SOCK ?? process.env.SSH_AUTH_SOCK;
1356
+ if (agentSocketSource === undefined || agentSocketSource.length === 0) {
1357
+ return {
1358
+ enabled: false,
1359
+ reason: "agent-socket-unavailable",
1360
+ detail: "SSH_AUTH_SOCK is not set"
1361
+ };
1362
+ }
1363
+ const mountProbe = await probeDockerAgentMount(agentSocketSource, runCommand);
1364
+ if (!mountProbe.ok) {
1365
+ return {
1366
+ enabled: false,
1367
+ reason: mountProbe.reason,
1368
+ detail: mountProbe.detail
1369
+ };
1370
+ }
1371
+ writeGitSigningPublicKey(context, selected.key);
1372
+ return {
1373
+ enabled: true,
1374
+ publicKey: selected.key,
1375
+ agentSocketSource
1376
+ };
1377
+ }
1378
+
1119
1379
  //#endregion
1120
1380
  //#region src/doctor.ts
1121
1381
  function nodeVersionPasses(version) {
@@ -1129,6 +1389,26 @@ function check(name, pass, okMessage, failMessage) {
1129
1389
  message: pass ? okMessage : failMessage
1130
1390
  };
1131
1391
  }
1392
+ function secretEnvironmentConfigCheck(context) {
1393
+ let config;
1394
+ try {
1395
+ config = existsSync(context.generatedConfigPath) ? JSON.parse(readFileSync(context.generatedConfigPath, "utf8")) : buildGeneratedDevcontainerConfig(context);
1396
+ } catch {
1397
+ return {
1398
+ name: "secret-environment-config",
1399
+ level: "warn",
1400
+ message: "Generated config could not be checked for secret-safe environment handling"
1401
+ };
1402
+ }
1403
+ const runArgs = Array.isArray(config.runArgs) ? config.runArgs : [];
1404
+ const containerEnv = config.containerEnv ?? {};
1405
+ const unsafe = runArgs.includes("--env-file") || runArgs.some((arg) => arg.includes(".env.development")) || BOXDOWN_SECRET_ENV_NAMES.some((name) => Object.hasOwn(containerEnv, name));
1406
+ return {
1407
+ name: "secret-environment-config",
1408
+ level: unsafe ? "warn" : "ok",
1409
+ message: unsafe ? "Generated config still exposes Boxdown secrets through Docker environment settings; recreate after upgrading Boxdown" : "Generated config uses runtime-mounted secrets without Docker environment values"
1410
+ };
1411
+ }
1132
1412
  async function runDoctorCommand(command, args) {
1133
1413
  return runBuffered(command, args, {
1134
1414
  mirrorStdout: false,
@@ -1149,11 +1429,91 @@ async function runDoctorChecks(context, options = {}) {
1149
1429
  const nodeVersion = process.versions.node;
1150
1430
  checks.push(check("node", nodeVersionPasses(nodeVersion), `Node ${nodeVersion}`, `Node ${nodeVersion}; expected >=24.0.0`));
1151
1431
  const sshAgent = await runCommand("ssh-add", ["-L"]);
1152
- const identities = sshAgent.code === 0 ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith("ssh-")).length : 0;
1432
+ const identityLines = sshAgent.code === 0 ? sshAgent.stdout.split(/\r?\n/).filter((line) => line.trim().startsWith("ssh-")) : [];
1433
+ const identities = identityLines.length;
1434
+ let configuredKey;
1435
+ let configuredFailure;
1436
+ const format = await runCommand("git", [
1437
+ "config",
1438
+ "--global",
1439
+ "--get",
1440
+ "gpg.format"
1441
+ ]);
1442
+ if (format.code === 0 && format.stdout.trim() === "ssh") {
1443
+ const signingKey = await runCommand("git", [
1444
+ "config",
1445
+ "--global",
1446
+ "--get",
1447
+ "user.signingkey"
1448
+ ]);
1449
+ if (signingKey.code === 0 && signingKey.stdout.trim().length > 0) {
1450
+ const resolved = resolveConfiguredSshSigningKey(signingKey.stdout.trim(), {
1451
+ homeDir: dirname(context.hostGitconfigPath),
1452
+ workspaceFolder: context.workspaceFolder
1453
+ });
1454
+ if (resolved.key === undefined) {
1455
+ configuredFailure = {
1456
+ reason: resolved.reason ?? "configured-key-invalid",
1457
+ detail: resolved.detail
1458
+ };
1459
+ } else {
1460
+ configuredKey = resolved.key;
1461
+ }
1462
+ }
1463
+ }
1464
+ const includeOptional = options.includeOptional ?? true;
1465
+ let ghAvailable = false;
1466
+ let ghAuth = false;
1467
+ let githubLogin;
1468
+ let githubAuthKeys;
1469
+ if (includeOptional) {
1470
+ ghAvailable = await commandWorks(runCommand, "gh", ["--version"]);
1471
+ if (ghAvailable) {
1472
+ ghAuth = await commandWorks(runCommand, "gh", [
1473
+ "auth",
1474
+ "status",
1475
+ "--hostname",
1476
+ "github.com"
1477
+ ]);
1478
+ if (ghAuth && configuredKey === undefined && configuredFailure === undefined && identities > 1) {
1479
+ const user = await runCommand("gh", [
1480
+ "api",
1481
+ "user",
1482
+ "--jq",
1483
+ ".login"
1484
+ ]);
1485
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined;
1486
+ if (githubLogin !== undefined) {
1487
+ const authentication = await runCommand("gh", [
1488
+ "api",
1489
+ `users/${githubLogin}/keys`,
1490
+ "--paginate",
1491
+ "--jq",
1492
+ ".[].key"
1493
+ ]);
1494
+ if (authentication.code === 0) githubAuthKeys = authentication.stdout.split(/\r?\n/);
1495
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ const selected = configuredFailure ?? selectGitSigningKey(identityLines, configuredKey, githubAuthKeys);
1500
+ const selectedByConfiguration = selected.key !== undefined && configuredKey !== undefined;
1501
+ const selectedByGithub = selected.key !== undefined && configuredKey === undefined && identities > 1;
1502
+ const signingMessages = {
1503
+ "agent-unavailable": "SSH agent is unavailable; Boxdown commits will remain unsigned",
1504
+ "no-identities": "SSH agent has no identities; Boxdown commits will remain unsigned",
1505
+ "ambiguous-identities": "SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned",
1506
+ "configured-key-unreadable": "Configured SSH signing-key file could not be read; Boxdown commits will remain unsigned",
1507
+ "configured-key-invalid": "Configured SSH signing key is not a valid public key; Boxdown commits will remain unsigned",
1508
+ "configured-key-not-loaded": "Configured SSH signing key is not loaded in the agent; Boxdown commits will remain unsigned",
1509
+ "agent-socket-unavailable": "Host SSH-agent socket is unavailable; Boxdown commits will remain unsigned",
1510
+ "docker-probe-image-unavailable": "No local Docker image is available to probe commit-signing agent forwarding",
1511
+ "agent-mount-unavailable": "Docker could not mount the host SSH-agent socket; Boxdown commits will remain unsigned"
1512
+ };
1153
1513
  checks.push({
1154
1514
  name: "git-signing-agent",
1155
- level: sshAgent.code === 0 && identities === 1 ? "ok" : "warn",
1156
- message: sshAgent.code !== 0 ? "SSH agent is unavailable; Boxdown commits will remain unsigned" : identities === 0 ? "SSH agent has no identities; Boxdown commits will remain unsigned" : identities > 1 ? "SSH agent has multiple identities; Boxdown will not guess a signing key and commits will remain unsigned" : "One SSH agent identity is available for Boxdown commit signing"
1515
+ level: sshAgent.code === 0 && selected.key !== undefined ? "ok" : "warn",
1516
+ message: sshAgent.code !== 0 ? signingMessages["agent-unavailable"] : selected.key === undefined ? signingMessages[selected.reason ?? "ambiguous-identities"] : selectedByConfiguration ? "Configured SSH signing key is loaded in the agent" : selectedByGithub ? "GitHub authentication keys identify one SSH agent identity for Boxdown commit signing" : "One SSH agent identity is available for Boxdown commit signing"
1157
1517
  });
1158
1518
  checks.push(check("devcontainers-cli", await packagedDevcontainerCliWorks(context, runCommand), "Packaged @devcontainers/cli is available", "Packaged @devcontainers/cli is required but was not available"));
1159
1519
  const dockerCliWorks = await commandWorks(runCommand, "docker", ["--version"]);
@@ -1166,29 +1526,27 @@ async function runDoctorChecks(context, options = {}) {
1166
1526
  }
1167
1527
  checks.push(check("ssh-keygen", await commandExists(runCommand, "ssh-keygen", ["-?"]), "ssh-keygen is available", "ssh-keygen is required but was not available"));
1168
1528
  checks.push(check("assets", existsSync(context.assetsDevcontainerDir), `Devcontainer assets found at ${context.assetsDevcontainerDir}`, `Missing Boxdown devcontainer assets: ${context.assetsDevcontainerDir}`));
1169
- if (options.includeOptional ?? true) {
1170
- if (await commandWorks(runCommand, "gh", ["--version"])) {
1171
- const ghAuth = await commandWorks(runCommand, "gh", [
1172
- "auth",
1173
- "status",
1174
- "--hostname",
1175
- "github.com"
1176
- ]);
1529
+ checks.push(secretEnvironmentConfigCheck(context));
1530
+ if (includeOptional) {
1531
+ if (ghAvailable) {
1177
1532
  checks.push({
1178
1533
  name: "gh-auth",
1179
1534
  level: ghAuth ? "ok" : "warn",
1180
1535
  message: ghAuth ? "GitHub CLI auth is available" : "GitHub CLI is available but not authenticated"
1181
1536
  });
1182
- if (ghAuth && identities === 1) {
1183
- const user = await runCommand("gh", [
1184
- "api",
1185
- "user",
1186
- "--jq",
1187
- ".login"
1188
- ]);
1189
- const signing = user.code === 0 && user.stdout.trim().length > 0 ? await runCommand("gh", [
1537
+ if (ghAuth && selected.key !== undefined) {
1538
+ if (githubLogin === undefined) {
1539
+ const user = await runCommand("gh", [
1540
+ "api",
1541
+ "user",
1542
+ "--jq",
1543
+ ".login"
1544
+ ]);
1545
+ githubLogin = user.code === 0 && user.stdout.trim().length > 0 ? user.stdout.trim() : undefined;
1546
+ }
1547
+ const signing = githubLogin !== undefined ? await runCommand("gh", [
1190
1548
  "api",
1191
- `users/${user.stdout.trim()}/ssh_signing_keys`,
1549
+ `users/${githubLogin}/ssh_signing_keys`,
1192
1550
  "--paginate",
1193
1551
  "--jq",
1194
1552
  ".[].key"
@@ -1197,11 +1555,10 @@ async function runDoctorChecks(context, options = {}) {
1197
1555
  stdout: "",
1198
1556
  stderr: ""
1199
1557
  };
1200
- const identity = sshAgent.stdout.split(/\r?\n/).find((line) => line.trim().startsWith("ssh-"))?.trim();
1201
1558
  checks.push({
1202
1559
  name: "git-signing-github",
1203
- level: signing.code === 0 && identity !== undefined && signing.stdout.includes(identity.split(/\s+/, 3).slice(0, 2).join(" ")) ? "ok" : "warn",
1204
- message: signing.code !== 0 ? "GitHub SSH signing-key registration could not be checked" : signing.stdout.includes(identity?.split(/\s+/, 3).slice(0, 2).join(" ") ?? "") ? "Selected SSH key is registered with GitHub for commit signing" : "Register the selected public key with GitHub as a signing key to receive Verified badges"
1560
+ level: signing.code === 0 && signing.stdout.includes(selected.key) ? "ok" : "warn",
1561
+ message: signing.code !== 0 ? "GitHub SSH signing-key registration could not be checked" : signing.stdout.includes(selected.key) ? "Selected SSH key is registered with GitHub for commit signing" : "Register the selected public key with GitHub as a signing key to receive Verified badges"
1205
1562
  });
1206
1563
  }
1207
1564
  } else {
@@ -1247,6 +1604,11 @@ async function checkDockerBindMounts(context, runCommand, dockerReady) {
1247
1604
  }
1248
1605
  mkdirSync(context.workspaceDataDir, { recursive: true });
1249
1606
  const runtimeProbeDir = mkdtempSync(join(context.workspaceDataDir, "doctor-mount-probe-"));
1607
+ mkdirSync(context.workspaceSecretEnvDir, {
1608
+ recursive: true,
1609
+ mode: 448
1610
+ });
1611
+ chmodSync(context.workspaceSecretEnvDir, 448);
1250
1612
  const sources = [
1251
1613
  {
1252
1614
  label: "workspace",
@@ -1259,6 +1621,10 @@ async function checkDockerBindMounts(context, runCommand, dockerReady) {
1259
1621
  {
1260
1622
  label: "Boxdown runtime state",
1261
1623
  path: runtimeProbeDir
1624
+ },
1625
+ {
1626
+ label: "Boxdown runtime secret state",
1627
+ path: context.workspaceSecretEnvDir
1262
1628
  }
1263
1629
  ];
1264
1630
  try {
@@ -1491,147 +1857,6 @@ async function runWorkspaceGit(workspaceFolder, args) {
1491
1857
  });
1492
1858
  }
1493
1859
 
1494
- //#endregion
1495
- //#region src/git-signing.ts
1496
- function parseSshPublicKey(value) {
1497
- const [algorithm, key] = value.trim().split(/\s+/, 3);
1498
- if (algorithm === undefined || key === undefined || !/^ssh-[A-Za-z0-9-]+$/.test(algorithm) || key.length === 0) {
1499
- return undefined;
1500
- }
1501
- return `${algorithm} ${key}`;
1502
- }
1503
- function selectGitSigningKey(identities, configuredKey, githubKeys) {
1504
- const keys = identities.map(parseSshPublicKey).filter((key) => key !== undefined);
1505
- if (keys.length === 0) return { reason: "no-identities" };
1506
- const configured = configuredKey === undefined ? undefined : parseSshPublicKey(configuredKey);
1507
- if (configured !== undefined) {
1508
- return keys.includes(configured) ? { key: configured } : { reason: "configured-key-not-loaded" };
1509
- }
1510
- const github = new Set((githubKeys ?? []).map(parseSshPublicKey).filter((key) => key !== undefined));
1511
- const matches = keys.filter((key) => github.has(key));
1512
- if (matches.length === 1) return { key: matches[0] };
1513
- if (keys.length === 1) return { key: keys[0] };
1514
- return { reason: "ambiguous-identities" };
1515
- }
1516
- function writeGitSigningPublicKey(context, key) {
1517
- mkdirSync(context.gitSigningStateDir, {
1518
- recursive: true,
1519
- mode: 448
1520
- });
1521
- writeFileSync(join(context.gitSigningStateDir, "signing-key.pub"), `${parseSshPublicKey(key) ?? key}\n`, { mode: 420 });
1522
- }
1523
- async function dockerCanMountAgent(source) {
1524
- const images = await runBuffered("docker", [
1525
- "image",
1526
- "ls",
1527
- "--format",
1528
- "{{.Repository}}:{{.Tag}}"
1529
- ], {
1530
- mirrorStdout: false,
1531
- mirrorStderr: false
1532
- });
1533
- const image = images.stdout.split(/\r?\n/).map((value) => value.trim()).find((value) => value.length > 0 && value !== "<none>:<none>");
1534
- if (images.code !== 0 || image === undefined) return false;
1535
- const created = await runBuffered("docker", [
1536
- "create",
1537
- "--pull=never",
1538
- "--entrypoint",
1539
- "/bin/true",
1540
- "--mount",
1541
- `type=bind,source=${source},target=/run/boxdown/ssh-agent.sock`,
1542
- image
1543
- ], {
1544
- mirrorStdout: false,
1545
- mirrorStderr: false
1546
- });
1547
- const containerId = created.stdout.trim().split(/\r?\n/)[0];
1548
- if (created.code !== 0 || containerId === undefined || containerId.length === 0) return false;
1549
- await runBuffered("docker", [
1550
- "rm",
1551
- "-f",
1552
- containerId
1553
- ], {
1554
- mirrorStdout: false,
1555
- mirrorStderr: false
1556
- });
1557
- return true;
1558
- }
1559
- async function resolveGitSigningPlan(context) {
1560
- const result = await runBuffered("ssh-add", ["-L"], {
1561
- mirrorStdout: false,
1562
- mirrorStderr: false
1563
- });
1564
- if (result.code !== 0) return {
1565
- enabled: false,
1566
- reason: "agent-unavailable"
1567
- };
1568
- const identities = result.stdout.split(/\r?\n/);
1569
- const format = await runBuffered("git", [
1570
- "config",
1571
- "--global",
1572
- "--get",
1573
- "gpg.format"
1574
- ], {
1575
- mirrorStdout: false,
1576
- mirrorStderr: false
1577
- });
1578
- const configuredKey = format.code === 0 && format.stdout.trim() === "ssh" ? (await runBuffered("git", [
1579
- "config",
1580
- "--global",
1581
- "--get",
1582
- "user.signingkey"
1583
- ], {
1584
- mirrorStdout: false,
1585
- mirrorStderr: false
1586
- })).stdout.trim() : undefined;
1587
- let githubKeys;
1588
- if (identities.filter((key) => parseSshPublicKey(key) !== undefined).length > 1) {
1589
- const user = await runBuffered("gh", [
1590
- "api",
1591
- "user",
1592
- "--jq",
1593
- ".login"
1594
- ], {
1595
- mirrorStdout: false,
1596
- mirrorStderr: false
1597
- });
1598
- const login = user.stdout.trim();
1599
- if (user.code === 0 && login.length > 0) {
1600
- const github = await runBuffered("gh", [
1601
- "api",
1602
- `users/${login}/keys`,
1603
- "--paginate",
1604
- "--jq",
1605
- ".[].key"
1606
- ], {
1607
- mirrorStdout: false,
1608
- mirrorStderr: false
1609
- });
1610
- if (github.code === 0) githubKeys = github.stdout.split(/\r?\n/);
1611
- }
1612
- }
1613
- const selected = selectGitSigningKey(identities, configuredKey, githubKeys);
1614
- if (selected.key === undefined) return {
1615
- enabled: false,
1616
- reason: selected.reason
1617
- };
1618
- const agentSocketSource = process.platform === "darwin" ? "/run/host-services/ssh-auth.sock" : process.env.SSH_AUTH_SOCK;
1619
- if (agentSocketSource === undefined || agentSocketSource.length === 0) return {
1620
- enabled: false,
1621
- reason: "agent-mount-unavailable"
1622
- };
1623
- if (!await dockerCanMountAgent(agentSocketSource)) return {
1624
- enabled: false,
1625
- reason: "agent-mount-unavailable"
1626
- };
1627
- writeGitSigningPublicKey(context, selected.key);
1628
- return {
1629
- enabled: true,
1630
- publicKey: selected.key,
1631
- agentSocketSource
1632
- };
1633
- }
1634
-
1635
1860
  //#endregion
1636
1861
  //#region src/metadata.ts
1637
1862
  const WORKSPACE_METADATA_VERSION = 1;
@@ -2702,34 +2927,30 @@ async function findRunningContainerId(context, options = {}) {
2702
2927
  }
2703
2928
  return result.stdout.split(/\r?\n/).find((line) => line.length > 0);
2704
2929
  }
2705
- function isRecord(value) {
2706
- return value !== null && typeof value === "object" && !Array.isArray(value);
2707
- }
2708
2930
  function parseDockerInspectImage(output, containerId) {
2709
- const trimmed = output.trim();
2710
- if (trimmed.length === 0) {
2931
+ const [rawId, rawName] = output.trim().split("|");
2932
+ if (rawId === undefined || rawId.length === 0) {
2711
2933
  return undefined;
2712
2934
  }
2713
- let parsed;
2935
+ let id;
2936
+ let name;
2714
2937
  try {
2715
- parsed = JSON.parse(trimmed);
2938
+ id = JSON.parse(rawId);
2939
+ name = rawName === undefined || rawName.length === 0 ? undefined : JSON.parse(rawName);
2716
2940
  } catch (error) {
2717
2941
  throw new Error(`Could not parse docker inspect output for ${containerId}`, { cause: error });
2718
2942
  }
2719
- if (typeof parsed.Image !== "string" || parsed.Image.length === 0) {
2720
- return undefined;
2721
- }
2722
- const configImage = isRecord(parsed.Config) && typeof parsed.Config.Image === "string" && parsed.Config.Image.length > 0 ? parsed.Config.Image : undefined;
2943
+ if (typeof id !== "string" || id.length === 0) return undefined;
2723
2944
  return {
2724
- id: parsed.Image,
2725
- ...configImage === undefined ? {} : { name: configImage }
2945
+ id,
2946
+ ...typeof name === "string" && name.length > 0 ? { name } : {}
2726
2947
  };
2727
2948
  }
2728
2949
  async function inspectContainerImage(containerId, options = {}) {
2729
2950
  const result = await runBuffered("docker", [
2730
2951
  "inspect",
2731
2952
  "--format",
2732
- "{{json .}}",
2953
+ "{{json .Image}}|{{json .Config.Image}}",
2733
2954
  containerId
2734
2955
  ], {
2735
2956
  logger: options.logger,
@@ -2851,7 +3072,15 @@ async function startDevcontainer(context, options = {}) {
2851
3072
  progress.detail(context.generatedConfigPath);
2852
3073
  }
2853
3074
  try {
2854
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context));
3075
+ const signingPlan = await resolveGitSigningPlan(context);
3076
+ reportGitSigningPlan(signingPlan, {
3077
+ logger: options.logger,
3078
+ quiet: proxyMode,
3079
+ ...progress === undefined ? {} : { writeWarning: (message) => {
3080
+ progress.warn(message.trimEnd());
3081
+ } }
3082
+ });
3083
+ writeGeneratedDevcontainerConfig(context, signingPlan);
2855
3084
  if (hasConfigStep) {
2856
3085
  progress?.completeStep("devcontainer-config");
2857
3086
  }
@@ -3137,7 +3366,14 @@ async function refreshContainerGhAuth(context, options = {}) {
3137
3366
  quiet: true,
3138
3367
  progress
3139
3368
  });
3140
- writeGeneratedDevcontainerConfig(context, await resolveGitSigningPlan(context));
3369
+ const signingPlan = await resolveGitSigningPlan(context);
3370
+ reportGitSigningPlan(signingPlan, {
3371
+ logger: options.logger,
3372
+ ...progress === undefined ? {} : { writeWarning: (message) => {
3373
+ progress.warn(message.trimEnd());
3374
+ } }
3375
+ });
3376
+ writeGeneratedDevcontainerConfig(context, signingPlan);
3141
3377
  if (hasConfigStep) {
3142
3378
  progress?.completeStep("gh-auth-config");
3143
3379
  }
@@ -3859,6 +4095,9 @@ function errorMessage$1(error) {
3859
4095
  function argvText(command, args) {
3860
4096
  return JSON.stringify([command, ...args]);
3861
4097
  }
4098
+ function redactKnownSecretEnvironmentAssignments(value) {
4099
+ return value.replace(/ANTHROPIC_API_KEY=[^\s,"\]}]+/gu, "ANTHROPIC_API_KEY=[redacted]").replace(/SNYK_TOKEN=[^\s,"\]}]+/gu, "SNYK_TOKEN=[redacted]").replace(/OP_SERVICE_ACCOUNT_TOKEN=[^\s,"\]}]+/gu, "OP_SERVICE_ACCOUNT_TOKEN=[redacted]");
4100
+ }
3862
4101
  var WorkspaceCommandLogger = class {
3863
4102
  logPath;
3864
4103
  workspaceFolder;
@@ -3909,7 +4148,7 @@ var WorkspaceCommandLogger = class {
3909
4148
  return `[${this.#now().toISOString()}]`;
3910
4149
  }
3911
4150
  #redact(value) {
3912
- return this.#redactions.reduce((current, redaction) => current.replaceAll(redaction, "[redacted]"), value);
4151
+ return this.#redactions.reduce((current, redaction) => current.replaceAll(redaction, "[redacted]"), redactKnownSecretEnvironmentAssignments(value));
3913
4152
  }
3914
4153
  #stream(stream, chunk) {
3915
4154
  const text = this.#redact(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk);
@@ -3992,6 +4231,15 @@ function defaultDataRoot(env = process.env) {
3992
4231
  }
3993
4232
  return join(homedir(), ".local", "share", PACKAGE_NAME);
3994
4233
  }
4234
+ function defaultRuntimeRoot(env = process.env) {
4235
+ if (env.BOXDOWN_RUNTIME_HOME) {
4236
+ return env.BOXDOWN_RUNTIME_HOME;
4237
+ }
4238
+ if (env.XDG_RUNTIME_DIR) {
4239
+ return join(env.XDG_RUNTIME_DIR, PACKAGE_NAME);
4240
+ }
4241
+ return join(tmpdir(), `${PACKAGE_NAME}-${process.getuid?.() ?? "user"}`);
4242
+ }
3995
4243
  function defaultHostAgentsDir(env = process.env) {
3996
4244
  return join(env.HOME ?? homedir(), ".agents");
3997
4245
  }
@@ -4007,10 +4255,12 @@ function createWorkspaceContextFromIdentity(identity, options = {}) {
4007
4255
  const assetsDevcontainerDir = options.assetsDevcontainerDir ?? env.BOXDOWN_DEVCONTAINER_ASSETS_DIR ?? join(packageRoot, "assets", "devcontainer");
4008
4256
  const cacheRoot = defaultCacheRoot(env);
4009
4257
  const dataRoot = defaultDataRoot(env);
4258
+ const runtimeRoot = defaultRuntimeRoot(env);
4010
4259
  const hostAgentsDir = defaultHostAgentsDir(env);
4011
4260
  const hostCodexAuthPath = defaultHostCodexAuthPath(env);
4012
4261
  const workspaceCacheDir = join(cacheRoot, "workspaces", identity.workspaceId);
4013
4262
  const workspaceDataDir = join(dataRoot, "workspaces", identity.workspaceId);
4263
+ const workspaceRuntimeDir = join(runtimeRoot, "workspaces", identity.workspaceId);
4014
4264
  const hostGitconfigSnapshotDir = join(workspaceDataDir, "gitconfig");
4015
4265
  return {
4016
4266
  workspaceFolder: identity.workspaceFolder,
@@ -4020,8 +4270,11 @@ function createWorkspaceContextFromIdentity(identity, options = {}) {
4020
4270
  assetsDevcontainerDir,
4021
4271
  cacheRoot,
4022
4272
  dataRoot,
4273
+ runtimeRoot,
4023
4274
  workspaceCacheDir,
4024
4275
  workspaceDataDir,
4276
+ workspaceRuntimeDir,
4277
+ workspaceSecretEnvDir: join(workspaceRuntimeDir, "secrets"),
4025
4278
  generatedConfigPath: join(workspaceCacheDir, "devcontainer.json"),
4026
4279
  hostAgentsDir,
4027
4280
  hostCodexAuthPath,
@@ -4110,6 +4363,9 @@ function removeWorkspaceStateDir(context, label, path, root) {
4110
4363
  });
4111
4364
  process.stdout.write(`Removed ${label}: ${path}\n`);
4112
4365
  }
4366
+ function removeWorkspaceRuntimeState(context) {
4367
+ removeWorkspaceStateDir(context, "workspace runtime directory", context.workspaceRuntimeDir, context.runtimeRoot);
4368
+ }
4113
4369
  async function purgeAliasIntegrations(context, alias) {
4114
4370
  let failed = false;
4115
4371
  failed = await runPurgeStep(`SSH alias ${alias}`, () => {
@@ -4186,6 +4442,9 @@ async function purgeWorkspace(context, options = {}) {
4186
4442
  await removeDockerImage(removedImageId, { logger: options.logger });
4187
4443
  }) || failed;
4188
4444
  }
4445
+ failed = await runPurgeStep("workspace runtime directory", () => {
4446
+ removeWorkspaceRuntimeState(context);
4447
+ }) || failed;
4189
4448
  failed = await runPurgeStep("workspace cache directory", () => {
4190
4449
  removeWorkspaceStateDir(context, "workspace cache", context.workspaceCacheDir, context.cacheRoot);
4191
4450
  }) || failed;
@@ -4891,6 +5150,7 @@ async function runDownCommand(workspaces, options) {
4891
5150
  const context = createWorkspaceContext({ workspace });
4892
5151
  await runLoggedLifecycle(context, "down", ["down", ...workspace === undefined ? [] : ["--workspace", workspace]], async (logger) => {
4893
5152
  await removeWorkspaceContainer(context, { logger });
5153
+ removeWorkspaceRuntimeState(context);
4894
5154
  });
4895
5155
  } catch (error) {
4896
5156
  failed = true;
@@ -5455,4 +5715,4 @@ async function runCli(argv = process.argv.slice(2), options = {}) {
5455
5715
 
5456
5716
  //#endregion
5457
5717
  export { parseTunnelPortList as a, parseTunnelPort as i, commandWritesWorkspaceMetadata as n, runCli as o, parseCliArgs as r, setupWorkspace as s, USAGE as t };
5458
- //# sourceMappingURL=main-XMBsKjIK.mjs.map
5718
+ //# sourceMappingURL=main-J4_2Up3o.mjs.map