fix: feat: disinto secrets add — store individual encrypted secrets (#31)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/smoke-init removed
ci/woodpecker/pr/smoke-init removed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-03-28 18:48:05 +00:00
parent 6b0e9b5f4d
commit 289f389398
2 changed files with 79 additions and 8 deletions

3
.gitignore vendored
View file

@ -22,3 +22,6 @@ metrics/supervisor-metrics.jsonl
.DS_Store .DS_Store
dev/ci-fixes-*.json dev/ci-fixes-*.json
gardener/dust.jsonl gardener/dust.jsonl
# Individual encrypted secrets (managed by disinto secrets add)
secrets/

View file

@ -2022,7 +2022,78 @@ disinto_secrets() {
fi fi
} }
local secrets_dir="${FACTORY_ROOT}/secrets"
local age_key_file="${HOME}/.config/sops/age/keys.txt"
# Shared helper: ensure age key exists and export AGE_PUBLIC_KEY
_secrets_ensure_age_key() {
if ! command -v age &>/dev/null; then
echo "Error: age is required." >&2
echo " Install age: apt install age / brew install age" >&2
exit 1
fi
if [ ! -f "$age_key_file" ]; then
echo "Error: age key not found at ${age_key_file}" >&2
echo " Run 'disinto init' to generate one, or create manually with:" >&2
echo " mkdir -p ~/.config/sops/age && age-keygen -o ${age_key_file}" >&2
exit 1
fi
AGE_PUBLIC_KEY="$(age-keygen -y "$age_key_file" 2>/dev/null)"
if [ -z "$AGE_PUBLIC_KEY" ]; then
echo "Error: failed to read public key from ${age_key_file}" >&2
exit 1
fi
export AGE_PUBLIC_KEY
}
case "$subcmd" in case "$subcmd" in
add)
local name="${2:-}"
if [ -z "$name" ]; then
echo "Usage: disinto secrets add <NAME>" >&2
exit 1
fi
_secrets_ensure_age_key
mkdir -p "$secrets_dir"
printf 'Enter value for %s: ' "$name" >&2
local value
IFS= read -r value
if [ -z "$value" ]; then
echo "Error: empty value" >&2
exit 1
fi
local enc_path="${secrets_dir}/${name}.enc"
if ! printf '%s' "$value" | age -r "$AGE_PUBLIC_KEY" -o "$enc_path"; then
echo "Error: encryption failed" >&2
exit 1
fi
echo "Stored: ${enc_path}"
;;
show)
local name="${2:-}"
if [ -n "$name" ]; then
# Show individual secret: disinto secrets show <NAME>
local enc_path="${secrets_dir}/${name}.enc"
if [ ! -f "$enc_path" ]; then
echo "Error: ${enc_path} not found" >&2
exit 1
fi
if [ ! -f "$age_key_file" ]; then
echo "Error: age key not found at ${age_key_file}" >&2
exit 1
fi
age -d -i "$age_key_file" "$enc_path"
else
# Show all agent secrets: disinto secrets show
if [ ! -f "$enc_file" ]; then
echo "Error: ${enc_file} not found." >&2
exit 1
fi
sops -d "$enc_file"
fi
;;
edit) edit)
if [ ! -f "$enc_file" ]; then if [ ! -f "$enc_file" ]; then
echo "Error: ${enc_file} not found. Run 'disinto secrets migrate' first." >&2 echo "Error: ${enc_file} not found. Run 'disinto secrets migrate' first." >&2
@ -2030,13 +2101,6 @@ disinto_secrets() {
fi fi
sops "$enc_file" sops "$enc_file"
;; ;;
show)
if [ ! -f "$enc_file" ]; then
echo "Error: ${enc_file} not found." >&2
exit 1
fi
sops -d "$enc_file"
;;
migrate) migrate)
if [ ! -f "$env_file" ]; then if [ ! -f "$env_file" ]; then
echo "Error: ${env_file} not found — nothing to migrate." >&2 echo "Error: ${env_file} not found — nothing to migrate." >&2
@ -2076,9 +2140,13 @@ disinto_secrets() {
cat <<EOF >&2 cat <<EOF >&2
Usage: disinto secrets <subcommand> Usage: disinto secrets <subcommand>
Individual secrets (secrets/<NAME>.enc):
add <NAME> Prompt for value, encrypt, store in secrets/<NAME>.enc
show <NAME> Decrypt and print an individual secret
Agent secrets (.env.enc): Agent secrets (.env.enc):
edit Edit agent secrets (FORGE_TOKEN, CLAUDE_API_KEY, etc.) edit Edit agent secrets (FORGE_TOKEN, CLAUDE_API_KEY, etc.)
show Show decrypted agent secrets show Show decrypted agent secrets (no argument)
migrate Encrypt .env -> .env.enc migrate Encrypt .env -> .env.enc
Vault secrets (.env.vault.enc): Vault secrets (.env.vault.enc):