feat(skills): add self-config skill
safe config editing with secret redaction - config.sh redacted|summary - config-patch.sh start|'... jq patch..'|commit|diff|sort|status|show|rollback|reset|backup - service.sh restart|restart-auto-rollback|confirm
This commit is contained in:
parent
25f26f305b
commit
ac71769b70
4 changed files with 549 additions and 0 deletions
89
unsupported/workspace/skills/self-config/SKILL.md
Normal file
89
unsupported/workspace/skills/self-config/SKILL.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
---
|
||||||
|
name: self-config
|
||||||
|
description: Allows the agent to safely update its own configuration files
|
||||||
|
---
|
||||||
|
|
||||||
|
# Self-Config Skill Capabilities
|
||||||
|
|
||||||
|
1. Presentation of the `config.json` with secrets redacted.
|
||||||
|
2. Staging of a `config.new.json` with utilities for patching, sorting, diffing, and validation.
|
||||||
|
3. Safe service restarts with auto-rollback timers.
|
||||||
|
|
||||||
|
## Tools used
|
||||||
|
- `scripts/config.sh`: Base utility for presenting redacted JSON files.
|
||||||
|
- `scripts/config-patch.sh`: Tools for iterative patching of a staged `config.new.json` file.
|
||||||
|
- `scripts/service.sh`: Manages service restarts with auto-rollback.
|
||||||
|
- `jq`: Used internally for JSON manipulation.
|
||||||
|
|
||||||
|
## View Current Configuration
|
||||||
|
|
||||||
|
### Redacted (Full)
|
||||||
|
```bash
|
||||||
|
scripts/config.sh redacted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Summary (Filtered)
|
||||||
|
Shows only configured models, agents, enabled tools, devices, and heartbeat settings.
|
||||||
|
```bash
|
||||||
|
scripts/config.sh summary
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow: Safe Configuration Update
|
||||||
|
|
||||||
|
### 1. Start a Session
|
||||||
|
Initialize the staging environment. This redacts sensitive keys (tokens, passwords) into a separate hidden file so they aren't exposed in plain text during editing.
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Apply Patches
|
||||||
|
Apply `jq` filters to the **staged** file. You can run this multiple times.
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh '.path.to.key = "new_value"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Review Changes
|
||||||
|
Review the diff between the original and the staged (redacted) version.
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh diff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. View Staged Config
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh config
|
||||||
|
# OR
|
||||||
|
scripts/config-patch.sh summary
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Sort Keys (Optional)
|
||||||
|
Sort the keys in the staged file alphabetically.
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh sort
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Reset (Abort)
|
||||||
|
If you make a mistake *before* switching, clear the staging files.
|
||||||
|
```bash
|
||||||
|
scripts/config-patch.sh reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Switch & Test (The "Hot" Update)
|
||||||
|
Commit the patch, restart the service, and create a timer that will rollback
|
||||||
|
unless 'confirm' action is performed. Default is 120 seconds.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TIMEOUT=300 scripts/service.sh restart-auto-rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8. Confirm
|
||||||
|
If the agent is still working and the changes are correct, confirm the update to remove the rollback marker.
|
||||||
|
```bash
|
||||||
|
scripts/service.sh confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
1. **Always** use `start` before applying patches.
|
||||||
|
2. **Never** manually edit the `.secrets.json` file.
|
||||||
|
3. **Always** confirm your changes within the time limit after a `switch`.
|
||||||
162
unsupported/workspace/skills/self-config/scripts/config-patch.sh
Executable file
162
unsupported/workspace/skills/self-config/scripts/config-patch.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# config-patch: A tool for iterative JSON patching with high-security secret handling.
|
||||||
|
|
||||||
|
# Determine script directory for relocatable operation
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Source shared config and functions
|
||||||
|
source "$SCRIPT_DIR/config.sh"
|
||||||
|
|
||||||
|
COMMAND="${1:-help}"
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
|
||||||
|
start|sort)
|
||||||
|
|
||||||
|
[ -f "$SECRETS_FILE" ] && { echo "Error: Session active. Commit or reset first."; exit 1; }
|
||||||
|
[ ! -f "$PICOCLAW_CONFIG" ] && { echo "Error: $PICOCLAW_CONFIG not found."; exit 1; }
|
||||||
|
|
||||||
|
redact_secrets || exit 1
|
||||||
|
|
||||||
|
# On success - Move results final locations (trap will clean if script crashes before this)
|
||||||
|
# Clear <file>_TMP so trap doesn't try to delete final files
|
||||||
|
|
||||||
|
mv "$CONFIG_TMP" "$STAGING_FILE"
|
||||||
|
CONFIG_TMP=""
|
||||||
|
mv "$SECRETS_TMP" "$SECRETS_FILE"
|
||||||
|
SECRETS_TMP=""
|
||||||
|
|
||||||
|
echo "Start: Redacted staging file created. Secrets mapped to $SECRETS_FILE"
|
||||||
|
|
||||||
|
;;&
|
||||||
|
|
||||||
|
start)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
sort)
|
||||||
|
|
||||||
|
{ rm "$STAGING_FILE" ; jq -S . > "$STAGING_FILE"; } < "$STAGING_FILE"
|
||||||
|
|
||||||
|
echo "Sorted: keys in $STAGING_FILE"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
commit)
|
||||||
|
[ ! -f "$STAGING_FILE" ] && { echo "Error: No staged changes."; exit 1; }
|
||||||
|
[ ! -f "$SECRETS_FILE" ] && { echo "Error: No secrets file found."; exit 1; }
|
||||||
|
|
||||||
|
# 3. Restoration & Validation
|
||||||
|
FINAL_FILE=$(mktemp)
|
||||||
|
|
||||||
|
# Restore secrets: replace path placeholders with stored values
|
||||||
|
# Keys in secrets file are the paths (e.g., "SECRET:agents.defaults.model")
|
||||||
|
jq --slurpfile secrets "$SECRETS_FILE" 'walk(if type == "string" and $secrets[0][.] != null then $secrets[0][.] else . end)' "$STAGING_FILE" > "$FINAL_FILE"
|
||||||
|
|
||||||
|
# Ensure no placeholders survived (means they weren't in the map)
|
||||||
|
# Check if any string key in secrets map still exists in output
|
||||||
|
while IFS= read -r key; do
|
||||||
|
if grep -q "\"$key\"" "$FINAL_FILE"; then
|
||||||
|
echo "Error: Placeholder '$key' was not restored. Check for typos in secrets map."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(jq -r 'keys[]' "$SECRETS_FILE")
|
||||||
|
|
||||||
|
# 4. JSON Integrity Check
|
||||||
|
jq . "$FINAL_FILE" > /dev/null 2>&1 || { echo "Error: Invalid JSON output. Aborting."; exit 1; }
|
||||||
|
|
||||||
|
# 5. Backup & Swap (unredacted config)
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
cp "$PICOCLAW_CONFIG" "$BACKUP_DIR/${BASE}_${TIMESTAMP}.json"
|
||||||
|
|
||||||
|
mv "$FINAL_FILE" "$PICOCLAW_CONFIG"
|
||||||
|
rm -f "$STAGING_FILE" "$SECRETS_FILE"
|
||||||
|
echo "Committed successfully. Backup: $BACKUP_DIR/${BASE}_${TIMESTAMP}.json"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
echo "Target: $PICOCLAW_CONFIG"
|
||||||
|
[ -f "$STAGING_FILE" ] && echo "Staging: Active ($STAGING_FILE)" || echo "Staging: None"
|
||||||
|
[ -f "$SECRETS_FILE" ] && echo "Secrets: $(jq 'keys | length' "$SECRETS_FILE") tracked" || echo "Secrets: None"
|
||||||
|
;;
|
||||||
|
|
||||||
|
summary)
|
||||||
|
# Output a summary of the staged config with only configured models, agents, enabled tools, devices, and heartbeat
|
||||||
|
|
||||||
|
summarize "$STAGING_FILE"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
show|redacted|config)
|
||||||
|
# Output the full staged config
|
||||||
|
|
||||||
|
cat "$STAGING_FILE"
|
||||||
|
;;
|
||||||
|
|
||||||
|
diff)
|
||||||
|
[ ! -f "$STAGING_FILE" ] && { echo "Error: No staged changes to diff."; exit 1; }
|
||||||
|
|
||||||
|
redact_secrets
|
||||||
|
diff -u "$CONFIG_TMP" "$STAGING_FILE"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
rollback)
|
||||||
|
# Restore the most recent backup
|
||||||
|
LATEST=$(ls -t "$BACKUP_DIR"/${BASE}_*.json 2>/dev/null | head -n 1)
|
||||||
|
if [ -z "$LATEST" ]; then
|
||||||
|
echo "Error: No backups found for $PICOCLAW_CONFIG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LATEST_BASE=$(basename "$LATEST" .json)
|
||||||
|
|
||||||
|
cp "$LATEST" "$PICOCLAW_CONFIG"
|
||||||
|
|
||||||
|
echo "Rollback: Restored $PICOCLAW_CONFIG from $LATEST"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
reset)
|
||||||
|
rm -f "$STAGING_FILE" "$SECRETS_FILE"
|
||||||
|
echo "Session cleared."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
help)
|
||||||
|
echo "Usage: $0 <command> [config_file]"
|
||||||
|
echo "Commands:"
|
||||||
|
echo " start - Create staging file with redacted secrets"
|
||||||
|
echo " sort - Sort JSON keys alphabetically"
|
||||||
|
echo " diff - Show staged changes"
|
||||||
|
echo " commit - Apply staged changes to config"
|
||||||
|
echo " reset - Clear staging (discard changes)"
|
||||||
|
echo " rollback - Restore from last backup"
|
||||||
|
echo " status - Show current state"
|
||||||
|
echo " summary - Show summary of the staged config with unused models/chatconfigs filtered"
|
||||||
|
echo " config - Show the staged config"
|
||||||
|
echo " <jq expr> - Apply inline jq patch (e.g. '.agents.model=\"gpt-4\"')"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
# Generic Patching
|
||||||
|
redact_secrets
|
||||||
|
if [ -f "$STAGING_FILE" ]; then
|
||||||
|
SOURCE="$STAGING_FILE"
|
||||||
|
else
|
||||||
|
SOURCE="$PICOCLAW_CONFIG"
|
||||||
|
fi
|
||||||
|
TMP=$(mktemp)
|
||||||
|
if jq "$COMMAND" "$SOURCE" > "$TMP"; then
|
||||||
|
mv "$TMP" "$STAGING_FILE"
|
||||||
|
echo "Applied patch to $STAGING_FILE"
|
||||||
|
else
|
||||||
|
rm -f "$TMP"; exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
157
unsupported/workspace/skills/self-config/scripts/config.sh
Executable file
157
unsupported/workspace/skills/self-config/scripts/config.sh
Executable file
|
|
@ -0,0 +1,157 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# config: A tool for presenting config in insecure channels.
|
||||||
|
|
||||||
|
# Input validation functions
|
||||||
|
assert_is_identifier() {
|
||||||
|
if ! echo "$1" | grep -qE '^[a-zA-Z_][a-zA-Z0-9_-]*$'; then
|
||||||
|
echo "Error: $2" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_is_file_path() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: $2" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local dir
|
||||||
|
dir=$(dirname "$1")
|
||||||
|
if [ ! -d "$dir" ] && [ "$dir" != "." ]; then
|
||||||
|
echo "Error: Directory does not exist: $dir" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_is_safe() {
|
||||||
|
# Only check the command argument, not the entire command line
|
||||||
|
local cmd="$1"
|
||||||
|
if echo "$cmd" | grep -qE '[\$\`\\]'; then
|
||||||
|
echo "Error: $2 - Command contains dangerous characters" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMAND="${1:-help}"
|
||||||
|
|
||||||
|
PICOCLAW_CONFIG="${2:-}"
|
||||||
|
PICOCLAW_CONFIG="${PICOCLAW_CONFIG:-$HOME/.picoclaw/config.json}"
|
||||||
|
|
||||||
|
assert_is_safe "$COMMAND" "Command must be a valid identifier" || exit 1
|
||||||
|
assert_is_file_path "$PICOCLAW_CONFIG" "Config file path must be valid" || exit 1
|
||||||
|
|
||||||
|
# Setup paths
|
||||||
|
BASE=$(basename "$PICOCLAW_CONFIG" .json)
|
||||||
|
DIR=$(dirname "$PICOCLAW_CONFIG")
|
||||||
|
STAGING_FILE="$DIR/$BASE.new.json"
|
||||||
|
SECRETS_FILE="$DIR/.$BASE.secrets.json"
|
||||||
|
BACKUP_DIR="$DIR/.config_backups"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
# Temp file for staging (cleaned up on script exit)
|
||||||
|
CONFIG_TMP=""
|
||||||
|
SECRETS_TMP=""
|
||||||
|
FINAL_FILE=""
|
||||||
|
cleanup() { rm -f "$CONFIG_TMP" "$FINAL_FILE" "$SECRETS_TMP"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
SECRET_PATTERN="key|pass|secret|token|auth|credential|sid|sk-"
|
||||||
|
EXCLUDE_PATTERN="max_tokens|auth_method|enable_auth|auth_type"
|
||||||
|
|
||||||
|
# Shared function: extract secrets if not already done
|
||||||
|
redact_secrets() {
|
||||||
|
|
||||||
|
# Check if original has any secrets
|
||||||
|
paths=$(jq -c "paths(scalars) | select(.[-1] | tostring | ascii_downcase | (test(\"$SECRET_PATTERN\"; \"i\") and (test(\"$EXCLUDE_PATTERN\"; \"i\") | not)))" "$PICOCLAW_CONFIG" 2>/dev/null) || return 0
|
||||||
|
|
||||||
|
# Create staging file in temp (global var for trap cleanup)
|
||||||
|
CONFIG_TMP=$(mktemp)
|
||||||
|
cp "$PICOCLAW_CONFIG" "$CONFIG_TMP"
|
||||||
|
|
||||||
|
# Extract secrets directly to final location
|
||||||
|
SECRETS_TMP=$(mktemp)
|
||||||
|
echo "{}" > "$SECRETS_TMP"
|
||||||
|
chmod 600 "$SECRETS_TMP"
|
||||||
|
|
||||||
|
while read -r p; do
|
||||||
|
[ -z "$p" ] && continue
|
||||||
|
ID="SECRET:$(echo "$p" | jq -r '. | map(tostring) | map(if . | test("^[0-9]+$") then "[" + . + "]" else "." + . end) | join("") | ltrimstr(".")')"
|
||||||
|
VAL=$(jq -r "getpath($p)" "$PICOCLAW_CONFIG")
|
||||||
|
|
||||||
|
# Add to secrets file
|
||||||
|
{ rm -f "$SECRETS_TMP" ; jq --arg id "$ID" --arg val "$VAL" '. + {($id): $val}' > "$SECRETS_TMP"; } < "$SECRETS_TMP"
|
||||||
|
|
||||||
|
# Replace with placeholder in staging
|
||||||
|
{ rm -f "$CONFIG_TMP" ; jq --argjson p "$p" --arg id "$ID" 'setpath($p; $id)' > "${CONFIG_TMP}"; } < "$CONFIG_TMP"
|
||||||
|
|
||||||
|
done <<< "$paths"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Output a summary with only configured models, agents, enabled tools, devices, and heartbeat
|
||||||
|
|
||||||
|
summarize()
|
||||||
|
{
|
||||||
|
jq '
|
||||||
|
def referenced_models:
|
||||||
|
[.. | objects | if has("model") then .model elif has("model_name") then .model_name else null end // empty] | unique;
|
||||||
|
|
||||||
|
def used_model_ids:
|
||||||
|
referenced_models;
|
||||||
|
|
||||||
|
def is_configured:
|
||||||
|
. != null and . != {} and . != [];
|
||||||
|
|
||||||
|
def filter_tools:
|
||||||
|
(walk(
|
||||||
|
if type == "object" then
|
||||||
|
if .enabled == false then
|
||||||
|
null
|
||||||
|
else
|
||||||
|
. as $obj | reduce keys_unsorted[] as $k ({}; . + {($k): ($obj[$k] | filter_tools)})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
.
|
||||||
|
end
|
||||||
|
) | if type == "object" then with_entries(select(.value != null)) else . end);
|
||||||
|
|
||||||
|
. * {
|
||||||
|
model_list: (.model_list // []) | map(select(.model_name as $id | used_model_ids | contains([$id]))),
|
||||||
|
agents: (.agents // {}) | map_values(select(is_configured))
|
||||||
|
} | .channels = ((.channels // {}) | to_entries | map(select(.value.enabled == true)) | from_entries)
|
||||||
|
| .tools = ((.tools // {}) | filter_tools)
|
||||||
|
| .providers = ((.providers // {} | to_entries | map(select(.value.api_key | gsub("^\\s+"; "") | gsub("\\s+$"; "") != "")) | from_entries))
|
||||||
|
| .devices = (if (.devices.enabled // false) then .devices else null end)
|
||||||
|
| .heartbeat = (if (.heartbeat.enabled // false) then .heartbeat else null end)
|
||||||
|
' "${1}" | jq 'with_entries(select(.value != null))'
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
case "$COMMAND" in
|
||||||
|
|
||||||
|
redacted)
|
||||||
|
|
||||||
|
redact_secrets
|
||||||
|
cat "$CONFIG_TMP"
|
||||||
|
;;
|
||||||
|
|
||||||
|
summary)
|
||||||
|
# Output a summary with only configured models, agents, enabled tools, devices, and heartbeat
|
||||||
|
|
||||||
|
redact_secrets
|
||||||
|
summarize "$CONFIG_TMP"
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
help)
|
||||||
|
echo "Usage: $0 <command> [config_file]"
|
||||||
|
echo "Commands:"
|
||||||
|
echo " redacted - Show config (default: PICOCLAW_CONFIG)"
|
||||||
|
echo " summary - Show config with unused models/chatconfigs filtered"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
fi
|
||||||
141
unsupported/workspace/skills/self-config/scripts/service.sh
Normal file
141
unsupported/workspace/skills/self-config/scripts/service.sh
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# service.sh - Service management with auto-rollback support
|
||||||
|
# A "dead man's switch" for configuration changes.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Validate identifier (alphanumeric, dash, underscore)
|
||||||
|
assert_is_identifier() {
|
||||||
|
if ! echo "$1" | grep -qE '^[a-zA-Z_][a-zA-Z0-9_-]*$'; then
|
||||||
|
echo "Error: $2" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_is_number ()
|
||||||
|
{
|
||||||
|
# Use grep for robust, portable POSIX regex matching.
|
||||||
|
if ! echo "$1" | grep -qE '^[0-9]+$';
|
||||||
|
then
|
||||||
|
echo "Error: $2" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVICE_NAME="${PICOCLAW_SERVICE_NAME:-picoclaw}"
|
||||||
|
|
||||||
|
COMMAND="${1:-help}"
|
||||||
|
assert_is_identifier "$COMMAND" "Action must be a valid identifier" || exit 1
|
||||||
|
|
||||||
|
SERVICE="${2:-$SERVICE_NAME}"
|
||||||
|
assert_is_identifier "$SERVICE" "Service name must be a valid identifier" || exit 1
|
||||||
|
|
||||||
|
CONFIG="${3:-${PICOCLAW_CONFIG:-$HOME/.picoclaw/config.json}}"
|
||||||
|
|
||||||
|
[[ -n "$CONFIG" ]] && ! [[ -f "$CONFIG" ]] && echo "Expected file" && exit 1
|
||||||
|
|
||||||
|
case "$COMMAND" in
|
||||||
|
|
||||||
|
restart)
|
||||||
|
|
||||||
|
echo "Restarting service: $SERVICE"
|
||||||
|
systemctl --user restart "$SERVICE"
|
||||||
|
echo "Done."
|
||||||
|
;;
|
||||||
|
|
||||||
|
_rollback)
|
||||||
|
|
||||||
|
# Check if marker file exists (auto-rollback was set)
|
||||||
|
MARKER="${CONFIG}-PENDING-ROLLBACK"
|
||||||
|
|
||||||
|
if [ ! -f "$MARKER" ]; then
|
||||||
|
# No marker = nothing to rollback = success
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Marker exists - perform rollback
|
||||||
|
rm -f "$MARKER"
|
||||||
|
|
||||||
|
"$SCRIPT_DIR/config.sh" rollback "$CONFIG"
|
||||||
|
|
||||||
|
echo "[ROLLBACK] Restarting service: $SERVICE"
|
||||||
|
systemctl --user restart "$SERVICE" || true
|
||||||
|
echo "Rollback complete."
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
restart-auto-rollback)
|
||||||
|
|
||||||
|
TIMEOUT="${TIMEOUT:-}"
|
||||||
|
TIMEOUT="${4:-120}"
|
||||||
|
assert_is_number "$TIMEOUT" "Timer must be number of seconds" || exit 1
|
||||||
|
|
||||||
|
# Create marker file
|
||||||
|
MARKER="${CONFIG}-PENDING-ROLLBACK"
|
||||||
|
|
||||||
|
# Restart service first
|
||||||
|
echo "Restarting service: $SERVICE"
|
||||||
|
systemctl --user restart "$SERVICE"
|
||||||
|
|
||||||
|
echo "Auto-rollback armed for $SERVICE (timeout: ${TIMEOUT}s)"
|
||||||
|
echo "Marker: $MARKER"
|
||||||
|
|
||||||
|
# Start background timer
|
||||||
|
(
|
||||||
|
sleep "$TIMEOUT"
|
||||||
|
if [ -f "$MARKER" ]; then
|
||||||
|
echo "[TIMEOUT] Auto-rollback triggered for $SERVICE"
|
||||||
|
bash "$0" _rollback "$SERVICE" "$CONFIG"
|
||||||
|
fi
|
||||||
|
) &
|
||||||
|
TIMER_PID=$!
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "Timer PID: $TIMER_PID"
|
||||||
|
echo "----------------------------------------------------"
|
||||||
|
echo "Service will ROLLBACK in $TIMEOUT seconds if not confirmed."
|
||||||
|
echo "To confirm: service.sh confirm $SERVICE"
|
||||||
|
echo "----------------------------------------------------"
|
||||||
|
} | tee "$MARKER"
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
confirm)
|
||||||
|
|
||||||
|
MARKER="${CONFIG}-PENDING-ROLLBACK"
|
||||||
|
|
||||||
|
if [ -n "$MARKER" ] && [ -f "$MARKER" ]; then
|
||||||
|
rm -f "$MARKER"
|
||||||
|
echo "Rollback cancelled. Changes confirmed."
|
||||||
|
else
|
||||||
|
echo "No pending rollback to confirm."
|
||||||
|
fi
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
help)
|
||||||
|
echo "Usage: $0 <command> [args...]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " restart [service] Restart service (default: \$PICOCLAW_SERVICE_NAME or picoclaw)"
|
||||||
|
echo " auto-rollback [service] [config] [timeout]"
|
||||||
|
echo " Arm rollback timer, restart service"
|
||||||
|
echo " confirm [service] [config] Cancel pending rollback"
|
||||||
|
echo " _rollback [service] [config]"
|
||||||
|
echo " Internal: check marker, rollback if present"
|
||||||
|
echo ""
|
||||||
|
echo "Environment:"
|
||||||
|
echo " PICOCLAW_SERVICE_NAME Default service name (default: picoclaw)"
|
||||||
|
echo " PICOCLAW_CONFIG Default config file (default: picoclaw)"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $COMMAND"
|
||||||
|
echo "Run '$0 help' for usage."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Add table
Reference in a new issue