Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a3efb72
fix: harden content-scanner and deploy-agents scripts (GH#3897, GH#3909)
Mar 8, 2026
9b62dd5
fix: propagate exit codes, escape osascript args, unify scorer aggreg…
Mar 8, 2026
a8e36f8
fix: remove error suppression, secure mysql access, harden glab insta…
Mar 8, 2026
9f99ab4
fix: jq injection, prompt-guard hardening, error suppression, securit…
Mar 8, 2026
e34ea8c
fix: HIGH priority quality-debt batch — regex, injection, error suppr…
Mar 8, 2026
a3487ff
fix: MEDIUM quality-debt batch — date correction, skip() detail, prin…
Mar 8, 2026
0e1b29b
fix: address review bot suggestions from CodeRabbit and Gemini (GH#3916)
marcusquinn Mar 8, 2026
ed9039f
fix: clean up regex metachar class escape in content-scanner prefilter
marcusquinn Mar 8, 2026
abd61ec
fix: address CodeRabbit critical/major review findings on PR #3916 — …
Mar 8, 2026
af74605
fix: address CodeRabbit review round 2 — 7 files
Mar 8, 2026
c15c7ab
fix: preserve trailing newlines in NFKC normalization (GH#3916)
marcusquinn Mar 8, 2026
2a1d574
fix: guard tenant-file reads against TOCTOU race (GH#3916)
marcusquinn Mar 8, 2026
150ce95
fix: remove redundant $? check under set -euo pipefail (GH#3916)
marcusquinn Mar 8, 2026
222a550
fix: correct version fallback syntax in session-manager docs (GH#3916)
marcusquinn Mar 8, 2026
4d031c5
fix: preserve trailing newlines in NFKC normalization (content-scanner)
Mar 8, 2026
5aafcca
fix: correct version fallback to use subshell pattern (session-manager)
Mar 8, 2026
c2eb94d
fix: address CodeRabbit review round 3 — 6 files (GH#3916)
marcusquinn Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Every agent session — interactive, worker, or supervisor — should improve th

**Route to the correct repo.** Not every improvement belongs in the current project. Before creating a self-improvement task, determine whether the problem is project-specific or framework-level:

- **Framework-level** — route to the aidevops repo. Indicators: the observation references files under `~/.aidevops/`, framework scripts (`ai-actions.sh`, `ai-lifecycle.sh`, `supervisor/`, `dispatch.sh`, `pre-edit-check.sh`, helper scripts), agent prompt behaviour, supervisor/pulse logic, or cross-repo orchestration. Use `claim-task-id.sh --repo-path <aidevops-repo-path> --title "description"` (resolve the slug from `~/.config/aidevops/repos.json`). Only run `gh issue create --repo <aidevops-slug>` if `claim-task-id.sh` was invoked with `--no-issue` or its output did not include a `ref=GH#` (or `ref=GL#` for GitLab) token — otherwise the issue already exists and a second `gh issue create` would produce a duplicate. The fix belongs in the framework, not in the project that happened to trigger it.
- **Framework-level** — route to the aidevops repo. Indicators: the observation references files under `~/.aidevops/`, framework scripts (`ai-actions.sh`, `ai-lifecycle.sh`, `supervisor/`, `dispatch.sh`, `pre-edit-check.sh`, helper scripts), agent prompt behaviour, supervisor/pulse logic, or cross-repo orchestration. Use `claim-task-id.sh --repo-path <aidevops-repo-path> --title "description"` and follow the cross-repo task creation workflow below. The fix belongs in the framework, not in the project that happened to trigger it.
- **Project-specific** — route to the current repo. Indicators: the observation is about this project's CI, code patterns, dependencies, or domain logic.

If uncertain, ask: "Would this fix apply to every repo the framework manages, or only this one?" Framework-wide problems go to aidevops; project-specific problems stay local. Never create framework tasks in a project repo — they become invisible to framework maintainers and pollute the project's task namespace.
Expand Down
20 changes: 16 additions & 4 deletions .agents/plugins/opencode-aidevops/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { execSync } from "child_process";
import { existsSync } from "fs";
import { join } from "path";

/**
* Escape a string for safe interpolation into a shell command.
* Wraps in single quotes and escapes internal single quotes.
* @param {string} s - The string to escape
* @returns {string} Shell-safe quoted string
*/
function shellEscape(s) {
if (typeof s !== "string") return "''";
return "'" + s.replace(/'/g, "'\\''") + "'";
}

/**
* Create a memory tool (recall or store) using a shared factory pattern.
* Deduplicates the near-identical memory_recall and memory_store definitions.
Expand Down Expand Up @@ -41,9 +52,10 @@ function createAidevopsTool(run) {
description:
'Run aidevops CLI commands (status, repos, features, secret, etc.). Pass command as string e.g. "status", "repos", "features"',
async execute(args) {
const cmd = `aidevops ${args.command || args}`;
const subcmd = String(args.command || args);
const cmd = `aidevops ${shellEscape(subcmd)}`;
const result = run(cmd, 15000);
return result || `Command completed: ${cmd}`;
return result || `Command completed: aidevops ${subcmd}`;
},
};
}
Expand Down Expand Up @@ -266,7 +278,7 @@ export function createTools(scriptsDir, run, pipelines) {
description:
'Recall memories from the aidevops cross-session memory system. Args: query (string), limit (string, default "5")',
buildArgs: (args, helper) => ({
cmd: `bash "${helper}" recall "${args.query}" --limit ${args.limit || "5"} 2>/dev/null`,
cmd: `bash "${helper}" recall ${shellEscape(args.query)} --limit ${shellEscape(String(args.limit || "5"))}`,
timeout: 10000,
}),
}),
Expand All @@ -283,7 +295,7 @@ export function createTools(scriptsDir, run, pipelines) {
return { cmd: `echo "Error: content is required to store a memory" >&2; exit 1`, timeout: 1000 };
}
return {
cmd: `bash "${helper}" store "${content}" --confidence ${args.confidence || "medium"} 2>/dev/null`,
cmd: `bash "${helper}" store ${shellEscape(content)} --confidence ${shellEscape(String(args.confidence || "medium"))}`,
timeout: 10000,
};
},
Expand Down
4 changes: 2 additions & 2 deletions .agents/scripts/circuit-breaker-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,12 @@ cb_write_state() {

# Atomic write via temp file + mv
local tmp_file="${state_file}.tmp.$$"
if ! printf '%s\n' "$state_json" >"$tmp_file" 2>/dev/null; then
if ! printf '%s\n' "$state_json" >"$tmp_file"; then
_cb_log_warn "failed to write temp state file: $tmp_file"
rm -f "$tmp_file" 2>/dev/null || true
return 1
fi
if ! mv -f "$tmp_file" "$state_file" 2>/dev/null; then
if ! mv -f "$tmp_file" "$state_file"; then
_cb_log_warn "failed to move temp state to: $state_file"
rm -f "$tmp_file" 2>/dev/null || true
return 1
Expand Down
8 changes: 4 additions & 4 deletions .agents/scripts/commands/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ Arguments: $ARGUMENTS

```bash
# Get success patterns
~/.aidevops/agents/scripts/memory-helper.sh recall --type SUCCESS_PATTERN --limit 20
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type SUCCESS_PATTERN --limit 20

# Get failure patterns
~/.aidevops/agents/scripts/memory-helper.sh recall --type FAILURE_PATTERN --limit 20
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type FAILURE_PATTERN --limit 20

# Get working solutions
~/.aidevops/agents/scripts/memory-helper.sh recall --type WORKING_SOLUTION --limit 10
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type WORKING_SOLUTION --limit 10

# Get failed approaches
~/.aidevops/agents/scripts/memory-helper.sh recall --type FAILED_APPROACH --limit 10
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type FAILED_APPROACH --limit 10
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

2. If arguments are provided, filter results relevant to the task description.
Expand Down
4 changes: 2 additions & 2 deletions .agents/scripts/commands/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Task: $ARGUMENTS
1. First, check pattern history from cross-session memory:

```bash
~/.aidevops/agents/scripts/memory-helper.sh recall --type SUCCESS_PATTERN --limit 10
~/.aidevops/agents/scripts/memory-helper.sh recall --type FAILURE_PATTERN --limit 10
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type SUCCESS_PATTERN --limit 10
~/.aidevops/agents/scripts/memory-helper.sh recall --recent --type FAILURE_PATTERN --limit 10
```

2. Read `tools/context/model-routing.md` for the routing rules and tier definitions.
Expand Down
62 changes: 46 additions & 16 deletions .agents/scripts/content-scanner-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 1
source "${SCRIPT_DIR}/shared-constants.sh" 2>/dev/null || true
source "${SCRIPT_DIR}/shared-constants.sh" || true

# Fallback colours if shared-constants.sh not loaded
[[ -z "${RED+x}" ]] && RED='\033[0;31m'
Expand Down Expand Up @@ -193,8 +193,16 @@ _cs_prefilter() {

local keyword
for keyword in "${_CS_PREFILTER_KEYWORDS[@]}"; do
if [[ "$lower_content" == *"$keyword"* ]]; then
return 0
# Keywords containing regex metacharacters (.*+?|()^$) use regex matching;
# plain keywords use faster literal substring matching.
if [[ "$keyword" =~ [.*+?|()^$] ]]; then
if [[ "$lower_content" =~ $keyword ]]; then
return 0
fi
else
if [[ "$lower_content" == *"$keyword"* ]]; then
return 0
fi
fi
done

Expand All @@ -221,17 +229,35 @@ _cs_normalize_nfkc() {
fi

# Try python3 first (most reliable NFKC)
# Sentinel character preserves trailing newlines through command substitution.
# Bash $() strips trailing newlines, causing false "changed" detection.
# The sentinel is appended inside the normalizer process (not via separate
# printf) so the exit code reflects whether normalization succeeded.
if command -v python3 &>/dev/null; then
printf '%s' "$content" | python3 -c "
local py_result
if py_result=$(printf '%s' "$content" | python3 -c "
import sys, unicodedata
text = sys.stdin.read()
sys.stdout.write(unicodedata.normalize('NFKC', text))
" 2>/dev/null && return 0
sys.stdout.write(unicodedata.normalize('NFKC', text) + 'x')
"); then
py_result="${py_result%x}"
printf '%s' "$py_result"
return 0
fi
fi

# Try perl as fallback (Unicode::Normalize is core since 5.8)
if command -v perl &>/dev/null; then
printf '%s' "$content" | perl -MUnicode::Normalize -CS -pe '$_ = NFKC($_)' 2>/dev/null && return 0
local perl_result
if perl_result=$(printf '%s' "$content" | perl -MUnicode::Normalize -CS -e '
local $/;
my $text = <STDIN>;
print NFKC($text) . "x";
'); then
perl_result="${perl_result%x}"
printf '%s' "$perl_result"
return 0
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
fi

# No normalizer available — pass through unchanged
Expand All @@ -250,12 +276,14 @@ sys.stdout.write(unicodedata.normalize('NFKC', text))
_cs_generate_boundary_id() {
# Use /dev/urandom for a short unique ID (8 hex chars)
if [[ -r /dev/urandom ]]; then
od -An -tx1 -N4 /dev/urandom 2>/dev/null | tr -d ' \n'
od -An -tx1 -N4 /dev/urandom | tr -d '[:space:]'
return 0
fi

# Fallback: use $RANDOM (less entropy but functional)
printf '%04x%04x' "$RANDOM" "$RANDOM"
# Fallback: combine $RANDOM with PID and epoch for less predictability.
# $RANDOM alone is a 15-bit PRNG seeded from PID — too predictable for
# boundary IDs that must resist crafted payloads.
printf '%04x%04x%04x' "$RANDOM" "$$" "$((SECONDS % 65536))"
return 0
}

Expand Down Expand Up @@ -292,18 +320,20 @@ scan_content() {
byte_count=$(printf '%s' "$content" | wc -c | tr -d ' ')
_cs_log_info "Scanning content ($byte_count bytes)"

# Layer 1: Keyword pre-filter
if ! _cs_prefilter "$content"; then
# Layer 2 first: NFKC normalization (must run before pre-filter to prevent
# Unicode bypass — e.g., "𝐈𝐠𝐧𝐨𝐫𝐞" normalizes to "Ignore" which the
# pre-filter can then match).
local normalized
normalized=$(_cs_normalize_nfkc "$content")

# Layer 1: Keyword pre-filter on NORMALIZED content
if ! _cs_prefilter "$normalized"; then
_cs_log_success "Pre-filter: no suspicious keywords found — skipping full scan"
echo "CLEAN"
return 0
fi
_cs_log_info "Pre-filter: keyword match — proceeding to full scan"

# Layer 2: NFKC normalization
local normalized
normalized=$(_cs_normalize_nfkc "$content")

# Layer 3: Delegate to prompt-guard-helper.sh
if [[ ! -x "$PROMPT_GUARD" ]]; then
_cs_log_error "prompt-guard-helper.sh not found at: $PROMPT_GUARD"
Expand Down
24 changes: 15 additions & 9 deletions .agents/scripts/credential-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@ validate_tenant_name() {
# Get the active tenant name
get_active_tenant() {
# Priority: 1) Project override, 2) Global active, 3) "default"
# Guard reads against TOCTOU race (file may vanish/change permissions between
# -f check and read) — degrade gracefully instead of aborting under set -e (GH#3916)
if [[ -f "$PROJECT_TENANT_FILE" ]]; then
local project_tenant
project_tenant=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null)
if [[ -n "$project_tenant" ]]; then
if validate_tenant_name "$project_tenant" 2>/dev/null; then
if ! project_tenant=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null); then
print_warning "Failed to read project tenant file, falling back"
elif [[ -n "$project_tenant" ]]; then
if validate_tenant_name "$project_tenant"; then
echo "$project_tenant"
return 0
fi
Expand All @@ -57,9 +60,10 @@ get_active_tenant() {

if [[ -f "$ACTIVE_TENANT_FILE" ]]; then
local active
active=$(tr -d '[:space:]' <"$ACTIVE_TENANT_FILE" 2>/dev/null)
if [[ -n "$active" ]]; then
if validate_tenant_name "$active" 2>/dev/null; then
if ! active=$(tr -d '[:space:]' <"$ACTIVE_TENANT_FILE" 2>/dev/null); then
print_warning "Failed to read active tenant file, falling back"
elif [[ -n "$active" ]]; then
if validate_tenant_name "$active"; then
echo "$active"
return 0
fi
Expand Down Expand Up @@ -693,7 +697,9 @@ cmd_use() {
# Show current project tenant
if [[ -f "$PROJECT_TENANT_FILE" ]]; then
local current
current=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null)
if ! current=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null); then
current="<unreadable>"
fi
print_info "Project tenant: $current"
else
print_info "No project-level tenant set (using global: $(get_active_tenant))"
Expand Down Expand Up @@ -743,7 +749,7 @@ cmd_status() {
local project_tenant=""

if [[ -f "$PROJECT_TENANT_FILE" ]]; then
project_tenant=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null)
project_tenant=$(tr -d '[:space:]' <"$PROJECT_TENANT_FILE" 2>/dev/null) || project_tenant=""
fi

echo ""
Expand All @@ -758,7 +764,7 @@ cmd_status() {

local global_active=""
if [[ -f "$ACTIVE_TENANT_FILE" ]]; then
global_active=$(tr -d '[:space:]' <"$ACTIVE_TENANT_FILE" 2>/dev/null)
global_active=$(tr -d '[:space:]' <"$ACTIVE_TENANT_FILE" 2>/dev/null) || global_active=""
fi
if [[ -n "$global_active" && "$global_active" != "$active" ]]; then
echo -e " Global tenant: ${DIM}$global_active${NC}"
Expand Down
22 changes: 16 additions & 6 deletions .agents/scripts/deploy-agents-on-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ detect_changes() {
local changed_files

if [[ -n "$since_commit" ]]; then
changed_files=$(git -C "$REPO_DIR" diff --name-only "$since_commit" HEAD -- '.agents/' 2>/dev/null || echo "")
# Validate since_commit is a valid revision (not an injected option)
if [[ "$since_commit" == -* ]] || ! git -C "$REPO_DIR" rev-parse --verify "$since_commit" >/dev/null 2>&1; then
log_error "Invalid commit reference: $since_commit"
return 1
fi
changed_files=$(git -C "$REPO_DIR" diff --name-only "$since_commit" HEAD -- '.agents/' || echo "")
else
# Compare deployed VERSION with repo VERSION to detect staleness
local repo_version deployed_version
Expand Down Expand Up @@ -203,7 +208,7 @@ deploy_scripts_only() {
else
# Fallback: tar-based copy
# Remove existing target contents first to match rsync --delete behavior
find "$target_scripts_dir" -mindepth 1 -delete 2>/dev/null || true
find "$target_scripts_dir" -mindepth 1 -delete || true
(cd "$source_dir" && tar cf - .) | (cd "$target_scripts_dir" && tar xf -)
fi

Expand Down Expand Up @@ -261,9 +266,14 @@ deploy_all_agents() {
fi

# Remove existing target contents (except custom/draft) to match rsync --delete behavior
if [[ -z "$TARGET_DIR" ]]; then
log_error "TARGET_DIR is empty — refusing to run find cleanup"
rm -rf "$tmp_preserve"
return 1
fi
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 \
! -name 'custom' ! -name 'draft' ! -name 'loop-state' \
-exec rm -rf {} + 2>/dev/null || true
-exec rm -rf {} + || true

# Copy all agents
(cd "$source_dir" && tar cf - --exclude='loop-state' --exclude='custom' --exclude='draft' .) |
Expand Down Expand Up @@ -340,15 +350,15 @@ deploy_changed_files() {
mkdir -p "$target_parent"

# Copy file (catch errors instead of letting set -e abort)
if ! cp "$source_file" "$target_file"; then
if ! cp -- "$source_file" "$target_file"; then
log_warn "Failed to copy: $rel_path"
failed=$((failed + 1))
continue
fi

# Set executable if it's a script
if [[ "$target_file" == *.sh ]]; then
if ! chmod +x "$target_file"; then
if ! chmod -- +x "$target_file"; then
log_warn "Failed to set executable: $rel_path"
failed=$((failed + 1))
continue
Expand All @@ -359,7 +369,7 @@ deploy_changed_files() {
elif [[ ! -e "$source_file" ]]; then
# File was deleted in source — remove from target
if [[ -f "$target_file" ]]; then
if ! rm -f "$target_file"; then
if ! rm -f -- "$target_file"; then
log_warn "Failed to remove deleted file: $rel_path"
failed=$((failed + 1))
continue
Expand Down
4 changes: 2 additions & 2 deletions .agents/scripts/document-creation-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ human_filesize() {
local file="$1"
local bytes
if [[ "$(uname)" == "Darwin" ]]; then
bytes=$(stat -f%z -- "$file" 2>/dev/null || echo "0")
bytes=$(stat -f%z -- "$file" || echo "0")
else
bytes=$(stat -c%s -- "$file" 2>/dev/null || echo "0")
bytes=$(stat -c%s -- "$file" || echo "0")
fi
if [[ "$bytes" -ge 1073741824 ]]; then
printf '%s.%sG' "$((bytes / 1073741824))" "$(((bytes % 1073741824) * 10 / 1073741824))"
Expand Down
Loading