feed-the-machine 1.5.0 → 1.6.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 +21 -21
- package/README.md +170 -170
- package/bin/generate-manifest.mjs +463 -463
- package/bin/install.mjs +491 -491
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +122 -122
- package/ftm-audit/SKILL.md +623 -541
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +498 -498
- package/ftm-brainstorm/evals/evals.json +100 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +224 -224
- package/ftm-brainstorm/references/plan-template.md +121 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +345 -345
- package/ftm-config.default.yml +82 -80
- package/ftm-config.yml +2 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -767
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -44
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +1943 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +296 -296
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +23 -23
- package/ftm-state/blackboard/experiences/index.json +9 -9
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- package/uninstall.sh +25 -25
|
@@ -1,139 +1,139 @@
|
|
|
1
|
-
# Remediation Protocol — Auto-Fix Steps
|
|
2
|
-
|
|
3
|
-
Detailed remediation steps for secrets found during Phase 1–2 scanning. Apply in sequence for each finding.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Phase 3: Auto-Remediate
|
|
8
|
-
|
|
9
|
-
For each finding, apply the appropriate fix automatically. The goal is to make the code safe without breaking functionality.
|
|
10
|
-
|
|
11
|
-
### Step 1: Ensure .env infrastructure exists
|
|
12
|
-
|
|
13
|
-
Check for a `.env` file in the project root. If it doesn't exist, create one with a header comment:
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
# Environment variables — DO NOT COMMIT THIS FILE
|
|
17
|
-
# Copy .env.example for the template, fill in real values locally
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Check `.gitignore` for `.env` coverage. If missing, add:
|
|
21
|
-
```
|
|
22
|
-
# Environment files with secrets
|
|
23
|
-
.env
|
|
24
|
-
.env.local
|
|
25
|
-
.env.production
|
|
26
|
-
.env.staging
|
|
27
|
-
.env.*.local
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Step 2: Extract secrets to .env
|
|
31
|
-
|
|
32
|
-
For each finding:
|
|
33
|
-
|
|
34
|
-
1. **Choose an env var name** — derive it from the context. If the code says `STRIPE_API_KEY = "sk_live_..."`, the env var is `STRIPE_API_KEY`. If it says `api_key: "AIza..."`, infer from the file/service context (e.g., `GOOGLE_API_KEY`). Use SCREAMING_SNAKE_CASE.
|
|
35
|
-
|
|
36
|
-
2. **Add to .env** — append `VAR_NAME=<actual-secret-value>` to `.env`. If the var already exists, don't duplicate it.
|
|
37
|
-
|
|
38
|
-
3. **Add to .env.example** — create or update `.env.example` with `VAR_NAME=your-value-here` so other developers know the variable exists without seeing the real value.
|
|
39
|
-
|
|
40
|
-
### Step 3: Refactor source files
|
|
41
|
-
|
|
42
|
-
Replace the hardcoded secret with an env var reference. Match the language/framework:
|
|
43
|
-
|
|
44
|
-
| Language | Pattern |
|
|
45
|
-
|---|---|
|
|
46
|
-
| Python | `os.environ["VAR_NAME"]` or `os.getenv("VAR_NAME")` (match existing style in file) |
|
|
47
|
-
| JavaScript/TypeScript | `process.env.VAR_NAME` |
|
|
48
|
-
| Ruby | `ENV["VAR_NAME"]` or `ENV.fetch("VAR_NAME")` |
|
|
49
|
-
| Go | `os.Getenv("VAR_NAME")` |
|
|
50
|
-
| Java | `System.getenv("VAR_NAME")` |
|
|
51
|
-
| Shell/Bash | `$VAR_NAME` or `${VAR_NAME}` |
|
|
52
|
-
| YAML/JSON config | `${VAR_NAME}` (if the framework supports interpolation) or add a comment pointing to the env var |
|
|
53
|
-
|
|
54
|
-
If the file doesn't already import the env-reading module (e.g., `import os` in Python, `require('dotenv').config()` in Node), add the import. Check if the project uses `python-dotenv`, `dotenv` (Node), or similar — if so, use the project's existing pattern for loading env vars.
|
|
55
|
-
|
|
56
|
-
### Step 4: Unstage remediated files
|
|
57
|
-
|
|
58
|
-
After refactoring, make sure the `.env` file (with real secrets) is NOT staged:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
git reset HEAD .env 2>/dev/null # unstage if accidentally staged
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Stage the refactored source files (which now reference env vars instead of hardcoded secrets):
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
git add <refactored-files>
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Step 5: Verify the fix
|
|
71
|
-
|
|
72
|
-
Re-run Phase 1 scan on the refactored files to confirm the secrets are gone. If any remain, loop back and fix. Do not proceed until the scan is clean.
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## Phase 4: Report
|
|
77
|
-
|
|
78
|
-
After remediation (or if the scan was clean from the start), produce a summary:
|
|
79
|
-
|
|
80
|
-
**Clean scan:**
|
|
81
|
-
```
|
|
82
|
-
ftm-git: Clean scan. 0 secrets found in <N> files scanned. Safe to commit.
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**After remediation:**
|
|
86
|
-
```
|
|
87
|
-
ftm-git: Found <N> hardcoded secrets. Auto-remediated:
|
|
88
|
-
|
|
89
|
-
CRITICAL: sk_live_**** in src/payments.py:42 -> STRIPE_SECRET_KEY
|
|
90
|
-
HIGH: AIza**** in config/google.ts:18 -> GOOGLE_API_KEY
|
|
91
|
-
MEDIUM: .env was not in .gitignore -> added
|
|
92
|
-
|
|
93
|
-
Actions taken:
|
|
94
|
-
- Extracted <N> secrets to .env (gitignored)
|
|
95
|
-
- Created/updated .env.example with placeholder vars
|
|
96
|
-
- Refactored <N> source files to use env var references
|
|
97
|
-
- Updated .gitignore
|
|
98
|
-
|
|
99
|
-
Verify the app still works with the new env var setup, then commit.
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Blocked (auto-fix not possible):**
|
|
103
|
-
|
|
104
|
-
Some secrets can't be auto-fixed — for example, a private key embedded in a binary file, or a secret in a format the skill can't safely refactor. In these cases:
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
ftm-git: BLOCKED. Found secrets that require manual remediation:
|
|
108
|
-
|
|
109
|
-
CRITICAL: Private key in assets/cert.pem:1
|
|
110
|
-
-> Move this file outside the repo and reference via path env var
|
|
111
|
-
|
|
112
|
-
Action required: Fix the above manually, then run ftm-git again.
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Phase 5: Git History Check (Manual Invocation Only)
|
|
118
|
-
|
|
119
|
-
When explicitly asked to do a deep scan (e.g., "scan the repo history for secrets"), also check past commits. This is expensive so it only runs on explicit request, not as part of the pre-commit gate.
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
git log --all --diff-filter=A --name-only --pretty=format:"%H" -- "*.env" "*.pem" "*.key" "*credentials*" "*secret*"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
For each historically added sensitive file, check if it's still in the current tree. If it was added and later removed, warn that the secret is still in git history and suggest:
|
|
126
|
-
|
|
127
|
-
1. Rotate the credential immediately (it's compromised)
|
|
128
|
-
2. Use `git filter-repo` or BFG Repo Cleaner to purge from history if needed
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## Operating Principles
|
|
133
|
-
|
|
134
|
-
1. **Block first, fix second.** Never let a secret through while figuring out the fix. The commit waits.
|
|
135
|
-
2. **Zero false negatives over zero false positives.** It's better to flag something that turns out to be harmless than to miss a real key.
|
|
136
|
-
3. **Never log full secrets.** In all output, mask secret values. Show only enough to identify which secret it is (first 8 + last 4 chars).
|
|
137
|
-
4. **Env vars are the escape hatch.** The remediation pattern is always: secret goes to gitignored .env, code references the env var.
|
|
138
|
-
5. **Existing patterns win.** If the project already uses dotenv, Vault, AWS Secrets Manager, or any other secret management system, match that pattern rather than introducing a new one.
|
|
139
|
-
6. **Test files are not exempt.** A real `sk_live_*` key in a test file is just as dangerous as one in production code. Only `sk_test_*` with obviously fake values get a pass.
|
|
1
|
+
# Remediation Protocol — Auto-Fix Steps
|
|
2
|
+
|
|
3
|
+
Detailed remediation steps for secrets found during Phase 1–2 scanning. Apply in sequence for each finding.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Phase 3: Auto-Remediate
|
|
8
|
+
|
|
9
|
+
For each finding, apply the appropriate fix automatically. The goal is to make the code safe without breaking functionality.
|
|
10
|
+
|
|
11
|
+
### Step 1: Ensure .env infrastructure exists
|
|
12
|
+
|
|
13
|
+
Check for a `.env` file in the project root. If it doesn't exist, create one with a header comment:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
# Environment variables — DO NOT COMMIT THIS FILE
|
|
17
|
+
# Copy .env.example for the template, fill in real values locally
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Check `.gitignore` for `.env` coverage. If missing, add:
|
|
21
|
+
```
|
|
22
|
+
# Environment files with secrets
|
|
23
|
+
.env
|
|
24
|
+
.env.local
|
|
25
|
+
.env.production
|
|
26
|
+
.env.staging
|
|
27
|
+
.env.*.local
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Step 2: Extract secrets to .env
|
|
31
|
+
|
|
32
|
+
For each finding:
|
|
33
|
+
|
|
34
|
+
1. **Choose an env var name** — derive it from the context. If the code says `STRIPE_API_KEY = "sk_live_..."`, the env var is `STRIPE_API_KEY`. If it says `api_key: "AIza..."`, infer from the file/service context (e.g., `GOOGLE_API_KEY`). Use SCREAMING_SNAKE_CASE.
|
|
35
|
+
|
|
36
|
+
2. **Add to .env** — append `VAR_NAME=<actual-secret-value>` to `.env`. If the var already exists, don't duplicate it.
|
|
37
|
+
|
|
38
|
+
3. **Add to .env.example** — create or update `.env.example` with `VAR_NAME=your-value-here` so other developers know the variable exists without seeing the real value.
|
|
39
|
+
|
|
40
|
+
### Step 3: Refactor source files
|
|
41
|
+
|
|
42
|
+
Replace the hardcoded secret with an env var reference. Match the language/framework:
|
|
43
|
+
|
|
44
|
+
| Language | Pattern |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Python | `os.environ["VAR_NAME"]` or `os.getenv("VAR_NAME")` (match existing style in file) |
|
|
47
|
+
| JavaScript/TypeScript | `process.env.VAR_NAME` |
|
|
48
|
+
| Ruby | `ENV["VAR_NAME"]` or `ENV.fetch("VAR_NAME")` |
|
|
49
|
+
| Go | `os.Getenv("VAR_NAME")` |
|
|
50
|
+
| Java | `System.getenv("VAR_NAME")` |
|
|
51
|
+
| Shell/Bash | `$VAR_NAME` or `${VAR_NAME}` |
|
|
52
|
+
| YAML/JSON config | `${VAR_NAME}` (if the framework supports interpolation) or add a comment pointing to the env var |
|
|
53
|
+
|
|
54
|
+
If the file doesn't already import the env-reading module (e.g., `import os` in Python, `require('dotenv').config()` in Node), add the import. Check if the project uses `python-dotenv`, `dotenv` (Node), or similar — if so, use the project's existing pattern for loading env vars.
|
|
55
|
+
|
|
56
|
+
### Step 4: Unstage remediated files
|
|
57
|
+
|
|
58
|
+
After refactoring, make sure the `.env` file (with real secrets) is NOT staged:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git reset HEAD .env 2>/dev/null # unstage if accidentally staged
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Stage the refactored source files (which now reference env vars instead of hardcoded secrets):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git add <refactored-files>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 5: Verify the fix
|
|
71
|
+
|
|
72
|
+
Re-run Phase 1 scan on the refactored files to confirm the secrets are gone. If any remain, loop back and fix. Do not proceed until the scan is clean.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Phase 4: Report
|
|
77
|
+
|
|
78
|
+
After remediation (or if the scan was clean from the start), produce a summary:
|
|
79
|
+
|
|
80
|
+
**Clean scan:**
|
|
81
|
+
```
|
|
82
|
+
ftm-git: Clean scan. 0 secrets found in <N> files scanned. Safe to commit.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**After remediation:**
|
|
86
|
+
```
|
|
87
|
+
ftm-git: Found <N> hardcoded secrets. Auto-remediated:
|
|
88
|
+
|
|
89
|
+
CRITICAL: sk_live_**** in src/payments.py:42 -> STRIPE_SECRET_KEY
|
|
90
|
+
HIGH: AIza**** in config/google.ts:18 -> GOOGLE_API_KEY
|
|
91
|
+
MEDIUM: .env was not in .gitignore -> added
|
|
92
|
+
|
|
93
|
+
Actions taken:
|
|
94
|
+
- Extracted <N> secrets to .env (gitignored)
|
|
95
|
+
- Created/updated .env.example with placeholder vars
|
|
96
|
+
- Refactored <N> source files to use env var references
|
|
97
|
+
- Updated .gitignore
|
|
98
|
+
|
|
99
|
+
Verify the app still works with the new env var setup, then commit.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Blocked (auto-fix not possible):**
|
|
103
|
+
|
|
104
|
+
Some secrets can't be auto-fixed — for example, a private key embedded in a binary file, or a secret in a format the skill can't safely refactor. In these cases:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
ftm-git: BLOCKED. Found secrets that require manual remediation:
|
|
108
|
+
|
|
109
|
+
CRITICAL: Private key in assets/cert.pem:1
|
|
110
|
+
-> Move this file outside the repo and reference via path env var
|
|
111
|
+
|
|
112
|
+
Action required: Fix the above manually, then run ftm-git again.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Phase 5: Git History Check (Manual Invocation Only)
|
|
118
|
+
|
|
119
|
+
When explicitly asked to do a deep scan (e.g., "scan the repo history for secrets"), also check past commits. This is expensive so it only runs on explicit request, not as part of the pre-commit gate.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
git log --all --diff-filter=A --name-only --pretty=format:"%H" -- "*.env" "*.pem" "*.key" "*credentials*" "*secret*"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
For each historically added sensitive file, check if it's still in the current tree. If it was added and later removed, warn that the secret is still in git history and suggest:
|
|
126
|
+
|
|
127
|
+
1. Rotate the credential immediately (it's compromised)
|
|
128
|
+
2. Use `git filter-repo` or BFG Repo Cleaner to purge from history if needed
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Operating Principles
|
|
133
|
+
|
|
134
|
+
1. **Block first, fix second.** Never let a secret through while figuring out the fix. The commit waits.
|
|
135
|
+
2. **Zero false negatives over zero false positives.** It's better to flag something that turns out to be harmless than to miss a real key.
|
|
136
|
+
3. **Never log full secrets.** In all output, mask secret values. Show only enough to identify which secret it is (first 8 + last 4 chars).
|
|
137
|
+
4. **Env vars are the escape hatch.** The remediation pattern is always: secret goes to gitignored .env, code references the env var.
|
|
138
|
+
5. **Existing patterns win.** If the project already uses dotenv, Vault, AWS Secrets Manager, or any other secret management system, match that pattern rather than introducing a new one.
|
|
139
|
+
6. **Test files are not exempt.** A real `sk_live_*` key in a test file is just as dangerous as one in production code. Only `sk_test_*` with obviously fake values get a pass.
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ftm-git pre-commit hook — blocks commits containing hardcoded secrets
|
|
3
|
-
# Installed by the ftm-git skill on first invocation. Safe to remove with:
|
|
4
|
-
# rm .git/hooks/pre-commit (or edit to remove the ftm-git section)
|
|
5
|
-
#
|
|
6
|
-
# This hook scans staged files only (fast). The full ftm-git skill does
|
|
7
|
-
# deeper scanning with context validation and auto-remediation — this is
|
|
8
|
-
# the safety net that catches what slips through.
|
|
9
|
-
|
|
10
|
-
set -euo pipefail
|
|
11
|
-
|
|
12
|
-
RED='\033[0;31m'
|
|
13
|
-
YELLOW='\033[0;33m'
|
|
14
|
-
NC='\033[0m'
|
|
15
|
-
|
|
16
|
-
# Get list of staged files (excluding deletions)
|
|
17
|
-
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
|
|
18
|
-
if [ -z "$STAGED_FILES" ]; then
|
|
19
|
-
exit 0
|
|
20
|
-
fi
|
|
21
|
-
|
|
22
|
-
# Skip binary files, lock files, and vendored directories
|
|
23
|
-
FILTERED_FILES=""
|
|
24
|
-
for f in $STAGED_FILES; do
|
|
25
|
-
case "$f" in
|
|
26
|
-
node_modules/*|vendor/*|.git/*|__pycache__/*|dist/*|build/*) continue ;;
|
|
27
|
-
package-lock.json|yarn.lock|Gemfile.lock|poetry.lock|pnpm-lock.yaml) continue ;;
|
|
28
|
-
*.png|*.jpg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.eot|*.pdf|*.zip|*.tar*) continue ;;
|
|
29
|
-
.env.example|.env.sample|.env.template) continue ;;
|
|
30
|
-
*) FILTERED_FILES="$FILTERED_FILES $f" ;;
|
|
31
|
-
esac
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
if [ -z "$FILTERED_FILES" ]; then
|
|
35
|
-
exit 0
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
FOUND=0
|
|
39
|
-
FINDINGS=""
|
|
40
|
-
|
|
41
|
-
# Tier 1: High-confidence patterns — these almost never false-positive
|
|
42
|
-
# We scan staged content (not working tree) to catch exactly what would be committed
|
|
43
|
-
PATTERNS=(
|
|
44
|
-
'AKIA[0-9A-Z]{16}' # AWS Access Key ID
|
|
45
|
-
'ghp_[A-Za-z0-9_]{36}' # GitHub PAT
|
|
46
|
-
'gho_[A-Za-z0-9_]{36}' # GitHub OAuth
|
|
47
|
-
'ghu_[A-Za-z0-9_]{36}' # GitHub user token
|
|
48
|
-
'ghs_[A-Za-z0-9_]{36}' # GitHub server token
|
|
49
|
-
'github_pat_[A-Za-z0-9_]{82}' # GitHub fine-grained PAT
|
|
50
|
-
'xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}' # Slack bot token
|
|
51
|
-
'xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack user token
|
|
52
|
-
'xoxa-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack app token
|
|
53
|
-
'AIza[0-9A-Za-z\-_]{35}' # Google API key
|
|
54
|
-
'sk_live_[0-9a-zA-Z]{24,}' # Stripe live secret
|
|
55
|
-
'sk_test_[0-9a-zA-Z]{24,}' # Stripe test secret
|
|
56
|
-
'rk_live_[0-9a-zA-Z]{24,}' # Stripe restricted
|
|
57
|
-
'SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}' # SendGrid
|
|
58
|
-
'SK[0-9a-fA-F]{32}' # Twilio
|
|
59
|
-
'npm_[A-Za-z0-9]{36}' # npm token
|
|
60
|
-
'glpat-[A-Za-z0-9\-_]{20,}' # GitLab PAT
|
|
61
|
-
'-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----' # Private keys
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
for f in $FILTERED_FILES; do
|
|
65
|
-
# Get the staged version of the file (what would actually be committed)
|
|
66
|
-
CONTENT=$(git show ":$f" 2>/dev/null || true)
|
|
67
|
-
if [ -z "$CONTENT" ]; then
|
|
68
|
-
continue
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
for pattern in "${PATTERNS[@]}"; do
|
|
72
|
-
MATCHES=$(echo "$CONTENT" | grep -nE "$pattern" 2>/dev/null || true)
|
|
73
|
-
if [ -n "$MATCHES" ]; then
|
|
74
|
-
while IFS= read -r match; do
|
|
75
|
-
LINE_NUM=$(echo "$match" | cut -d: -f1)
|
|
76
|
-
# Mask the secret value in output (show first 8 chars only)
|
|
77
|
-
MASKED=$(echo "$match" | cut -d: -f2- | sed -E 's/([A-Za-z0-9_\-]{8})[A-Za-z0-9_\-]{8,}/\1****/g')
|
|
78
|
-
FINDINGS="${FINDINGS}\n ${RED}BLOCKED${NC} $f:$LINE_NUM $MASKED"
|
|
79
|
-
FOUND=$((FOUND + 1))
|
|
80
|
-
done <<< "$MATCHES"
|
|
81
|
-
fi
|
|
82
|
-
done
|
|
83
|
-
done
|
|
84
|
-
|
|
85
|
-
# Also check: is a .env file being committed?
|
|
86
|
-
for f in $STAGED_FILES; do
|
|
87
|
-
case "$f" in
|
|
88
|
-
.env|.env.local|.env.production|.env.staging|.env.*.local)
|
|
89
|
-
FINDINGS="${FINDINGS}\n ${YELLOW}WARNING${NC} $f is staged — this file typically contains secrets and should be gitignored"
|
|
90
|
-
FOUND=$((FOUND + 1))
|
|
91
|
-
;;
|
|
92
|
-
esac
|
|
93
|
-
done
|
|
94
|
-
|
|
95
|
-
if [ "$FOUND" -gt 0 ]; then
|
|
96
|
-
echo ""
|
|
97
|
-
echo -e "${RED}ftm-git: COMMIT BLOCKED — $FOUND secret(s) detected in staged files${NC}"
|
|
98
|
-
echo ""
|
|
99
|
-
echo -e "$FINDINGS"
|
|
100
|
-
echo ""
|
|
101
|
-
echo "To fix: run /ftm-git for auto-remediation, or manually:"
|
|
102
|
-
echo " 1. Move secrets to .env (gitignored)"
|
|
103
|
-
echo " 2. Replace hardcoded values with env var references"
|
|
104
|
-
echo " 3. Stage the cleaned files and commit again"
|
|
105
|
-
echo ""
|
|
106
|
-
echo "To bypass (NOT recommended): git commit --no-verify"
|
|
107
|
-
exit 1
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
exit 0
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ftm-git pre-commit hook — blocks commits containing hardcoded secrets
|
|
3
|
+
# Installed by the ftm-git skill on first invocation. Safe to remove with:
|
|
4
|
+
# rm .git/hooks/pre-commit (or edit to remove the ftm-git section)
|
|
5
|
+
#
|
|
6
|
+
# This hook scans staged files only (fast). The full ftm-git skill does
|
|
7
|
+
# deeper scanning with context validation and auto-remediation — this is
|
|
8
|
+
# the safety net that catches what slips through.
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
YELLOW='\033[0;33m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
# Get list of staged files (excluding deletions)
|
|
17
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)
|
|
18
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Skip binary files, lock files, and vendored directories
|
|
23
|
+
FILTERED_FILES=""
|
|
24
|
+
for f in $STAGED_FILES; do
|
|
25
|
+
case "$f" in
|
|
26
|
+
node_modules/*|vendor/*|.git/*|__pycache__/*|dist/*|build/*) continue ;;
|
|
27
|
+
package-lock.json|yarn.lock|Gemfile.lock|poetry.lock|pnpm-lock.yaml) continue ;;
|
|
28
|
+
*.png|*.jpg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.eot|*.pdf|*.zip|*.tar*) continue ;;
|
|
29
|
+
.env.example|.env.sample|.env.template) continue ;;
|
|
30
|
+
*) FILTERED_FILES="$FILTERED_FILES $f" ;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
if [ -z "$FILTERED_FILES" ]; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
FOUND=0
|
|
39
|
+
FINDINGS=""
|
|
40
|
+
|
|
41
|
+
# Tier 1: High-confidence patterns — these almost never false-positive
|
|
42
|
+
# We scan staged content (not working tree) to catch exactly what would be committed
|
|
43
|
+
PATTERNS=(
|
|
44
|
+
'AKIA[0-9A-Z]{16}' # AWS Access Key ID
|
|
45
|
+
'ghp_[A-Za-z0-9_]{36}' # GitHub PAT
|
|
46
|
+
'gho_[A-Za-z0-9_]{36}' # GitHub OAuth
|
|
47
|
+
'ghu_[A-Za-z0-9_]{36}' # GitHub user token
|
|
48
|
+
'ghs_[A-Za-z0-9_]{36}' # GitHub server token
|
|
49
|
+
'github_pat_[A-Za-z0-9_]{82}' # GitHub fine-grained PAT
|
|
50
|
+
'xoxb-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}' # Slack bot token
|
|
51
|
+
'xoxp-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack user token
|
|
52
|
+
'xoxa-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,34}' # Slack app token
|
|
53
|
+
'AIza[0-9A-Za-z\-_]{35}' # Google API key
|
|
54
|
+
'sk_live_[0-9a-zA-Z]{24,}' # Stripe live secret
|
|
55
|
+
'sk_test_[0-9a-zA-Z]{24,}' # Stripe test secret
|
|
56
|
+
'rk_live_[0-9a-zA-Z]{24,}' # Stripe restricted
|
|
57
|
+
'SG\.[A-Za-z0-9\-_]{22}\.[A-Za-z0-9\-_]{43}' # SendGrid
|
|
58
|
+
'SK[0-9a-fA-F]{32}' # Twilio
|
|
59
|
+
'npm_[A-Za-z0-9]{36}' # npm token
|
|
60
|
+
'glpat-[A-Za-z0-9\-_]{20,}' # GitLab PAT
|
|
61
|
+
'-----BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY-----' # Private keys
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
for f in $FILTERED_FILES; do
|
|
65
|
+
# Get the staged version of the file (what would actually be committed)
|
|
66
|
+
CONTENT=$(git show ":$f" 2>/dev/null || true)
|
|
67
|
+
if [ -z "$CONTENT" ]; then
|
|
68
|
+
continue
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
for pattern in "${PATTERNS[@]}"; do
|
|
72
|
+
MATCHES=$(echo "$CONTENT" | grep -nE "$pattern" 2>/dev/null || true)
|
|
73
|
+
if [ -n "$MATCHES" ]; then
|
|
74
|
+
while IFS= read -r match; do
|
|
75
|
+
LINE_NUM=$(echo "$match" | cut -d: -f1)
|
|
76
|
+
# Mask the secret value in output (show first 8 chars only)
|
|
77
|
+
MASKED=$(echo "$match" | cut -d: -f2- | sed -E 's/([A-Za-z0-9_\-]{8})[A-Za-z0-9_\-]{8,}/\1****/g')
|
|
78
|
+
FINDINGS="${FINDINGS}\n ${RED}BLOCKED${NC} $f:$LINE_NUM $MASKED"
|
|
79
|
+
FOUND=$((FOUND + 1))
|
|
80
|
+
done <<< "$MATCHES"
|
|
81
|
+
fi
|
|
82
|
+
done
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
# Also check: is a .env file being committed?
|
|
86
|
+
for f in $STAGED_FILES; do
|
|
87
|
+
case "$f" in
|
|
88
|
+
.env|.env.local|.env.production|.env.staging|.env.*.local)
|
|
89
|
+
FINDINGS="${FINDINGS}\n ${YELLOW}WARNING${NC} $f is staged — this file typically contains secrets and should be gitignored"
|
|
90
|
+
FOUND=$((FOUND + 1))
|
|
91
|
+
;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [ "$FOUND" -gt 0 ]; then
|
|
96
|
+
echo ""
|
|
97
|
+
echo -e "${RED}ftm-git: COMMIT BLOCKED — $FOUND secret(s) detected in staged files${NC}"
|
|
98
|
+
echo ""
|
|
99
|
+
echo -e "$FINDINGS"
|
|
100
|
+
echo ""
|
|
101
|
+
echo "To fix: run /ftm-git for auto-remediation, or manually:"
|
|
102
|
+
echo " 1. Move secrets to .env (gitignored)"
|
|
103
|
+
echo " 2. Replace hardcoded values with env var references"
|
|
104
|
+
echo " 3. Stage the cleaned files and commit again"
|
|
105
|
+
echo ""
|
|
106
|
+
echo "To bypass (NOT recommended): git commit --no-verify"
|
|
107
|
+
exit 1
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
exit 0
|
package/ftm-git.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
name: ftm-git
|
|
2
|
-
description: Secret scanning and credential safety gate for git operations. Prevents API keys, tokens, passwords, and other secrets from ever being committed or pushed to remote repositories. Scans staged files, working tree, and git history for hardcoded credentials using regex pattern matching, then auto-remediates by extracting secrets to gitignored .env files and replacing hardcoded values with env var references. Use when user says "scan for secrets", "check for keys", "audit credentials", "ftm-git", "secret scan", "remove api keys", "check before push", or any time git commit/push operations are about to happen. Also auto-invoked by ftm-executor and ftm-mind before any commit or push operation. Even if the user just says "commit this" or "push to remote", this skill MUST run first. Do NOT use for general git workflow operations like branching or merging — that's git-workflow territory. This skill is specifically the security gate.
|
|
1
|
+
name: ftm-git
|
|
2
|
+
description: Secret scanning and credential safety gate for git operations. Prevents API keys, tokens, passwords, and other secrets from ever being committed or pushed to remote repositories. Scans staged files, working tree, and git history for hardcoded credentials using regex pattern matching, then auto-remediates by extracting secrets to gitignored .env files and replacing hardcoded values with env var references. Use when user says "scan for secrets", "check for keys", "audit credentials", "ftm-git", "secret scan", "remove api keys", "check before push", or any time git commit/push operations are about to happen. Also auto-invoked by ftm-executor and ftm-mind before any commit or push operation. Even if the user just says "commit this" or "push to remote", this skill MUST run first. Do NOT use for general git workflow operations like branching or merging — that's git-workflow territory. This skill is specifically the security gate.
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Exponential-backoff retry decorator for HTTP adapter calls.
|
|
3
|
-
|
|
4
|
-
Usage:
|
|
5
|
-
@retry(max_attempts=3, base_delay=1.0)
|
|
6
|
-
def poll(self) -> list[dict]:
|
|
7
|
-
...
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import functools
|
|
13
|
-
import logging
|
|
14
|
-
import time
|
|
15
|
-
from typing import Callable, TypeVar
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
F = TypeVar("F", bound=Callable)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def retry(
|
|
23
|
-
max_attempts: int = 3,
|
|
24
|
-
base_delay: float = 1.0,
|
|
25
|
-
backoff_factor: float = 2.0,
|
|
26
|
-
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
27
|
-
) -> Callable[[F], F]:
|
|
28
|
-
"""
|
|
29
|
-
Decorator: retry on exception with exponential backoff.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
max_attempts: Total number of attempts (including the first).
|
|
33
|
-
base_delay: Initial sleep duration in seconds.
|
|
34
|
-
backoff_factor: Multiplier applied to delay after each failure.
|
|
35
|
-
exceptions: Exception types that trigger a retry.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def decorator(func: F) -> F:
|
|
39
|
-
@functools.wraps(func)
|
|
40
|
-
def wrapper(*args, **kwargs):
|
|
41
|
-
delay = base_delay
|
|
42
|
-
last_exc: Exception | None = None
|
|
43
|
-
for attempt in range(1, max_attempts + 1):
|
|
44
|
-
try:
|
|
45
|
-
return func(*args, **kwargs)
|
|
46
|
-
except exceptions as exc:
|
|
47
|
-
last_exc = exc
|
|
48
|
-
if attempt == max_attempts:
|
|
49
|
-
break
|
|
50
|
-
logger.warning(
|
|
51
|
-
"%s failed (attempt %d/%d): %s — retrying in %.1fs",
|
|
52
|
-
func.__qualname__,
|
|
53
|
-
attempt,
|
|
54
|
-
max_attempts,
|
|
55
|
-
exc,
|
|
56
|
-
delay,
|
|
57
|
-
)
|
|
58
|
-
time.sleep(delay)
|
|
59
|
-
delay *= backoff_factor
|
|
60
|
-
raise last_exc # type: ignore[misc]
|
|
61
|
-
|
|
62
|
-
return wrapper # type: ignore[return-value]
|
|
63
|
-
|
|
64
|
-
return decorator
|
|
1
|
+
"""
|
|
2
|
+
Exponential-backoff retry decorator for HTTP adapter calls.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
@retry(max_attempts=3, base_delay=1.0)
|
|
6
|
+
def poll(self) -> list[dict]:
|
|
7
|
+
...
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import functools
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from typing import Callable, TypeVar
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
F = TypeVar("F", bound=Callable)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def retry(
|
|
23
|
+
max_attempts: int = 3,
|
|
24
|
+
base_delay: float = 1.0,
|
|
25
|
+
backoff_factor: float = 2.0,
|
|
26
|
+
exceptions: tuple[type[Exception], ...] = (Exception,),
|
|
27
|
+
) -> Callable[[F], F]:
|
|
28
|
+
"""
|
|
29
|
+
Decorator: retry on exception with exponential backoff.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
max_attempts: Total number of attempts (including the first).
|
|
33
|
+
base_delay: Initial sleep duration in seconds.
|
|
34
|
+
backoff_factor: Multiplier applied to delay after each failure.
|
|
35
|
+
exceptions: Exception types that trigger a retry.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def decorator(func: F) -> F:
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
def wrapper(*args, **kwargs):
|
|
41
|
+
delay = base_delay
|
|
42
|
+
last_exc: Exception | None = None
|
|
43
|
+
for attempt in range(1, max_attempts + 1):
|
|
44
|
+
try:
|
|
45
|
+
return func(*args, **kwargs)
|
|
46
|
+
except exceptions as exc:
|
|
47
|
+
last_exc = exc
|
|
48
|
+
if attempt == max_attempts:
|
|
49
|
+
break
|
|
50
|
+
logger.warning(
|
|
51
|
+
"%s failed (attempt %d/%d): %s — retrying in %.1fs",
|
|
52
|
+
func.__qualname__,
|
|
53
|
+
attempt,
|
|
54
|
+
max_attempts,
|
|
55
|
+
exc,
|
|
56
|
+
delay,
|
|
57
|
+
)
|
|
58
|
+
time.sleep(delay)
|
|
59
|
+
delay *= backoff_factor
|
|
60
|
+
raise last_exc # type: ignore[misc]
|
|
61
|
+
|
|
62
|
+
return wrapper # type: ignore[return-value]
|
|
63
|
+
|
|
64
|
+
return decorator
|