boxdown 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/assets/devcontainer/README.md +29 -6
- package/assets/devcontainer/devcontainer.json +4 -9
- package/assets/devcontainer/hooks/initialize.sh +49 -27
- package/assets/devcontainer/hooks/post-create.sh +23 -1
- package/assets/devcontainer/hooks/post-start.sh +0 -10
- package/assets/devcontainer/utils/git-signing-bootstrap.sh +58 -5
- package/assets/devcontainer/utils/secret-env-bootstrap.sh +22 -0
- package/assets/devcontainer/utils/ssh-agent-proxy-bootstrap.sh +27 -0
- package/assets/devcontainer/utils/ssh-agent-proxy.mjs +39 -0
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/{main-Df4E8ARj.cjs → main-CT2n9qcb.cjs} +907 -266
- package/dist/{main-XMBsKjIK.mjs → main-O__JaiXe.mjs} +891 -268
- package/dist/main-O__JaiXe.mjs.map +1 -0
- package/dist/main.cjs +4 -1
- package/dist/main.d.cts +98 -26
- package/dist/main.d.cts.map +1 -1
- package/dist/main.d.mts +98 -26
- package/dist/main.d.mts.map +1 -1
- package/dist/main.mjs +2 -2
- package/docs/features/commit-signing.md +71 -0
- package/docs/features/generated-config-and-state.md +19 -4
- package/docs/features/github-auth-refresh.md +3 -2
- package/docs/features/lifecycle.md +19 -0
- package/docs/features/setup.md +5 -0
- package/docs/features/start-and-shell.md +10 -0
- package/docs/superpowers/plans/2026-07-17-node-ssh-agent-proxy-signing.md +132 -0
- package/docs/superpowers/plans/2026-07-17-runtime-secret-environment.md +174 -0
- package/docs/superpowers/plans/2026-07-18-architecture-aware-1password-installer.md +163 -0
- package/docs/superpowers/plans/2026-07-18-devcontainer-node-image-digest.md +341 -0
- package/docs/superpowers/plans/2026-07-19-container-runtime-readiness.md +1536 -0
- package/docs/superpowers/specs/2026-07-11-default-commit-signing-design.md +20 -0
- package/docs/superpowers/specs/2026-07-17-node-ssh-agent-proxy-design.md +63 -0
- package/docs/superpowers/specs/2026-07-17-runtime-secret-environment-design.md +194 -0
- package/docs/superpowers/specs/2026-07-18-architecture-aware-1password-installer-design.md +63 -0
- package/docs/superpowers/specs/2026-07-18-devcontainer-node-image-digest-design.md +108 -0
- package/docs/superpowers/specs/2026-07-19-container-runtime-readiness-design.md +353 -0
- package/package.json +1 -1
- package/src/config.ts +14 -2
- package/src/constants.ts +7 -0
- package/src/container-runtime.ts +295 -0
- package/src/devcontainer.ts +57 -30
- package/src/doctor.ts +169 -46
- package/src/git-signing.ts +205 -25
- package/src/logging.ts +11 -1
- package/src/main.ts +97 -12
- package/src/paths.ts +21 -1
- package/src/process.ts +50 -10
- package/src/progress.ts +63 -8
- package/src/purge.ts +8 -0
- package/src/shell.ts +6 -0
- package/dist/main-XMBsKjIK.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -169,6 +169,16 @@ no-start bind-mount probe for the workspace and Boxdown-managed mount paths.
|
|
|
169
169
|
Run `boxdown doctor` directly for the complete diagnostic report; an unavailable
|
|
170
170
|
best-effort mount probe is reported as a warning and does not block setup.
|
|
171
171
|
|
|
172
|
+
`boxdown start` is standalone: it can create or reuse the devcontainer even if
|
|
173
|
+
`boxdown setup` was skipped or its preflight failed. Setup-only SSH aliases and
|
|
174
|
+
Codex/Claude application integrations are still installed only by `setup`.
|
|
175
|
+
|
|
176
|
+
Before a command creates or starts a container, Boxdown waits up to 60 seconds
|
|
177
|
+
for the Docker daemon and the selected Docker Buildx builder. If Buildx is not
|
|
178
|
+
installed, the bundled Dev Containers CLI uses its supported classic-build
|
|
179
|
+
fallback and Boxdown continues with a warning. Boxdown does not retry an actual
|
|
180
|
+
Dev Containers build failure.
|
|
181
|
+
|
|
172
182
|
Container bring-up installs Codex and Claude Code by default. The OpenCode and
|
|
173
183
|
Antigravity commands stay available, but install/update those CLIs only when you
|
|
174
184
|
launch them. Use `--` to pass arguments to the selected agent:
|
|
@@ -17,7 +17,7 @@ Run this project in a **consistent Node.js 24 + TypeScript** environment without
|
|
|
17
17
|
| `devcontainer.json` | Slim Node base image, pinned feature set/order, Boxdown state mounts, lifecycle commands, env forwarding. |
|
|
18
18
|
| `start.sh` | Brings the dev container up with the Dev Containers CLI, then opens a shell **inside** the container or acts as an SSH `ProxyCommand`. |
|
|
19
19
|
| `ssh-config-install.sh` | Installs/updates a host SSH config alias for Cursor, Claude, or plain `ssh`. |
|
|
20
|
-
| `hooks/initialize.sh` | Runs on the host before container create/start;
|
|
20
|
+
| `hooks/initialize.sh` | Runs on the host before container create/start; refreshes private runtime secret files and host Git state. |
|
|
21
21
|
| `hooks/post-create.sh` | Runs once after the container is created — e.g. installs OpenSSH server, Debian Python, [APM](https://github.com/microsoft/apm) (Agent Package Manager), and default coding-agent CLIs. |
|
|
22
22
|
| `hooks/post-start.sh` | Runs on each container start; refreshes runtime state such as SSH host keys and authorized keys. |
|
|
23
23
|
| `utils/git-config-bootstrap.sh` | Container-side Git config copy/sanitization helper used by lifecycle scripts. |
|
|
@@ -28,7 +28,18 @@ Run this project in a **consistent Node.js 24 + TypeScript** environment without
|
|
|
28
28
|
|
|
29
29
|
## Base image
|
|
30
30
|
|
|
31
|
-
Boxdown uses `node:24-trixie-slim` as the base
|
|
31
|
+
Boxdown uses `node:24-trixie-slim` as the base-image update track to keep the
|
|
32
|
+
shared image smaller than the full Dev Containers TypeScript/Node image. The
|
|
33
|
+
template appends the upstream multi-platform OCI index digest, making rebuilds
|
|
34
|
+
immutable while allowing AMD64 and ARM64 hosts to select the matching platform
|
|
35
|
+
manifest from the same pinned release set. Renovate checks the packaged
|
|
36
|
+
template monthly and opens a pull request when that tag's index digest changes,
|
|
37
|
+
so base-image updates remain explicit and auditable.
|
|
38
|
+
|
|
39
|
+
The devcontainer then installs the required operating-system tools through
|
|
40
|
+
pinned Dev Container features. `common-utils` and `git` run first so later
|
|
41
|
+
features and lifecycle hooks can rely on shell basics, `sudo`, package
|
|
42
|
+
metadata, Git, and related utilities.
|
|
32
43
|
|
|
33
44
|
Python is installed during `postCreateCommand` from Debian apt packages (`python3`, `python3-venv`, `python3-pip`, and `pipx`). On Debian trixie this currently provides Python 3.13. Boxdown intentionally avoids the Dev Containers Python feature because it added a large Python runtime/dev-tooling layer, including bundled environments for tools such as mypy, black, pylint, pytest, bandit, pipenv, and flake8.
|
|
34
45
|
|
|
@@ -175,10 +186,22 @@ export ANTHROPIC_API_KEY=sk-...
|
|
|
175
186
|
export SNYK_TOKEN=...
|
|
176
187
|
```
|
|
177
188
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
Boxdown writes available values to owner-only files in per-workspace runtime
|
|
190
|
+
state outside the repository, then mounts that directory read-only. Bash
|
|
191
|
+
sessions export the values as ordinary environment variables, but Docker
|
|
192
|
+
container configuration and `docker inspect` do not contain their values.
|
|
193
|
+
|
|
194
|
+
When the host `op` CLI can read Boxdown's configured service-account item,
|
|
195
|
+
Boxdown provides `OP_SERVICE_ACCOUNT_TOKEN` through the same runtime mount. A
|
|
196
|
+
missing host value or failed 1Password lookup is non-blocking; the variable is
|
|
197
|
+
simply absent. This token is not a GitHub token and does not authenticate `gh`
|
|
198
|
+
or GitHub Git remotes.
|
|
199
|
+
|
|
200
|
+
Boxdown never creates, modifies, reads, or deletes a project
|
|
201
|
+
`.env.development` file. Existing containers created by an older Boxdown need
|
|
202
|
+
`boxdown start --recreate` to stop receiving legacy Docker environment values.
|
|
203
|
+
If an older version left a service-account token in `.env.development` or
|
|
204
|
+
`boxdown.log`, remove it manually and rotate the token.
|
|
182
205
|
|
|
183
206
|
## Optional customization
|
|
184
207
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Project Devcontainer",
|
|
3
|
-
"image": "node:24-trixie-slim",
|
|
3
|
+
"image": "node:24-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573",
|
|
4
4
|
"features": {
|
|
5
5
|
"ghcr.io/devcontainers/features/common-utils@sha256:cb0c4d3c276f157eed17935747e364178d75fee17f55c4e129966f64633deb3a": {
|
|
6
6
|
"username": "node"
|
|
@@ -75,17 +75,14 @@
|
|
|
75
75
|
// Codex auth.json is mounted automatically by Boxdown's generated config when present.
|
|
76
76
|
],
|
|
77
77
|
|
|
78
|
-
// Initialize
|
|
78
|
+
// Initialize host runtime state before container create/start.
|
|
79
79
|
"initializeCommand": "bash .devcontainer/hooks/initialize.sh",
|
|
80
80
|
// After up, discover the published host binding, e.g.:
|
|
81
81
|
// docker port <container-name-or-id> 3000/tcp
|
|
82
82
|
"runArgs": [
|
|
83
83
|
// publish container port 3000 with dynamic host port (CLI / start.sh)
|
|
84
84
|
"-p",
|
|
85
|
-
"127.0.0.1::3000"
|
|
86
|
-
// pass the development environment file to the container
|
|
87
|
-
"--env-file",
|
|
88
|
-
"${localWorkspaceFolder}/.env.development"
|
|
85
|
+
"127.0.0.1::3000"
|
|
89
86
|
],
|
|
90
87
|
// --
|
|
91
88
|
"postCreateCommand": "bash .devcontainer/hooks/post-create.sh",
|
|
@@ -93,8 +90,6 @@
|
|
|
93
90
|
"remoteUser": "node",
|
|
94
91
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
|
95
92
|
"containerEnv": {
|
|
96
|
-
"NODE_ENV": "development"
|
|
97
|
-
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
|
|
98
|
-
"SNYK_TOKEN": "${localEnv:SNYK_TOKEN}"
|
|
93
|
+
"NODE_ENV": "development"
|
|
99
94
|
}
|
|
100
95
|
}
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# initialize: runs on host before container create/start (initializeCommand).
|
|
3
|
-
#
|
|
3
|
+
# Refreshes private runtime secret files and snapshots host Git config.
|
|
4
4
|
|
|
5
5
|
set -euo pipefail
|
|
6
6
|
|
|
7
|
-
HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
-
WORKSPACE_DIR="${BOXDOWN_WORKSPACE_FOLDER:-$(cd "${HOOKS_DIR}/../.." && pwd)}"
|
|
9
|
-
ENV_FILE="${WORKSPACE_DIR}/.env.development"
|
|
10
7
|
HOST_GITCONFIG_PATH="${BOXDOWN_HOST_GITCONFIG_PATH:-${HOME:-}/.gitconfig}"
|
|
11
8
|
HOST_GITCONFIG_SNAPSHOT_PATH="${BOXDOWN_HOST_GITCONFIG_SNAPSHOT_PATH:-}"
|
|
9
|
+
SECRET_ENV_DIR="${BOXDOWN_SECRET_ENV_DIR:-}"
|
|
12
10
|
OP_TOKEN_REFERENCE="op://Private/1Password op CLI Service Account for DevContainers/password"
|
|
13
11
|
|
|
14
12
|
main() {
|
|
15
|
-
progress "Preparing workspace environment file"
|
|
16
|
-
ensure_env_file_exists
|
|
17
13
|
progress "Snapshotting host Git config"
|
|
18
14
|
snapshot_host_gitconfig
|
|
19
|
-
progress "
|
|
20
|
-
|
|
15
|
+
progress "Refreshing runtime secret environment"
|
|
16
|
+
refresh_runtime_secret_environment
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
progress() {
|
|
@@ -26,12 +22,6 @@ progress() {
|
|
|
26
22
|
fi
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
ensure_env_file_exists() {
|
|
30
|
-
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
31
|
-
: > "${ENV_FILE}"
|
|
32
|
-
fi
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
snapshot_host_gitconfig() {
|
|
36
26
|
local snapshot_dir
|
|
37
27
|
|
|
@@ -52,36 +42,68 @@ snapshot_host_gitconfig() {
|
|
|
52
42
|
rm -f "${HOST_GITCONFIG_SNAPSHOT_PATH}"
|
|
53
43
|
}
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
prepare_secret_env_dir() {
|
|
46
|
+
if [[ -z "${SECRET_ENV_DIR}" ]]; then
|
|
47
|
+
echo "initialize.sh: runtime secret directory is not configured; skipping optional secrets." >&2
|
|
48
|
+
return 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
umask 077
|
|
52
|
+
mkdir -p "${SECRET_ENV_DIR}"
|
|
53
|
+
chmod 0700 "${SECRET_ENV_DIR}"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
write_secret_file() {
|
|
57
|
+
local name="$1"
|
|
58
|
+
local value="$2"
|
|
59
|
+
local temporary_path
|
|
60
|
+
|
|
61
|
+
temporary_path="$(mktemp "${SECRET_ENV_DIR}/.${name}.XXXXXX")"
|
|
62
|
+
printf '%s' "${value}" > "${temporary_path}"
|
|
63
|
+
chmod 0600 "${temporary_path}"
|
|
64
|
+
mv -f "${temporary_path}" "${SECRET_ENV_DIR}/${name}"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
refresh_host_environment_secret() {
|
|
68
|
+
local name="$1"
|
|
69
|
+
local value="${!name:-}"
|
|
70
|
+
|
|
71
|
+
if [[ -n "${value}" ]]; then
|
|
72
|
+
write_secret_file "${name}" "${value}"
|
|
73
|
+
else
|
|
74
|
+
rm -f "${SECRET_ENV_DIR}/${name}"
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
refresh_1password_service_account_token() {
|
|
56
79
|
local token
|
|
57
80
|
|
|
58
81
|
if ! command -v op >/dev/null 2>&1; then
|
|
59
|
-
|
|
82
|
+
rm -f "${SECRET_ENV_DIR}/OP_SERVICE_ACCOUNT_TOKEN"
|
|
60
83
|
return 0
|
|
61
84
|
fi
|
|
62
85
|
|
|
63
86
|
if ! token="$(op read "${OP_TOKEN_REFERENCE}" 2>/dev/null)"; then
|
|
64
|
-
|
|
87
|
+
rm -f "${SECRET_ENV_DIR}/OP_SERVICE_ACCOUNT_TOKEN"
|
|
65
88
|
return 0
|
|
66
89
|
fi
|
|
67
90
|
|
|
68
91
|
if [[ -z "${token}" ]]; then
|
|
69
|
-
|
|
92
|
+
rm -f "${SECRET_ENV_DIR}/OP_SERVICE_ACCOUNT_TOKEN"
|
|
70
93
|
return 0
|
|
71
94
|
fi
|
|
72
95
|
|
|
73
|
-
|
|
96
|
+
write_secret_file "OP_SERVICE_ACCOUNT_TOKEN" "${token}"
|
|
74
97
|
}
|
|
75
98
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
refresh_runtime_secret_environment() {
|
|
100
|
+
if ! prepare_secret_env_dir; then
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
mv "${tmp_file}" "${ENV_FILE}"
|
|
104
|
+
refresh_host_environment_secret "ANTHROPIC_API_KEY"
|
|
105
|
+
refresh_host_environment_secret "SNYK_TOKEN"
|
|
106
|
+
refresh_1password_service_account_token
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
main "$@"
|
|
@@ -8,6 +8,7 @@ main() {
|
|
|
8
8
|
run_step "Configuring global Git" configure_global_git
|
|
9
9
|
run_step "Configuring Git commit signing" configure_git_signing
|
|
10
10
|
run_step "Configuring workspace Git" configure_local_git
|
|
11
|
+
run_step "Configuring runtime secret environment" configure_runtime_secret_environment
|
|
11
12
|
run_step "Installing OpenSSH server" install_openssh_server
|
|
12
13
|
run_step "Installing Python runtime" install_python_runtime
|
|
13
14
|
run_step "Installing Agent Package Manager" install_apm
|
|
@@ -49,6 +50,16 @@ configure_local_git() {
|
|
|
49
50
|
fi
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
configure_runtime_secret_environment() {
|
|
54
|
+
local bashrc="${HOME}/.bashrc"
|
|
55
|
+
local source_line='source /opt/boxdown/devcontainer/utils/secret-env-bootstrap.sh'
|
|
56
|
+
|
|
57
|
+
touch "${bashrc}"
|
|
58
|
+
if ! grep -Fqx "${source_line}" "${bashrc}"; then
|
|
59
|
+
printf '%s\n' "${source_line}" >> "${bashrc}"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
install_apm() {
|
|
53
64
|
# Agent Package Manager: https://github.com/microsoft/apm
|
|
54
65
|
local installer
|
|
@@ -100,7 +111,18 @@ install_snyk_cli() {
|
|
|
100
111
|
|
|
101
112
|
install_1password_cli() {
|
|
102
113
|
local op_version="2.32.1"
|
|
103
|
-
|
|
114
|
+
local op_arch
|
|
115
|
+
|
|
116
|
+
case "$(uname -m)" in
|
|
117
|
+
aarch64 | arm64) op_arch="arm64" ;;
|
|
118
|
+
x86_64 | amd64) op_arch="amd64" ;;
|
|
119
|
+
*)
|
|
120
|
+
echo "post-create: skipping 1Password CLI (unsupported arch: $(uname -m))" >&2
|
|
121
|
+
return 0
|
|
122
|
+
;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
curl -fsSL "https://cache.agilebits.com/dist/1P/op2/pkg/v${op_version}/op_linux_${op_arch}_v${op_version}.zip" -o /tmp/op.zip
|
|
104
126
|
python3 -c "import zipfile; zipfile.ZipFile('/tmp/op.zip').extract('op', '/tmp')"
|
|
105
127
|
sudo mv /tmp/op /usr/local/bin/op && chmod +x /usr/local/bin/op && rm /tmp/op.zip
|
|
106
128
|
}
|
|
@@ -13,7 +13,6 @@ DEVCONTAINER_DIR="$(cd "${HOOKS_DIR}/.." && pwd)"
|
|
|
13
13
|
main() {
|
|
14
14
|
run_step "Preparing SSH runtime" configure_sshd_runtime
|
|
15
15
|
run_step "Refreshing coding-agent CLIs" refresh_coding_agent_clis
|
|
16
|
-
run_step "Cleaning ephemeral environment file" remove_ephemeral_env_file_if_present
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
progress() {
|
|
@@ -39,13 +38,4 @@ refresh_coding_agent_clis() {
|
|
|
39
38
|
echo "post-start: warning: one or more coding-agent CLI refreshes failed." >&2
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
# When initializeCommand + runArgs inject secrets via .env.development, remove the file
|
|
43
|
-
# after start so it is not left on disk and tooling that assumes absence does not break.
|
|
44
|
-
remove_ephemeral_env_file_if_present() {
|
|
45
|
-
local env_file=".env.development"
|
|
46
|
-
if [[ -f "$env_file" ]]; then
|
|
47
|
-
rm -f "$env_file"
|
|
48
|
-
fi
|
|
49
|
-
}
|
|
50
|
-
|
|
51
41
|
main "$@"
|
|
@@ -6,22 +6,75 @@ set -euo pipefail
|
|
|
6
6
|
TARGET_PATH="${BOXDOWN_GITCONFIG_TARGET_PATH:-/home/node/.gitconfig}"
|
|
7
7
|
KEY_PATH="${BOXDOWN_GIT_SIGNING_KEY_PATH:-/opt/boxdown/state/git-signing/signing-key.pub}"
|
|
8
8
|
ENABLED="${BOXDOWN_GIT_SIGNING_ENABLED:-0}"
|
|
9
|
+
HOST_REASON="${BOXDOWN_GIT_SIGNING_REASON:-host-preflight-unavailable}"
|
|
9
10
|
|
|
10
11
|
git_global() {
|
|
11
12
|
GIT_CONFIG_GLOBAL="${TARGET_PATH}" git config --global "$@"
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
git_local_value() {
|
|
16
|
+
git config --local --get "$1" 2>/dev/null || true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
git_global_value() {
|
|
20
|
+
git_global --get "$1" 2>/dev/null || true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
preserve_user_signing_preference() {
|
|
24
|
+
local local_commit global_commit local_format global_format local_program global_program
|
|
25
|
+
local_commit="$(git_local_value commit.gpgsign)"
|
|
26
|
+
global_commit="$(git_global_value commit.gpgsign)"
|
|
27
|
+
local_format="$(git_local_value gpg.format)"
|
|
28
|
+
global_format="$(git_global_value gpg.format)"
|
|
29
|
+
local_program="$(git_local_value gpg.program)"
|
|
30
|
+
global_program="$(git_global_value gpg.program)"
|
|
31
|
+
|
|
32
|
+
[[ "${local_commit}" == "false" || "${global_commit}" == "false" ]] && return 0
|
|
33
|
+
[[ -n "${local_program}" || -n "${global_program}" ]] && return 0
|
|
34
|
+
[[ -n "${local_format}" && "${local_format}" != "ssh" ]] && return 0
|
|
35
|
+
[[ -n "${global_format}" && "${global_format}" != "ssh" ]] && return 0
|
|
36
|
+
return 1
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
disable_signing() {
|
|
40
|
+
local reason="${1:-unknown}"
|
|
41
|
+
if preserve_user_signing_preference; then
|
|
42
|
+
printf 'boxdown: commit signing configuration preserved (reason: %s).\n' "${reason}" >&2
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
15
45
|
git_global --unset-all gpg.format >/dev/null 2>&1 || true
|
|
16
46
|
git_global --unset-all user.signingkey >/dev/null 2>&1 || true
|
|
17
47
|
git_global --unset-all gpg.program >/dev/null 2>&1 || true
|
|
18
48
|
git_global --replace-all commit.gpgsign false
|
|
19
|
-
printf '
|
|
49
|
+
printf 'boxdown: commit signing unavailable (reason: %s); commits will remain unsigned.\n' "${reason}" >&2
|
|
20
50
|
}
|
|
21
51
|
|
|
22
52
|
enable_signing() {
|
|
23
|
-
if
|
|
24
|
-
|
|
53
|
+
if preserve_user_signing_preference; then
|
|
54
|
+
printf '%s\n' 'boxdown: preserving explicit user Git signing configuration.' >&2
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
if [[ ! -r "${KEY_PATH}" ]]; then
|
|
59
|
+
disable_signing 'container-key-unavailable'
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
if ! bash "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ssh-agent-proxy-bootstrap.sh"; then
|
|
64
|
+
disable_signing 'container-agent-proxy-unavailable'
|
|
65
|
+
return 0
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
local agent_identities
|
|
69
|
+
if ! agent_identities="$(ssh-add -L 2>/dev/null)"; then
|
|
70
|
+
disable_signing 'container-agent-unavailable'
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
local selected_key
|
|
75
|
+
selected_key="$(awk 'NR == 1 { print $1 " " $2 }' "${KEY_PATH}")"
|
|
76
|
+
if ! grep -qF -- "${selected_key}" <<<"${agent_identities}"; then
|
|
77
|
+
disable_signing 'container-key-not-loaded'
|
|
25
78
|
return 0
|
|
26
79
|
fi
|
|
27
80
|
|
|
@@ -43,7 +96,7 @@ enable_signing() {
|
|
|
43
96
|
git commit --allow-empty -m 'boxdown signing probe' >/dev/null
|
|
44
97
|
); then
|
|
45
98
|
rm -rf "${probe_dir}"
|
|
46
|
-
disable_signing
|
|
99
|
+
disable_signing 'container-signing-probe-failed'
|
|
47
100
|
return 0
|
|
48
101
|
fi
|
|
49
102
|
rm -rf "${probe_dir}"
|
|
@@ -52,5 +105,5 @@ enable_signing() {
|
|
|
52
105
|
if [[ "${ENABLED}" == "1" ]]; then
|
|
53
106
|
enable_signing
|
|
54
107
|
else
|
|
55
|
-
disable_signing
|
|
108
|
+
disable_signing "${HOST_REASON}"
|
|
56
109
|
fi
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shell bootstrap for Boxdown runtime-mounted secret environment files.
|
|
3
|
+
|
|
4
|
+
SECRET_ENV_DIR="${BOXDOWN_SECRET_ENV_DIR:-/run/boxdown/secrets}"
|
|
5
|
+
|
|
6
|
+
export_if_present() {
|
|
7
|
+
local name="$1"
|
|
8
|
+
local path="${SECRET_ENV_DIR}/${name}"
|
|
9
|
+
local value
|
|
10
|
+
|
|
11
|
+
[[ -r "${path}" ]] || return 0
|
|
12
|
+
IFS= read -r value < "${path}" || true
|
|
13
|
+
|
|
14
|
+
if [[ -n "${value}" ]]; then
|
|
15
|
+
export "${name}=${value}"
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export_if_present ANTHROPIC_API_KEY
|
|
20
|
+
export_if_present SNYK_TOKEN
|
|
21
|
+
export_if_present OP_SERVICE_ACCOUNT_TOKEN
|
|
22
|
+
unset value
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Expose a root-only forwarded SSH-agent socket to the non-root container user.
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SOURCE_SOCKET="${BOXDOWN_GIT_SIGNING_SOURCE_SOCKET:-/run/boxdown/ssh-agent.sock}"
|
|
7
|
+
TARGET_SOCKET="${SSH_AUTH_SOCK:-/run/boxdown/ssh-agent-node.sock}"
|
|
8
|
+
PROXY_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ssh-agent-proxy.mjs"
|
|
9
|
+
|
|
10
|
+
if ssh-add -L >/dev/null 2>&1; then
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
if ! command -v sudo >/dev/null 2>&1 || ! command -v node >/dev/null 2>&1; then
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
sudo -n node "${PROXY_SCRIPT}" --source "${SOURCE_SOCKET}" --target "${TARGET_SOCKET}" --uid "$(id -u)" --gid "$(id -g)" >/dev/null 2>&1 &
|
|
19
|
+
|
|
20
|
+
for _ in $(seq 1 20); do
|
|
21
|
+
if [[ -S "${TARGET_SOCKET}" ]] && ssh-add -L >/dev/null 2>&1; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
sleep 0.1
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
exit 1
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chmodSync, chownSync, existsSync, unlinkSync } from 'node:fs'
|
|
4
|
+
import { createConnection, createServer } from 'node:net'
|
|
5
|
+
|
|
6
|
+
function option (name) {
|
|
7
|
+
const index = process.argv.indexOf(name)
|
|
8
|
+
return index === -1 ? undefined : process.argv[index + 1]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const source = option('--source')
|
|
12
|
+
const target = option('--target')
|
|
13
|
+
const uid = Number(option('--uid'))
|
|
14
|
+
const gid = Number(option('--gid'))
|
|
15
|
+
|
|
16
|
+
if (source === undefined || target === undefined || !Number.isInteger(uid) || !Number.isInteger(gid)) {
|
|
17
|
+
process.stderr.write('ssh-agent-proxy: expected --source, --target, --uid, and --gid.\n')
|
|
18
|
+
process.exit(2)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (existsSync(target)) unlinkSync(target)
|
|
22
|
+
|
|
23
|
+
const server = createServer((client) => {
|
|
24
|
+
const upstream = createConnection(source)
|
|
25
|
+
client.pipe(upstream)
|
|
26
|
+
upstream.pipe(client)
|
|
27
|
+
client.on('error', () => upstream.destroy())
|
|
28
|
+
upstream.on('error', () => client.destroy())
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
server.on('error', (error) => {
|
|
32
|
+
process.stderr.write(`ssh-agent-proxy: ${error.message}\n`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
server.listen(target, () => {
|
|
37
|
+
chmodSync(target, 0o600)
|
|
38
|
+
chownSync(target, uid, gid)
|
|
39
|
+
})
|
package/dist/bin/cli.cjs
CHANGED
package/dist/bin/cli.mjs
CHANGED