forgedock 1.0.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.
Files changed (44) hide show
  1. package/LICENSE +244 -0
  2. package/README.md +251 -0
  3. package/bin/forgedock.mjs +232 -0
  4. package/commands/analytics.md +777 -0
  5. package/commands/audit-agents.md +410 -0
  6. package/commands/audit.md +410 -0
  7. package/commands/autopilot.md +432 -0
  8. package/commands/cleanup.md +264 -0
  9. package/commands/deploy-info.md +292 -0
  10. package/commands/failure-recon.md +375 -0
  11. package/commands/forge-stats.md +183 -0
  12. package/commands/geo-audit.md +343 -0
  13. package/commands/incident-response.md +384 -0
  14. package/commands/issue.md +506 -0
  15. package/commands/milestone.md +599 -0
  16. package/commands/orchestrate.md +1044 -0
  17. package/commands/pipeline-health.md +2343 -0
  18. package/commands/qa-sweep.md +237 -0
  19. package/commands/quality-gate.md +777 -0
  20. package/commands/review-pr-agents.md +1711 -0
  21. package/commands/review-pr-staging.md +325 -0
  22. package/commands/review-pr.md +888 -0
  23. package/commands/rollback.md +253 -0
  24. package/commands/security-audit.md +529 -0
  25. package/commands/sync-ecosystem.md +527 -0
  26. package/commands/validate.md +131 -0
  27. package/commands/work-on/build/architect.md +291 -0
  28. package/commands/work-on/build/context.md +349 -0
  29. package/commands/work-on/build/implement.md +280 -0
  30. package/commands/work-on/build/validate.md +292 -0
  31. package/commands/work-on/build.md +262 -0
  32. package/commands/work-on/close.md +350 -0
  33. package/commands/work-on/decompose.md +254 -0
  34. package/commands/work-on/investigate.md +437 -0
  35. package/commands/work-on/review.md +245 -0
  36. package/commands/work-on-monolithic.md +298 -0
  37. package/commands/work-on.md +1089 -0
  38. package/docs/CODEX.md +92 -0
  39. package/docs/WORKFLOW.md +104 -0
  40. package/package.json +45 -0
  41. package/scripts/verify-env-vars.sh +201 -0
  42. package/scripts/verify-host-headers.sh +117 -0
  43. package/scripts/verify-route-registration.sh +118 -0
  44. package/scripts/verify-sops-chain.sh +163 -0
package/docs/CODEX.md ADDED
@@ -0,0 +1,92 @@
1
+ # Codex Support
2
+
3
+ Forge now has an additive Codex-native install path. The goal is to preserve Forge behavior without regressing the existing Claude setup.
4
+
5
+ ## Architecture
6
+
7
+ Shared source:
8
+ - `commands/**/*.md` remains the workflow spec for both runtimes
9
+
10
+ Claude wrapper:
11
+ - `install.sh` symlinks command files into `~/.claude/commands`
12
+
13
+ Codex wrapper:
14
+ - `install-codex.sh` generates namespaced skills in `~/.codex/skills`
15
+ - each installed skill adapts one shared command spec for Codex-native execution
16
+ - if a matching repo-local skill exists under `.agents/skills/<command>/SKILL.md`, the installer points the installed wrapper at that repo-local adapter instead of the generic wrapper
17
+
18
+ This is intentionally a wrapper architecture, not a duplicate command tree.
19
+
20
+ ## Install
21
+
22
+ From the repo root:
23
+
24
+ ```bash
25
+ ./install-codex.sh
26
+ ```
27
+
28
+ The installer creates:
29
+ - `~/.codex/skills/forge/SKILL.md`
30
+ - `~/.codex/skills/forge-*/SKILL.md` for each command file under `commands/`, excluding internal catalog files that are read on demand
31
+
32
+ It does not overwrite unrelated global Codex skills.
33
+
34
+ ## Skill Naming
35
+
36
+ Command files map to installed Codex skill names using:
37
+ - `forge-` prefix
38
+ - `/` replaced by `-`
39
+ - `.md` removed
40
+
41
+ Examples:
42
+
43
+ | Shared command spec | Installed Codex skill |
44
+ |---|---|
45
+ | `commands/work-on.md` | `forge-work-on` |
46
+ | `commands/review-pr.md` | `forge-review-pr` |
47
+ | `commands/orchestrate.md` | `forge-orchestrate` |
48
+ | `commands/work-on/investigate.md` | `forge-work-on-investigate` |
49
+ | `commands/work-on/build/context.md` | `forge-work-on-build-context` |
50
+
51
+ `commands/review-pr-agents.md` is excluded because it is a read-on-demand catalog, not a standalone entrypoint.
52
+
53
+ ## How the Adapter Works
54
+
55
+ Each generated skill instructs Codex to:
56
+ - read the shared command spec from `commands/`
57
+ - translate Claude-specific workflow assumptions into Codex-native tools
58
+ - preserve GitHub labels, structured comments, worktree conventions, and workflow sequencing
59
+ - continue with the closest Codex-native execution path when a Claude-only affordance does not exist
60
+
61
+ Translation rules:
62
+ - Claude slash command invocation -> installed `forge-*` skill
63
+ - `Skill("x")` -> corresponding `forge-*` skill when available, otherwise direct continuation from the referenced source file
64
+ - `Agent(...)` / `Task(...)` -> Codex sub-agents when the source workflow genuinely benefits from orchestration
65
+ - `Bash`, `Read`, `Grep`, `Glob` -> Codex shell/file tools with `gh`, `git`, `rg`, `sed`, `find`, and repo scripts
66
+ - `WebFetch` -> web tooling or API calls via `gh` / `curl`
67
+
68
+ When a repo-local adapter exists under `.agents/skills/`, it becomes authoritative for that command in this repo. Use that layer for repo-specific overrides such as:
69
+ - different default GitHub repo resolution
70
+ - Codex-native worktree roots
71
+ - Forge-specific review heuristics
72
+ - best-effort handling of project-board steps that are AlterLab-specific upstream
73
+
74
+ ## Trust and Global Config
75
+
76
+ Codex may require the repo directory to be trusted in `~/.codex/config.toml`.
77
+
78
+ If Forge is not already trusted, add a project entry like:
79
+
80
+ ```toml
81
+ [projects."/absolute/path/to/forge"]
82
+ trust_level = "trusted"
83
+ ```
84
+
85
+ The installer does not modify global trust settings automatically.
86
+
87
+ ## Usage Notes
88
+
89
+ - Use the installed `forge` skill as the overview/router entrypoint.
90
+ - Use command-specific skills such as `forge-work-on` or `forge-review-pr` when the task is already clear.
91
+ - Keep `CLAUDE.md` and `AGENTS.md` aligned; Claude and Codex can differ at the wrapper layer, but not on Forge workflow invariants.
92
+ - For workflow paths that have repo-local adapters, prefer those adapters over the generic generated behavior.
@@ -0,0 +1,104 @@
1
+ # Workflow Reference
2
+
3
+ Canonical reference for field IDs, option IDs, and integration constants used by Forge commands.
4
+
5
+ ---
6
+
7
+ ## Project Board Integration
8
+
9
+ **Project ID**: `PVT_kwHOCx3gR84BSK2L`
10
+
11
+ All commands that add issues to the GitHub Project board use the field and option IDs below.
12
+ Reference this document rather than hardcoding values inline.
13
+
14
+ ### Field IDs
15
+
16
+ | Field | Field ID |
17
+ |-------|----------|
18
+ | Status | `PVTSSF_lAHOCx3gR84BSK2Lzg_yF6E` |
19
+ | Lane | `PVTSSF_lAHOCx3gR84BSK2Lzg_yF98` |
20
+ | Component | `PVTSSF_lAHOCx3gR84BSK2Lzg_yF-o` |
21
+ | Priority | `PVTSSF_lAHOCx3gR84BSK2Lzg_yF8o` |
22
+ | Workflow | `PVTSSF_lAHOCx3gR84BSK2Lzg_yGAA` |
23
+
24
+ ### Status Field Options (`PVTSSF_lAHOCx3gR84BSK2Lzg_yF6E`)
25
+
26
+ | Value | Option ID |
27
+ |-------|-----------|
28
+ | Todo | `f75ad846` |
29
+ | In Progress | _(query live if needed)_ |
30
+ | Done | `98236657` |
31
+
32
+ ### Lane Field Options (`PVTSSF_lAHOCx3gR84BSK2Lzg_yF98`)
33
+
34
+ | Value | Option ID |
35
+ |-------|-----------|
36
+ | Fast | `62864af4` |
37
+ | Feature | `4ff6f9e6` |
38
+ | Sync | `c0c37d33` |
39
+
40
+ ### Component Field Options (`PVTSSF_lAHOCx3gR84BSK2Lzg_yF-o`)
41
+
42
+ | Value | Option ID | Repo |
43
+ |-------|-----------|------|
44
+ | Platform | `214c4d65` | `RapierCraft/AlterLab` |
45
+ | MCP Server | _(query live if needed)_ | `RapierCraft/alterlab-mcp-server` |
46
+ | n8n Node | _(query live if needed)_ | `RapierCraft/n8n-nodes-alterlab` |
47
+ | Python SDK | _(query live if needed)_ | `RapierCraft/alterlab-python` |
48
+ | Node SDK | _(query live if needed)_ | `RapierCraft/alterlab-node` |
49
+
50
+ ### Priority Field Options (`PVTSSF_lAHOCx3gR84BSK2Lzg_yF8o`)
51
+
52
+ | Value | Option ID |
53
+ |-------|-----------|
54
+ | P0 | _(query live if needed)_ |
55
+ | P1 | _(query live if needed)_ |
56
+ | P2 | `4d95eef3` |
57
+ | P3 | _(query live if needed)_ |
58
+
59
+ ### Workflow Field Options (`PVTSSF_lAHOCx3gR84BSK2Lzg_yGAA`)
60
+
61
+ | Value | Option ID |
62
+ |-------|-----------|
63
+ | Investigating | _(query live if needed)_ |
64
+ | Building | _(query live if needed)_ |
65
+ | In Review | _(query live if needed)_ |
66
+ | Merged | `b510c537` |
67
+
68
+ ### Usage Pattern
69
+
70
+ ```bash
71
+ # Add issue to project board and set initial fields
72
+ ITEM_ID=$(gh project item-add PVT_kwHOCx3gR84BSK2L --owner RapierCraft --url "$ISSUE_URL" --format json --jq '.id')
73
+
74
+ # Set Status=Todo
75
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
76
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yF6E --single-select-option-id f75ad846 2>/dev/null || true
77
+
78
+ # Set Lane (Fast=62864af4, Feature=4ff6f9e6, Sync=c0c37d33)
79
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
80
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yF98 --single-select-option-id 62864af4 2>/dev/null || true
81
+
82
+ # Set Component=Platform
83
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
84
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yF-o --single-select-option-id 214c4d65 2>/dev/null || true
85
+
86
+ # Set Priority (P2=4d95eef3)
87
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
88
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yF8o --single-select-option-id 4d95eef3 2>/dev/null || true
89
+
90
+ # On merge: set Status=Done and Workflow=Merged
91
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
92
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yF6E --single-select-option-id 98236657 2>/dev/null || true
93
+ gh project item-edit --project-id PVT_kwHOCx3gR84BSK2L --id "$ITEM_ID" \
94
+ --field-id PVTSSF_lAHOCx3gR84BSK2Lzg_yGAA --single-select-option-id b510c537 2>/dev/null || true
95
+ ```
96
+
97
+ ### Querying Unknown Option IDs
98
+
99
+ For option IDs marked "_(query live if needed)_", use:
100
+
101
+ ```bash
102
+ gh project field-list PVT_kwHOCx3gR84BSK2L --owner RapierCraft --format json \
103
+ --jq '.fields[] | select(.name == "Status") | .options'
104
+ ```
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "forgedock",
3
+ "version": "1.0.0",
4
+ "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
+ "bin": {
6
+ "forgedock": "./bin/forgedock.mjs"
7
+ },
8
+ "keywords": [
9
+ "claude-code",
10
+ "ai-agents",
11
+ "autonomous-coding",
12
+ "github-automation",
13
+ "developer-tools",
14
+ "ai-pipeline",
15
+ "code-review",
16
+ "llm",
17
+ "knowledge-graph",
18
+ "claude",
19
+ "anthropic",
20
+ "agentic-ai",
21
+ "devtools",
22
+ "automation"
23
+ ],
24
+ "author": "RapierCraft Studios",
25
+ "license": "AGPL-3.0",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/RapierCraftStudios/ForgeDock.git"
29
+ },
30
+ "homepage": "https://github.com/RapierCraftStudios/ForgeDock",
31
+ "bugs": {
32
+ "url": "https://github.com/RapierCraftStudios/ForgeDock/issues"
33
+ },
34
+ "files": [
35
+ "bin/",
36
+ "commands/",
37
+ "docs/",
38
+ "scripts/",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=18"
44
+ }
45
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env bash
2
+ # verify-env-vars.sh — Check that new environment variables are properly wired
3
+ #
4
+ # Usage: verify-env-vars.sh <pr_diff_file> <repo_root>
5
+ # pr_diff_file: path to file containing unified diff output (or "-" for stdin)
6
+ # repo_root: path to the repository root
7
+ #
8
+ # Output: Structured findings (one per line, prefixed with severity)
9
+ # BLOCKING: <message> — must fix before merge
10
+ # WARNING: <message> — likely issue, verify manually
11
+ # OK: <message> — check passed
12
+ #
13
+ # Exit codes: 0 = all checks passed, 1 = blocking findings, 2 = warnings only
14
+
15
+ set -euo pipefail
16
+
17
+ DIFF_INPUT="${1:--}"
18
+ REPO_ROOT="${2:-.}"
19
+
20
+ if [ "$DIFF_INPUT" = "-" ]; then
21
+ DIFF_CONTENT=$(cat)
22
+ else
23
+ DIFF_CONTENT=$(cat "$DIFF_INPUT")
24
+ fi
25
+
26
+ BLOCKING=0
27
+ WARNINGS=0
28
+
29
+ # Extract new env var references from added lines in the diff (Python + Node.js)
30
+ NEW_ENVS=$(echo "$DIFF_CONTENT" | grep -E "^\+" | grep -oE 'os\.getenv\("[^"]+"\)|os\.environ\["[^"]+"\]|os\.environ\.get\("[^"]+"\)|process\.env\.[A-Z_]+' | sort -u)
31
+
32
+ # --- Docker Compose entrypoint/command env var wiring check ---
33
+ # When docker-compose files change entrypoint: or command: to reference shell vars,
34
+ # those vars must be available inside the container via environment: or env_file:.
35
+ # Compose interpolation (${VAR} in YAML) happens at parse time on the host.
36
+ # Shell vars (${VAR} in entrypoint scripts) need runtime injection via environment:.
37
+ # Mixing these up causes silent failures — the var is empty inside the container.
38
+ #
39
+ # This check was added after the Redis incident (forge #185): PR #14554 migrated Redis
40
+ # from command: args (Compose interpolation) to entrypoint.sh (container runtime),
41
+ # but never added environment: to pass REDIS_PASSWORD into the container.
42
+
43
+ COMPOSE_ENV_BLOCKING=0
44
+ COMPOSE_FILES=$(echo "$DIFF_CONTENT" | grep -E "^\+\+\+ b/" | grep -oE 'docker-compose[^[:space:]]*\.yml' | sort -u)
45
+
46
+ if [ -n "$COMPOSE_FILES" ]; then
47
+ echo "=== Docker Compose Entrypoint/Command Env Var Wiring ==="
48
+
49
+ for compose_file in $COMPOSE_FILES; do
50
+ COMPOSE_PATH="$REPO_ROOT/$compose_file"
51
+ [ -f "$COMPOSE_PATH" ] || continue
52
+
53
+ # Find services that have entrypoint: or command: referencing scripts
54
+ # (not inline ${VAR} which is Compose interpolation — those are fine)
55
+ # We look for entrypoint lines pointing to .sh files
56
+ ENTRYPOINT_SCRIPTS=$(grep -A1 'entrypoint:' "$COMPOSE_PATH" 2>/dev/null \
57
+ | grep -oE '/[a-zA-Z0-9_./-]+\.sh' | sort -u)
58
+
59
+ for script_ref in $ENTRYPOINT_SCRIPTS; do
60
+ # Find the script file in the repo
61
+ SCRIPT_PATH=$(find "$REPO_ROOT" -path "*${script_ref}" -type f 2>/dev/null | head -1)
62
+ [ -n "$SCRIPT_PATH" ] || continue
63
+
64
+ # Extract ${VAR} and $VAR references from the script (skip comments)
65
+ SCRIPT_VARS=$(grep -v '^\s*#' "$SCRIPT_PATH" 2>/dev/null \
66
+ | grep -oE '\$\{?[A-Z][A-Z_0-9]{2,}\}?' \
67
+ | sed 's/[${}]//g' \
68
+ | sort -u)
69
+
70
+ [ -n "$SCRIPT_VARS" ] || continue
71
+
72
+ # Determine which service this entrypoint belongs to
73
+ # (search backward from the entrypoint line to find the service name)
74
+ SERVICE_NAME=$(awk "/entrypoint:.*$(basename "$script_ref")/{found=1} found && /^ [a-z]/{print; exit}" "$COMPOSE_PATH" 2>/dev/null \
75
+ | sed 's/://; s/^ //' || echo "unknown")
76
+ # Fallback: extract service by finding the indented block containing this entrypoint
77
+ if [ "$SERVICE_NAME" = "unknown" ] || [ -z "$SERVICE_NAME" ]; then
78
+ SERVICE_NAME=$(awk '/^ [a-z_-]+:/{svc=$1} /entrypoint:.*'"$(basename "$script_ref")"'/{print svc; exit}' "$COMPOSE_PATH" 2>/dev/null \
79
+ | sed 's/://')
80
+ fi
81
+
82
+ # Extract vars from this service's environment: section
83
+ SERVICE_ENV_VARS=$(awk "/^ ${SERVICE_NAME}:/,/^ [a-z]/" "$COMPOSE_PATH" 2>/dev/null \
84
+ | awk '/environment:/,/^ [a-z]|^ [a-z]/' \
85
+ | grep -oE '[A-Z][A-Z_0-9]{2,}' \
86
+ | sort -u)
87
+
88
+ # Also check env_file: — if present, vars come from .env or specified file
89
+ HAS_ENV_FILE=$(awk "/^ ${SERVICE_NAME}:/,/^ [a-z]/" "$COMPOSE_PATH" 2>/dev/null \
90
+ | grep -c 'env_file:' || true)
91
+
92
+ for var in $SCRIPT_VARS; do
93
+ # Skip common shell vars that aren't env config
94
+ case "$var" in
95
+ PATH|HOME|USER|SHELL|PWD|TERM|HOSTNAME|LANG) continue ;;
96
+ esac
97
+
98
+ IN_ENV=$(echo "$SERVICE_ENV_VARS" | grep -cx "$var" || true)
99
+
100
+ if [ "$IN_ENV" -eq 0 ] && [ "$HAS_ENV_FILE" -eq 0 ]; then
101
+ echo "BLOCKING: $var used in entrypoint $script_ref (service: ${SERVICE_NAME:-unknown}) but NOT in service environment: section of $compose_file — container will not receive this var at runtime"
102
+ COMPOSE_ENV_BLOCKING=$((COMPOSE_ENV_BLOCKING + 1))
103
+ elif [ "$IN_ENV" -eq 0 ] && [ "$HAS_ENV_FILE" -gt 0 ]; then
104
+ echo "WARNING: $var used in entrypoint $script_ref (service: ${SERVICE_NAME:-unknown}) — not in explicit environment: but service has env_file: — verify var is in the referenced env file"
105
+ WARNINGS=$((WARNINGS + 1))
106
+ else
107
+ echo "OK: $var — in environment: section of service ${SERVICE_NAME:-unknown}"
108
+ fi
109
+ done
110
+ done
111
+ done
112
+
113
+ if [ "$COMPOSE_ENV_BLOCKING" -eq 0 ] && [ -n "$COMPOSE_FILES" ]; then
114
+ echo "OK: No docker-compose entrypoint env var wiring issues found"
115
+ fi
116
+ echo ""
117
+ fi
118
+
119
+ BLOCKING=$((BLOCKING + COMPOSE_ENV_BLOCKING))
120
+
121
+ if [ -z "$NEW_ENVS" ] && [ "$COMPOSE_ENV_BLOCKING" -eq 0 ]; then
122
+ echo "OK: No new environment variable references detected in diff"
123
+ exit 0
124
+ fi
125
+
126
+ if [ -n "$NEW_ENVS" ]; then
127
+ echo "=== New Environment Variable References ==="
128
+ echo "$NEW_ENVS"
129
+ echo ""
130
+ fi
131
+
132
+ # Extract just the variable names from Python/Node references
133
+ VAR_NAMES=$(echo "$NEW_ENVS" | grep -oE '[A-Z][A-Z_0-9]{2,}' | sort -u)
134
+
135
+ # If no Python/Node env vars found, skip to summary (compose checks already ran above)
136
+ if [ -z "$VAR_NAMES" ]; then
137
+ echo ""
138
+ echo "=== Summary ==="
139
+ echo "Python/Node vars checked: 0"
140
+ echo "Compose entrypoint vars blocking: $COMPOSE_ENV_BLOCKING"
141
+ echo "Blocking: $BLOCKING"
142
+ echo "Warnings: $WARNINGS"
143
+
144
+ if [ "$BLOCKING" -gt 0 ]; then
145
+ exit 1
146
+ elif [ "$WARNINGS" -gt 0 ]; then
147
+ exit 2
148
+ fi
149
+ exit 0
150
+ fi
151
+
152
+ for var in $VAR_NAMES; do
153
+ echo "--- Checking: $var ---"
154
+ FOUND_IN=""
155
+
156
+ # Check .env.example
157
+ if [ -f "$REPO_ROOT/.env.example" ] && grep -q "$var" "$REPO_ROOT/.env.example" 2>/dev/null; then
158
+ FOUND_IN="${FOUND_IN} .env.example"
159
+ fi
160
+
161
+ # Check docker-compose.yml
162
+ if [ -f "$REPO_ROOT/docker-compose.yml" ] && grep -q "$var" "$REPO_ROOT/docker-compose.yml" 2>/dev/null; then
163
+ FOUND_IN="${FOUND_IN} docker-compose.yml"
164
+ fi
165
+
166
+ # Check docker-compose.prod.yml
167
+ if [ -f "$REPO_ROOT/docker-compose.prod.yml" ] && grep -q "$var" "$REPO_ROOT/docker-compose.prod.yml" 2>/dev/null; then
168
+ FOUND_IN="${FOUND_IN} docker-compose.prod.yml"
169
+ fi
170
+
171
+ # Check env_validation.py (Python API vars)
172
+ if [ -f "$REPO_ROOT/services/api/app/core/env_validation.py" ] && grep -q "$var" "$REPO_ROOT/services/api/app/core/env_validation.py" 2>/dev/null; then
173
+ FOUND_IN="${FOUND_IN} env_validation.py"
174
+ fi
175
+
176
+ # Check decrypt-secrets.sh (SOPS mapping)
177
+ if [ -f "$REPO_ROOT/scripts/decrypt-secrets.sh" ] && grep -q "$var" "$REPO_ROOT/scripts/decrypt-secrets.sh" 2>/dev/null; then
178
+ FOUND_IN="${FOUND_IN} decrypt-secrets.sh"
179
+ fi
180
+
181
+ if [ -z "$FOUND_IN" ]; then
182
+ echo "BLOCKING: $var — not found in .env.example, docker-compose.yml, or env_validation.py"
183
+ BLOCKING=$((BLOCKING + 1))
184
+ else
185
+ echo "OK: $var — found in:$FOUND_IN"
186
+ fi
187
+ done
188
+
189
+ echo ""
190
+ echo "=== Summary ==="
191
+ echo "Variables checked: $(echo "$VAR_NAMES" | wc -w)"
192
+ echo "Blocking: $BLOCKING"
193
+ echo "Warnings: $WARNINGS"
194
+
195
+ # --- Exit code ---
196
+ if [ "$BLOCKING" -gt 0 ]; then
197
+ exit 1
198
+ elif [ "$WARNINGS" -gt 0 ]; then
199
+ exit 2
200
+ fi
201
+ exit 0
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # verify-host-headers.sh — Check that shell scripts using curl/wget set proper Host headers
3
+ #
4
+ # Usage: verify-host-headers.sh <changed_files_file> <repo_root>
5
+ # changed_files_file: path to a file listing changed shell scripts (one per line), or "-" for stdin
6
+ # repo_root: path to the repository root
7
+ #
8
+ # Output: Structured findings (one per line, prefixed with severity)
9
+ # BLOCKING: <message> — must fix before merge
10
+ # WARNING: <message> — likely issue, verify manually
11
+ # OK: <message> — check passed
12
+ #
13
+ # Exit codes: 0 = all checks passed, 1 = blocking findings, 2 = warnings only
14
+
15
+ set -euo pipefail
16
+
17
+ CHANGED_FILES="${1:--}"
18
+ REPO_ROOT="${2:-.}"
19
+
20
+ if [ "$CHANGED_FILES" = "-" ]; then
21
+ FILES=$(cat)
22
+ else
23
+ FILES=$(cat "$CHANGED_FILES")
24
+ fi
25
+
26
+ # Filter to only shell scripts
27
+ SHELL_FILES=$(echo "$FILES" | grep -E '\.(sh|bash)$' || true)
28
+
29
+ if [ -z "$SHELL_FILES" ]; then
30
+ echo "OK: No shell scripts in changed files"
31
+ exit 0
32
+ fi
33
+
34
+ BLOCKING=0
35
+ WARNINGS=0
36
+
37
+ # Internal service patterns — requests to these need explicit Host headers
38
+ INTERNAL_PATTERNS="localhost|127\.0\.0\.1|api-|worker-|alterlab-|172\.[0-9]+\.[0-9]+\.[0-9]+|\\\$\{?[a-z_]*ip|\\\$\{?[A-Z_]*IP"
39
+
40
+ echo "$SHELL_FILES" | while read -r f; do
41
+ FILEPATH="$REPO_ROOT/$f"
42
+ if [ ! -f "$FILEPATH" ]; then
43
+ continue
44
+ fi
45
+
46
+ # Find curl/wget commands targeting internal services
47
+ CURL_LINES=$(grep -nE "(curl|wget)" "$FILEPATH" 2>/dev/null | grep -E "$INTERNAL_PATTERNS" || true)
48
+
49
+ if [ -z "$CURL_LINES" ]; then
50
+ echo "OK: $f — no internal service HTTP calls found"
51
+ continue
52
+ fi
53
+
54
+ echo "=== Checking: $f ==="
55
+ echo "$CURL_LINES" | while IFS= read -r line; do
56
+ LINENO=$(echo "$line" | cut -d: -f1)
57
+ CONTENT=$(echo "$line" | cut -d: -f2-)
58
+
59
+ # Check if a Host header is explicitly set
60
+ if echo "$CONTENT" | grep -qE "\-H\s+[\"']Host:"; then
61
+ echo "OK: $f:$LINENO — Host header explicitly set"
62
+ elif echo "$CONTENT" | grep -qE "\-\-header\s+[\"']Host:"; then
63
+ echo "OK: $f:$LINENO — Host header explicitly set (--header)"
64
+ else
65
+ echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. TrustedHostMiddleware will reject raw IP as Host value."
66
+ echo " Line: $CONTENT"
67
+ echo " Fix: Add -H 'Host: localhost' (or appropriate allowed host from services/api/app/main.py allowed_hosts)"
68
+ BLOCKING=$((BLOCKING + 1))
69
+ fi
70
+ done
71
+ done || true
72
+
73
+ # --- Client-side proxy bypass check ---
74
+ # Check frontend files for direct /api/v1/ calls (bypasses Next.js auth proxy)
75
+ FRONTEND_FILES=$(echo "$FILES" | grep -E '\.(tsx?|jsx?)$' | grep -v 'route\.ts$' | grep -v '\.d\.ts$' || true)
76
+
77
+ if [ -n "$FRONTEND_FILES" ]; then
78
+ echo ""
79
+ echo "=== Client-Side Proxy Bypass Check ==="
80
+ echo "$FRONTEND_FILES" | while read -r f; do
81
+ FILEPATH="$REPO_ROOT/$f"
82
+ if [ ! -f "$FILEPATH" ]; then
83
+ continue
84
+ fi
85
+
86
+ # Check for direct /api/v1/ calls in fetch/useSWR/apiFetch
87
+ DIRECT_CALLS=$(grep -nE "(fetch|useSWR|apiFetch|adminFetcher)\s*[(<]\s*[\`\"']/api/v1/" "$FILEPATH" 2>/dev/null || true)
88
+ if [ -z "$DIRECT_CALLS" ]; then
89
+ # Also check template literals
90
+ DIRECT_CALLS=$(grep -nE '`/api/v1/' "$FILEPATH" 2>/dev/null | grep -vE '^\s*//' || true)
91
+ fi
92
+
93
+ if [ -n "$DIRECT_CALLS" ]; then
94
+ echo "BLOCKING: $f — direct /api/v1/ call bypasses Next.js proxy (auth will fail)"
95
+ echo "$DIRECT_CALLS" | while IFS= read -r match; do
96
+ echo " $match"
97
+ done
98
+ echo " Fix: Use /api/* proxy routes instead of /api/v1/* direct FastAPI calls"
99
+ BLOCKING=$((BLOCKING + 1))
100
+ else
101
+ echo "OK: $f — no direct backend calls"
102
+ fi
103
+ done
104
+ fi
105
+
106
+ echo ""
107
+ echo "=== Summary ==="
108
+ echo "Blocking: $BLOCKING"
109
+ echo "Warnings: $WARNINGS"
110
+
111
+ # --- Exit code ---
112
+ if [ "$BLOCKING" -gt 0 ]; then
113
+ exit 1
114
+ elif [ "$WARNINGS" -gt 0 ]; then
115
+ exit 2
116
+ fi
117
+ exit 0
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # verify-route-registration.sh — Check that route handlers and API routers are registered
3
+ #
4
+ # Usage: verify-route-registration.sh <changed_files_file> <repo_root>
5
+ # changed_files_file: path to a file listing changed files (one per line)
6
+ # repo_root: path to the repository root
7
+ #
8
+ # Output: Structured findings (one per line, prefixed with severity)
9
+ # BLOCKING: <message> — must fix before merge
10
+ # WARNING: <message> — likely issue, verify manually
11
+ # OK: <message> — check passed
12
+ #
13
+ # Exit codes: 0 = all checks passed, 1 = blocking findings, 2 = warnings only
14
+
15
+ set -euo pipefail
16
+
17
+ CHANGED_FILES="${1:--}"
18
+ REPO_ROOT="${2:-.}"
19
+
20
+ if [ "$CHANGED_FILES" = "-" ]; then
21
+ FILES=$(cat)
22
+ else
23
+ FILES=$(cat "$CHANGED_FILES")
24
+ fi
25
+
26
+ BLOCKING=0
27
+ WARNINGS=0
28
+
29
+ # --- Next.js Route Handler registration check ---
30
+ echo "$FILES" | grep -E "^web/src/app/api/.*route\.ts$" | while read -r f; do
31
+ URL_PATH=$(echo "$f" | sed 's|^web/src/app||; s|/route\.ts$||; s|\[.*\]|*|g')
32
+ ROUTE_SEGMENT=$(echo "$URL_PATH" | sed 's|/api/v1/||; s|/.*||')
33
+
34
+ # Check if next.config.js has a rewrite that might shadow this route
35
+ if [ -f "$REPO_ROOT/web/next.config.js" ]; then
36
+ if grep -q "$ROUTE_SEGMENT" "$REPO_ROOT/web/next.config.js" 2>/dev/null; then
37
+ echo "WARNING: Route handler $f ($URL_PATH) — next.config.js references '$ROUTE_SEGMENT', may shadow this route"
38
+ WARNINGS=$((WARNINGS + 1))
39
+ else
40
+ echo "OK: Route handler $f ($URL_PATH) — no shadowing rewrites found in next.config.js"
41
+ fi
42
+ fi
43
+
44
+ # Check if nginx routes this path to Next.js or directly to backend
45
+ if [ -f "$REPO_ROOT/infra/nginx/nginx.conf" ]; then
46
+ if grep -q "location.*$ROUTE_SEGMENT" "$REPO_ROOT/infra/nginx/nginx.conf" 2>/dev/null; then
47
+ echo "WARNING: Route handler $f ($URL_PATH) — nginx.conf has a location block for '$ROUTE_SEGMENT', may bypass Next.js"
48
+ WARNINGS=$((WARNINGS + 1))
49
+ fi
50
+ fi
51
+ done || true
52
+
53
+ # --- Python API Router registration check ---
54
+ echo "$FILES" | grep -E "^services/api/app/routers/.*\.py$" | while read -r f; do
55
+ ROUTER_NAME=$(basename "$f" .py)
56
+ MAIN_PY="$REPO_ROOT/services/api/app/main.py"
57
+
58
+ if [ -f "$MAIN_PY" ]; then
59
+ if grep -q "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
60
+ echo "OK: API Router '$ROUTER_NAME' ($f) — registered in main.py"
61
+ else
62
+ echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in main.py. Router will not be reachable."
63
+ BLOCKING=$((BLOCKING + 1))
64
+ fi
65
+ else
66
+ echo "WARNING: Cannot verify router registration — main.py not found at $MAIN_PY"
67
+ WARNINGS=$((WARNINGS + 1))
68
+ fi
69
+ done || true
70
+
71
+ # --- Python Middleware registration check ---
72
+ echo "$FILES" | grep -E "^services/api/app/middleware/.*\.py$" | while read -r f; do
73
+ MIDDLEWARE_NAME=$(basename "$f" .py)
74
+ MAIN_PY="$REPO_ROOT/services/api/app/main.py"
75
+
76
+ if [ -f "$MAIN_PY" ]; then
77
+ if grep -q "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
78
+ echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in main.py"
79
+ else
80
+ echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in main.py. May not be active."
81
+ WARNINGS=$((WARNINGS + 1))
82
+ fi
83
+ fi
84
+ done || true
85
+
86
+ # --- Shared module import check ---
87
+ echo "$FILES" | grep -E "^shared/.*\.py$" | while read -r f; do
88
+ MODULE_NAME=$(basename "$f" .py)
89
+
90
+ IMPORTERS=$(grep -rl "$MODULE_NAME" "$REPO_ROOT/services/api/" "$REPO_ROOT/services/worker/" 2>/dev/null | head -5)
91
+ if [ -n "$IMPORTERS" ]; then
92
+ echo "OK: Shared module '$MODULE_NAME' ($f) — imported by services"
93
+ else
94
+ echo "WARNING: Shared module '$MODULE_NAME' ($f) — no imports found in services/api/ or services/worker/"
95
+ WARNINGS=$((WARNINGS + 1))
96
+ fi
97
+ done || true
98
+
99
+ # --- Component import check ---
100
+ echo "$FILES" | grep -E "^web/src/components/.*\.tsx$" | while read -r f; do
101
+ COMPONENT_NAME=$(basename "$f" .tsx)
102
+
103
+ IMPORTERS=$(grep -rl "$COMPONENT_NAME" "$REPO_ROOT/web/src/app/" "$REPO_ROOT/web/src/components/" 2>/dev/null | grep -v "$f" | head -3)
104
+ if [ -n "$IMPORTERS" ]; then
105
+ echo "OK: Component '$COMPONENT_NAME' ($f) — imported by other files"
106
+ else
107
+ echo "WARNING: Component '$COMPONENT_NAME' ($f) — no imports found. May be unused or new."
108
+ WARNINGS=$((WARNINGS + 1))
109
+ fi
110
+ done || true
111
+
112
+ # --- Exit code ---
113
+ if [ "$BLOCKING" -gt 0 ]; then
114
+ exit 1
115
+ elif [ "$WARNINGS" -gt 0 ]; then
116
+ exit 2
117
+ fi
118
+ exit 0