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.
- package/LICENSE +244 -0
- package/README.md +251 -0
- package/bin/forgedock.mjs +232 -0
- package/commands/analytics.md +777 -0
- package/commands/audit-agents.md +410 -0
- package/commands/audit.md +410 -0
- package/commands/autopilot.md +432 -0
- package/commands/cleanup.md +264 -0
- package/commands/deploy-info.md +292 -0
- package/commands/failure-recon.md +375 -0
- package/commands/forge-stats.md +183 -0
- package/commands/geo-audit.md +343 -0
- package/commands/incident-response.md +384 -0
- package/commands/issue.md +506 -0
- package/commands/milestone.md +599 -0
- package/commands/orchestrate.md +1044 -0
- package/commands/pipeline-health.md +2343 -0
- package/commands/qa-sweep.md +237 -0
- package/commands/quality-gate.md +777 -0
- package/commands/review-pr-agents.md +1711 -0
- package/commands/review-pr-staging.md +325 -0
- package/commands/review-pr.md +888 -0
- package/commands/rollback.md +253 -0
- package/commands/security-audit.md +529 -0
- package/commands/sync-ecosystem.md +527 -0
- package/commands/validate.md +131 -0
- package/commands/work-on/build/architect.md +291 -0
- package/commands/work-on/build/context.md +349 -0
- package/commands/work-on/build/implement.md +280 -0
- package/commands/work-on/build/validate.md +292 -0
- package/commands/work-on/build.md +262 -0
- package/commands/work-on/close.md +350 -0
- package/commands/work-on/decompose.md +254 -0
- package/commands/work-on/investigate.md +437 -0
- package/commands/work-on/review.md +245 -0
- package/commands/work-on-monolithic.md +298 -0
- package/commands/work-on.md +1089 -0
- package/docs/CODEX.md +92 -0
- package/docs/WORKFLOW.md +104 -0
- package/package.json +45 -0
- package/scripts/verify-env-vars.sh +201 -0
- package/scripts/verify-host-headers.sh +117 -0
- package/scripts/verify-route-registration.sh +118 -0
- 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.
|
package/docs/WORKFLOW.md
ADDED
|
@@ -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
|