fix: eliminate shell injection vectors in deploy/research/state scripts
- lib-state.sh: all 7 functions now use os.environ instead of string interpolation - deploy.sh: syntax checker uses sys.argv[1] instead of '$f' interpolation - research-session.sh: per-command auth header instead of credential helper, tweet parsers use sys.argv instead of '$OUTFILE' interpolation - state_end_session: now writes pr_number to session JSON via env var Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05d74d5e32
commit
c5deadb546
3 changed files with 100 additions and 78 deletions
|
|
@ -14,15 +14,6 @@ _state_dir() {
|
||||||
echo "$STATE_ROOT/$agent"
|
echo "$STATE_ROOT/$agent"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Atomic write: write to tmp file, then rename. Prevents partial reads.
|
|
||||||
_atomic_write() {
|
|
||||||
local filepath="$1"
|
|
||||||
local content="$2"
|
|
||||||
local tmpfile="${filepath}.tmp.$$"
|
|
||||||
echo "$content" > "$tmpfile"
|
|
||||||
mv -f "$tmpfile" "$filepath"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Report (current status) ---
|
# --- Report (current status) ---
|
||||||
|
|
||||||
state_read_report() {
|
state_read_report() {
|
||||||
|
|
@ -37,17 +28,18 @@ state_update_report() {
|
||||||
local summary="$3"
|
local summary="$3"
|
||||||
local file="$(_state_dir "$agent")/report.json"
|
local file="$(_state_dir "$agent")/report.json"
|
||||||
|
|
||||||
# Read existing, merge with updates using python (available on VPS)
|
_STATE_FILE="$file" _STATE_AGENT="$agent" _STATE_STATUS="$status" \
|
||||||
|
_STATE_SUMMARY="$summary" _STATE_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json, sys
|
import json, os
|
||||||
try:
|
try:
|
||||||
with open('$file') as f:
|
with open(os.environ['_STATE_FILE']) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except:
|
except:
|
||||||
data = {'agent': '$agent'}
|
data = {'agent': os.environ['_STATE_AGENT']}
|
||||||
data['status'] = '$status'
|
data['status'] = os.environ['_STATE_STATUS']
|
||||||
data['summary'] = '''$summary'''
|
data['summary'] = os.environ['_STATE_SUMMARY']
|
||||||
data['updated_at'] = '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
|
data['updated_at'] = os.environ['_STATE_TS']
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
" | _atomic_write_stdin "$file"
|
" | _atomic_write_stdin "$file"
|
||||||
}
|
}
|
||||||
|
|
@ -75,25 +67,35 @@ state_finalize_report() {
|
||||||
local next_priority="${11:-null}"
|
local next_priority="${11:-null}"
|
||||||
local file="$(_state_dir "$agent")/report.json"
|
local file="$(_state_dir "$agent")/report.json"
|
||||||
|
|
||||||
|
_STATE_FILE="$file" _STATE_AGENT="$agent" _STATE_STATUS="$status" \
|
||||||
|
_STATE_SUMMARY="$summary" _STATE_SESSION_ID="$session_id" \
|
||||||
|
_STATE_STARTED="$started_at" _STATE_ENDED="$ended_at" \
|
||||||
|
_STATE_OUTCOME="$outcome" _STATE_SOURCES="$sources" \
|
||||||
|
_STATE_BRANCH="$branch" _STATE_PR="$pr_number" \
|
||||||
|
_STATE_NEXT="$next_priority" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json, os
|
||||||
|
e = os.environ
|
||||||
|
sources = int(e['_STATE_SOURCES']) if e['_STATE_SOURCES'].isdigit() else 0
|
||||||
|
pr = int(e['_STATE_PR']) if e['_STATE_PR'].isdigit() else None
|
||||||
|
next_p = None if e['_STATE_NEXT'] == 'null' else e['_STATE_NEXT']
|
||||||
data = {
|
data = {
|
||||||
'agent': '$agent',
|
'agent': e['_STATE_AGENT'],
|
||||||
'updated_at': '$ended_at',
|
'updated_at': e['_STATE_ENDED'],
|
||||||
'status': '$status',
|
'status': e['_STATE_STATUS'],
|
||||||
'summary': '''$summary''',
|
'summary': e['_STATE_SUMMARY'],
|
||||||
'current_task': None,
|
'current_task': None,
|
||||||
'last_session': {
|
'last_session': {
|
||||||
'id': '$session_id',
|
'id': e['_STATE_SESSION_ID'],
|
||||||
'started_at': '$started_at',
|
'started_at': e['_STATE_STARTED'],
|
||||||
'ended_at': '$ended_at',
|
'ended_at': e['_STATE_ENDED'],
|
||||||
'outcome': '$outcome',
|
'outcome': e['_STATE_OUTCOME'],
|
||||||
'sources_archived': $sources,
|
'sources_archived': sources,
|
||||||
'branch': '$branch',
|
'branch': e['_STATE_BRANCH'],
|
||||||
'pr_number': $pr_number
|
'pr_number': pr
|
||||||
},
|
},
|
||||||
'blocked_by': None,
|
'blocked_by': None,
|
||||||
'next_priority': $([ "$next_priority" = "null" ] && echo "None" || echo "'$next_priority'")
|
'next_priority': next_p
|
||||||
}
|
}
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
" | _atomic_write_stdin "$file"
|
" | _atomic_write_stdin "$file"
|
||||||
|
|
@ -113,19 +115,23 @@ state_start_session() {
|
||||||
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
local file="$(_state_dir "$agent")/session.json"
|
local file="$(_state_dir "$agent")/session.json"
|
||||||
|
|
||||||
|
_STATE_FILE="$file" _STATE_AGENT="$agent" _STATE_SID="$session_id" \
|
||||||
|
_STATE_STARTED="$started_at" _STATE_TYPE="$type" _STATE_DOMAIN="$domain" \
|
||||||
|
_STATE_BRANCH="$branch" _STATE_MODEL="$model" _STATE_TIMEOUT="$timeout" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json, os
|
||||||
|
e = os.environ
|
||||||
data = {
|
data = {
|
||||||
'agent': '$agent',
|
'agent': e['_STATE_AGENT'],
|
||||||
'session_id': '$session_id',
|
'session_id': e['_STATE_SID'],
|
||||||
'started_at': '$started_at',
|
'started_at': e['_STATE_STARTED'],
|
||||||
'ended_at': None,
|
'ended_at': None,
|
||||||
'type': '$type',
|
'type': e['_STATE_TYPE'],
|
||||||
'domain': '$domain',
|
'domain': e['_STATE_DOMAIN'],
|
||||||
'branch': '$branch',
|
'branch': e['_STATE_BRANCH'],
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'model': '$model',
|
'model': e['_STATE_MODEL'],
|
||||||
'timeout_seconds': $timeout,
|
'timeout_seconds': int(e['_STATE_TIMEOUT']),
|
||||||
'research_question': None,
|
'research_question': None,
|
||||||
'belief_targeted': None,
|
'belief_targeted': None,
|
||||||
'disconfirmation_target': None,
|
'disconfirmation_target': None,
|
||||||
|
|
@ -149,13 +155,18 @@ state_end_session() {
|
||||||
local pr_number="${4:-null}"
|
local pr_number="${4:-null}"
|
||||||
local file="$(_state_dir "$agent")/session.json"
|
local file="$(_state_dir "$agent")/session.json"
|
||||||
|
|
||||||
|
_STATE_FILE="$file" _STATE_OUTCOME="$outcome" _STATE_SOURCES="$sources" \
|
||||||
|
_STATE_PR="$pr_number" _STATE_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json, os
|
||||||
with open('$file') as f:
|
e = os.environ
|
||||||
|
with open(e['_STATE_FILE']) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
data['ended_at'] = '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
|
data['ended_at'] = e['_STATE_TS']
|
||||||
data['status'] = '$outcome'
|
data['status'] = e['_STATE_OUTCOME']
|
||||||
data['sources_archived'] = $sources
|
data['sources_archived'] = int(e['_STATE_SOURCES']) if e['_STATE_SOURCES'].isdigit() else 0
|
||||||
|
pr = e.get('_STATE_PR', 'null')
|
||||||
|
data['pr_number'] = int(pr) if pr.isdigit() else None
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
" | _atomic_write_stdin "$file"
|
" | _atomic_write_stdin "$file"
|
||||||
}
|
}
|
||||||
|
|
@ -168,13 +179,17 @@ state_journal_append() {
|
||||||
shift 2
|
shift 2
|
||||||
# Remaining args are key=value pairs for extra fields
|
# Remaining args are key=value pairs for extra fields
|
||||||
local file="$(_state_dir "$agent")/journal.jsonl"
|
local file="$(_state_dir "$agent")/journal.jsonl"
|
||||||
local extras=""
|
|
||||||
for kv in "$@"; do
|
_STATE_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" _STATE_EVT="$event" \
|
||||||
local key="${kv%%=*}"
|
python3 -c "
|
||||||
local val="${kv#*=}"
|
import json, os, sys
|
||||||
extras="$extras, \"$key\": \"$val\""
|
entry = {'ts': os.environ['_STATE_TS'], 'event': os.environ['_STATE_EVT']}
|
||||||
done
|
for pair in sys.argv[1:]:
|
||||||
echo "{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"event\":\"$event\"$extras}" >> "$file"
|
k, _, v = pair.partition('=')
|
||||||
|
if k:
|
||||||
|
entry[k] = v
|
||||||
|
print(json.dumps(entry))
|
||||||
|
" "$@" >> "$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Metrics ---
|
# --- Metrics ---
|
||||||
|
|
@ -185,25 +200,29 @@ state_update_metrics() {
|
||||||
local sources="${3:-0}"
|
local sources="${3:-0}"
|
||||||
local file="$(_state_dir "$agent")/metrics.json"
|
local file="$(_state_dir "$agent")/metrics.json"
|
||||||
|
|
||||||
|
_STATE_FILE="$file" _STATE_AGENT="$agent" _STATE_OUTCOME="$outcome" \
|
||||||
|
_STATE_SOURCES="$sources" _STATE_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json, os
|
||||||
|
e = os.environ
|
||||||
try:
|
try:
|
||||||
with open('$file') as f:
|
with open(e['_STATE_FILE']) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except:
|
except:
|
||||||
data = {'agent': '$agent', 'lifetime': {}, 'rolling_30d': {}}
|
data = {'agent': e['_STATE_AGENT'], 'lifetime': {}, 'rolling_30d': {}}
|
||||||
|
|
||||||
lt = data.setdefault('lifetime', {})
|
lt = data.setdefault('lifetime', {})
|
||||||
lt['sessions_total'] = lt.get('sessions_total', 0) + 1
|
lt['sessions_total'] = lt.get('sessions_total', 0) + 1
|
||||||
if '$outcome' == 'completed':
|
outcome = e['_STATE_OUTCOME']
|
||||||
|
if outcome == 'completed':
|
||||||
lt['sessions_completed'] = lt.get('sessions_completed', 0) + 1
|
lt['sessions_completed'] = lt.get('sessions_completed', 0) + 1
|
||||||
elif '$outcome' == 'timeout':
|
elif outcome == 'timeout':
|
||||||
lt['sessions_timeout'] = lt.get('sessions_timeout', 0) + 1
|
lt['sessions_timeout'] = lt.get('sessions_timeout', 0) + 1
|
||||||
elif '$outcome' == 'error':
|
elif outcome == 'error':
|
||||||
lt['sessions_error'] = lt.get('sessions_error', 0) + 1
|
lt['sessions_error'] = lt.get('sessions_error', 0) + 1
|
||||||
lt['sources_archived'] = lt.get('sources_archived', 0) + $sources
|
lt['sources_archived'] = lt.get('sources_archived', 0) + (int(e['_STATE_SOURCES']) if e['_STATE_SOURCES'].isdigit() else 0)
|
||||||
|
|
||||||
data['updated_at'] = '$(date -u +%Y-%m-%dT%H:%M:%SZ)'
|
data['updated_at'] = e['_STATE_TS']
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
" | _atomic_write_stdin "$file"
|
" | _atomic_write_stdin "$file"
|
||||||
}
|
}
|
||||||
|
|
@ -227,17 +246,21 @@ state_send_message() {
|
||||||
local file="$inbox/${msg_id}.json"
|
local file="$inbox/${msg_id}.json"
|
||||||
|
|
||||||
mkdir -p "$inbox"
|
mkdir -p "$inbox"
|
||||||
|
_STATE_FILE="$file" _STATE_MSGID="$msg_id" _STATE_FROM="$from" \
|
||||||
|
_STATE_TO="$to" _STATE_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
||||||
|
_STATE_TYPE="$type" _STATE_SUBJECT="$subject" _STATE_BODY="$body" \
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json
|
import json, os
|
||||||
|
e = os.environ
|
||||||
data = {
|
data = {
|
||||||
'id': '$msg_id',
|
'id': e['_STATE_MSGID'],
|
||||||
'from': '$from',
|
'from': e['_STATE_FROM'],
|
||||||
'to': '$to',
|
'to': e['_STATE_TO'],
|
||||||
'created_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
|
'created_at': e['_STATE_TS'],
|
||||||
'type': '$type',
|
'type': e['_STATE_TYPE'],
|
||||||
'priority': 'normal',
|
'priority': 'normal',
|
||||||
'subject': '''$subject''',
|
'subject': e['_STATE_SUBJECT'],
|
||||||
'body': '''$body''',
|
'body': e['_STATE_BODY'],
|
||||||
'source_ref': None,
|
'source_ref': None,
|
||||||
'expires_at': None
|
'expires_at': None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ echo "=== Pre-deploy syntax check ==="
|
||||||
ERRORS=0
|
ERRORS=0
|
||||||
for f in "$REPO_ROOT/ops/pipeline-v2/lib/"*.py "$REPO_ROOT/ops/pipeline-v2/"*.py "$REPO_ROOT/ops/diagnostics/"*.py; do
|
for f in "$REPO_ROOT/ops/pipeline-v2/lib/"*.py "$REPO_ROOT/ops/pipeline-v2/"*.py "$REPO_ROOT/ops/diagnostics/"*.py; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
if ! python3 -c "import ast; ast.parse(open('$f').read())" 2>/dev/null; then
|
if ! python3 -c "import ast, sys; ast.parse(open(sys.argv[1]).read())" "$f" 2>/dev/null; then
|
||||||
echo "SYNTAX ERROR: $f"
|
echo "SYNTAX ERROR: $f"
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,9 @@ if [ ! -d "$REPO_DIR/.git" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$REPO_DIR"
|
cd "$REPO_DIR"
|
||||||
git config credential.helper "!f() { echo username=m3taversal; echo password=$FORGEJO_ADMIN_TOKEN; }; f"
|
|
||||||
git remote set-url origin "${FORGEJO_URL}/teleo/teleo-codex.git" 2>/dev/null || true
|
git remote set-url origin "${FORGEJO_URL}/teleo/teleo-codex.git" 2>/dev/null || true
|
||||||
git checkout main >> "$LOG" 2>&1
|
git -c http.extraHeader="Authorization: token $FORGEJO_ADMIN_TOKEN" checkout main >> "$LOG" 2>&1
|
||||||
git pull --rebase >> "$LOG" 2>&1
|
git -c http.extraHeader="Authorization: token $FORGEJO_ADMIN_TOKEN" pull --rebase >> "$LOG" 2>&1
|
||||||
|
|
||||||
# --- Map agent to domain ---
|
# --- Map agent to domain ---
|
||||||
case "$AGENT" in
|
case "$AGENT" in
|
||||||
|
|
@ -94,13 +93,13 @@ if [ ! -f "$NETWORK_FILE" ]; then
|
||||||
else
|
else
|
||||||
log "Pulling tweets from ${AGENT}'s network..."
|
log "Pulling tweets from ${AGENT}'s network..."
|
||||||
ACCOUNTS=$(python3 -c "
|
ACCOUNTS=$(python3 -c "
|
||||||
import json
|
import json, sys
|
||||||
with open('$NETWORK_FILE') as f:
|
with open(sys.argv[1]) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
for acct in data.get('accounts', []):
|
for acct in data.get('accounts', []):
|
||||||
if acct.get('tier') in ('core', 'extended'):
|
if acct.get('tier') in ('core', 'extended'):
|
||||||
print(acct['username'])
|
print(acct['username'])
|
||||||
" 2>/dev/null || true)
|
" "$NETWORK_FILE" 2>/dev/null || true)
|
||||||
|
|
||||||
TWEET_DATA=""
|
TWEET_DATA=""
|
||||||
API_CALLS=0
|
API_CALLS=0
|
||||||
|
|
@ -132,7 +131,7 @@ for acct in data.get('accounts', []):
|
||||||
$(python3 -c "
|
$(python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
try:
|
try:
|
||||||
d = json.load(open('$OUTFILE'))
|
d = json.load(open(sys.argv[1]))
|
||||||
tweets = d.get('tweets', d.get('data', []))
|
tweets = d.get('tweets', d.get('data', []))
|
||||||
for t in tweets[:20]:
|
for t in tweets[:20]:
|
||||||
text = t.get('text', '')[:500]
|
text = t.get('text', '')[:500]
|
||||||
|
|
@ -144,7 +143,7 @@ try:
|
||||||
print()
|
print()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Error reading: {e}', file=sys.stderr)
|
print(f'Error reading: {e}', file=sys.stderr)
|
||||||
" 2>/dev/null || echo "(failed to parse)")"
|
" "$OUTFILE" 2>/dev/null || echo "(failed to parse)")"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
log "API usage: ${API_CALLS} calls, ${API_CACHED} cached for ${AGENT}"
|
log "API usage: ${API_CALLS} calls, ${API_CACHED} cached for ${AGENT}"
|
||||||
|
|
@ -168,7 +167,7 @@ if [ -d "$INBOX_RAW" ] && ls "$INBOX_RAW"/*.json 2>/dev/null | head -1 > /dev/nu
|
||||||
$(python3 -c "
|
$(python3 -c "
|
||||||
import json, sys
|
import json, sys
|
||||||
try:
|
try:
|
||||||
d = json.load(open('$RAWFILE'))
|
d = json.load(open(sys.argv[1]))
|
||||||
tweets = d.get('tweets', d.get('data', []))
|
tweets = d.get('tweets', d.get('data', []))
|
||||||
for t in tweets[:20]:
|
for t in tweets[:20]:
|
||||||
text = t.get('text', '')[:500]
|
text = t.get('text', '')[:500]
|
||||||
|
|
@ -180,7 +179,7 @@ try:
|
||||||
print()
|
print()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'Error: {e}', file=sys.stderr)
|
print(f'Error: {e}', file=sys.stderr)
|
||||||
" 2>/dev/null || echo "(failed to parse)")"
|
" "$RAWFILE" 2>/dev/null || echo "(failed to parse)")"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -432,7 +431,7 @@ git commit -m "${AGENT}: research session ${DATE} — ${SOURCE_COUNT} sources ar
|
||||||
Pentagon-Agent: ${AGENT_UPPER} <HEADLESS>" >> "$LOG" 2>&1
|
Pentagon-Agent: ${AGENT_UPPER} <HEADLESS>" >> "$LOG" 2>&1
|
||||||
|
|
||||||
# --- Push ---
|
# --- Push ---
|
||||||
git push -u origin "$BRANCH" --force >> "$LOG" 2>&1
|
git -c http.extraHeader="Authorization: token $AGENT_TOKEN" push -u origin "$BRANCH" --force >> "$LOG" 2>&1
|
||||||
log "Pushed $BRANCH"
|
log "Pushed $BRANCH"
|
||||||
|
|
||||||
# --- Check for existing PR on this branch ---
|
# --- Check for existing PR on this branch ---
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue