thumbgate 1.2.0 → 1.4.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/.claude-plugin/README.md +4 -4
- package/.claude-plugin/marketplace.json +32 -13
- package/.claude-plugin/plugin.json +15 -2
- package/.well-known/llms.txt +60 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +133 -23
- package/adapters/README.md +1 -1
- package/adapters/chatgpt/openapi.yaml +168 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +85 -2
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +215 -19
- package/bin/postinstall.js +8 -2
- package/config/budget.json +18 -0
- package/config/gates/code-edit.json +61 -0
- package/config/gates/db-write.json +61 -0
- package/config/gates/default.json +154 -3
- package/config/gates/deploy.json +61 -0
- package/config/github-about.json +2 -1
- package/config/merge-quality-checks.json +23 -0
- package/config/model-tiers.json +11 -0
- package/openapi/openapi.yaml +168 -0
- package/package.json +47 -13
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +1 -3
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +27 -4
- package/plugins/codex-profile/README.md +33 -9
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-marketplace/README.md +2 -2
- package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/blog.html +73 -0
- package/public/compare/mem0.html +189 -0
- package/public/compare/speclock.html +180 -0
- package/public/compare.html +12 -4
- package/public/guide.html +5 -5
- package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
- package/public/guides/codex-cli-guardrails.html +158 -0
- package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
- package/public/guides/pre-action-gates.html +162 -0
- package/public/guides/stop-repeated-ai-agent-mistakes.html +159 -0
- package/public/index.html +169 -70
- package/public/learn/ai-agent-persistent-memory.html +1 -0
- package/public/lessons.html +334 -17
- package/public/llm-context.md +140 -0
- package/public/pro.html +24 -22
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/access-anomaly-detector.js +1 -1
- package/scripts/adk-consolidator.js +1 -5
- package/scripts/agent-security-hardening.js +4 -6
- package/scripts/agentic-data-pipeline.js +1 -3
- package/scripts/async-job-runner.js +1 -5
- package/scripts/audit-trail.js +7 -5
- package/scripts/background-agent-governance.js +2 -10
- package/scripts/billing.js +2 -16
- package/scripts/budget-enforcer.js +173 -0
- package/scripts/build-codex-plugin.js +152 -0
- package/scripts/capture-railway-diagnostics.sh +97 -0
- package/scripts/check-congruence.js +133 -15
- package/scripts/claude-feedback-sync.js +320 -0
- package/scripts/cli-telemetry.js +4 -1
- package/scripts/commercial-offer.js +5 -7
- package/scripts/content-engine/linkedin-content-generator.js +154 -0
- package/scripts/content-engine/output/linkedin-memento-validation.md +17 -0
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +175 -0
- package/scripts/content-engine/reddit-thread-finder.js +154 -0
- package/scripts/context-engine.js +21 -6
- package/scripts/contextfs.js +33 -44
- package/scripts/dashboard.js +104 -0
- package/scripts/decision-journal.js +341 -0
- package/scripts/delegation-runtime.js +1 -5
- package/scripts/distribution-surfaces.js +26 -0
- package/scripts/document-intake.js +927 -0
- package/scripts/ephemeral-agent-store.js +1 -8
- package/scripts/evolution-state.js +1 -5
- package/scripts/experiment-tracker.js +1 -5
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-hf-dataset.js +1 -5
- package/scripts/export-training.js +1 -5
- package/scripts/feedback-attribution.js +1 -16
- package/scripts/feedback-history-distiller.js +1 -16
- package/scripts/feedback-loop.js +17 -5
- package/scripts/feedback-root-consolidator.js +2 -21
- package/scripts/feedback-session.js +49 -0
- package/scripts/feedback-to-rules.js +188 -28
- package/scripts/filesystem-search.js +1 -9
- package/scripts/fs-utils.js +104 -0
- package/scripts/gates-engine.js +149 -4
- package/scripts/github-about.js +32 -8
- package/scripts/gtm-revenue-loop.js +1 -5
- package/scripts/harness-selector.js +148 -0
- package/scripts/hosted-job-launcher.js +1 -5
- package/scripts/hybrid-feedback-context.js +7 -33
- package/scripts/intervention-policy.js +753 -0
- package/scripts/lesson-db.js +3 -18
- package/scripts/lesson-inference.js +194 -16
- package/scripts/lesson-retrieval.js +60 -24
- package/scripts/llm-client.js +59 -0
- package/scripts/local-model-profile.js +18 -2
- package/scripts/managed-lesson-agent.js +183 -0
- package/scripts/marketing-experiment.js +8 -22
- package/scripts/meta-agent-loop.js +624 -0
- package/scripts/metered-billing.js +1 -1
- package/scripts/model-tier-router.js +10 -1
- package/scripts/money-watcher.js +1 -4
- package/scripts/obsidian-export.js +1 -5
- package/scripts/operational-integrity.js +369 -34
- package/scripts/org-dashboard.js +6 -1
- package/scripts/per-step-scoring.js +2 -4
- package/scripts/pr-manager.js +201 -19
- package/scripts/pro-features.js +3 -2
- package/scripts/prompt-dlp.js +3 -3
- package/scripts/prove-adapters.js +2 -5
- package/scripts/prove-attribution.js +1 -5
- package/scripts/prove-automation.js +3 -5
- package/scripts/prove-cloudflare-sandbox.js +1 -3
- package/scripts/prove-data-pipeline.js +1 -3
- package/scripts/prove-intelligence.js +1 -3
- package/scripts/prove-lancedb.js +1 -5
- package/scripts/prove-local-intelligence.js +1 -3
- package/scripts/prove-packaged-runtime.js +326 -0
- package/scripts/prove-predictive-insights.js +1 -3
- package/scripts/prove-runtime.js +13 -0
- package/scripts/prove-training-export.js +1 -3
- package/scripts/prove-workflow-contract.js +1 -5
- package/scripts/rate-limiter.js +6 -4
- package/scripts/reddit-dm-outreach.js +14 -4
- package/scripts/schedule-manager.js +3 -5
- package/scripts/security-scanner.js +448 -0
- package/scripts/self-distill-agent.js +579 -0
- package/scripts/semantic-dedup.js +115 -0
- package/scripts/skill-exporter.js +1 -3
- package/scripts/skill-generator.js +1 -5
- package/scripts/social-analytics/engagement-audit.js +1 -18
- package/scripts/social-analytics/pollers/linkedin.js +26 -16
- package/scripts/social-analytics/publishers/linkedin.js +1 -1
- package/scripts/social-analytics/publishers/zernio.js +51 -0
- package/scripts/social-pipeline.js +1 -3
- package/scripts/social-post-hourly.js +47 -4
- package/scripts/statusline-links.js +6 -5
- package/scripts/statusline-local-stats.js +2 -0
- package/scripts/statusline.sh +38 -7
- package/scripts/sync-branch-protection.js +340 -0
- package/scripts/tessl-export.js +1 -3
- package/scripts/thumbgate-search.js +32 -1
- package/scripts/tool-kpi-tracker.js +1 -1
- package/scripts/tool-registry.js +108 -4
- package/scripts/vector-store.js +1 -5
- package/scripts/weekly-auto-post.js +1 -1
- package/scripts/workflow-sentinel.js +205 -4
- package/skills/thumbgate/SKILL.md +2 -2
- package/src/api/server.js +273 -4
- package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
- /package/scripts/social-analytics/db/{social-analytics.db-wal → analytics.sqlite} +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# ThumbGate — Agent Governance for Engineering Teams
|
|
2
|
+
|
|
3
|
+
## What ThumbGate Is
|
|
4
|
+
|
|
5
|
+
ThumbGate is agent governance software for engineering teams that run AI coding agents (Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode) on shared codebases. It enforces approval policies, captures and propagates lessons from agent mistakes, and physically blocks known-bad tool calls before they execute via PreToolUse hooks wired into the agent runtime. Unlike CLAUDE.md rules or .cursorrules files, which are suggestions the agent can ignore, ThumbGate enforcement operates at the tool-call level and cannot be bypassed by the agent. The business is enterprise-first: the best first paid motion is the Workflow Hardening Sprint for one workflow, while the local CLI stays free as the adoption wedge and Pro remains a solo side lane.
|
|
6
|
+
|
|
7
|
+
## Problems ThumbGate Prevents
|
|
8
|
+
|
|
9
|
+
- An AI coding agent force-pushes to the main branch, overwriting a teammate's commit
|
|
10
|
+
- An agent deletes a production config file because the prompt said "clean up unused files"
|
|
11
|
+
- An agent bypasses CI by committing with --no-verify after seeing test failures
|
|
12
|
+
- An agent repeats the same database migration mistake across three pull requests because the lesson was never captured
|
|
13
|
+
- One engineer gives a thumbs-down on a bad agent pattern; teammates running the same agent repeat the mistake because lessons are not shared
|
|
14
|
+
- An agent modifies secrets or PII-bearing files because no approval policy was in place
|
|
15
|
+
- A team cannot audit which agent actions were blocked, approved, or overridden, making compliance reporting impossible
|
|
16
|
+
|
|
17
|
+
## How ThumbGate Works Technically
|
|
18
|
+
|
|
19
|
+
ThumbGate is built on Node.js >=18.18.0 and runs locally on each developer's machine with optional team sync.
|
|
20
|
+
|
|
21
|
+
**CLI-first install, MCP-compatible transport**: `npx thumbgate init` is the default setup path. It installs the local gateway, wires the needed hooks, and configures MCP transport automatically for the agent that is already in use. MCP matters for compatibility, but the product surface is the operator-friendly CLI.
|
|
22
|
+
|
|
23
|
+
**PreToolUse Hooks**: Every agent tool call (Bash, file writes, git operations, API calls) passes through a hook before execution. If the call matches a known-bad pattern stored in the lesson database, the hook blocks it and returns a descriptive error. The agent cannot proceed until the human approves or the policy is updated.
|
|
24
|
+
|
|
25
|
+
**SQLite + FTS5 Lesson Database**: When an agent makes a mistake, the developer gives a thumbs-down with context. ThumbGate stores this as a lesson in a local SQLite database with full-text search. Lessons are retrieved at the start of every agent session via the `recall` MCP tool, so the agent enters each session already aware of known failure patterns.
|
|
26
|
+
|
|
27
|
+
**Thompson Sampling for Adaptive Gates**: Gates use Thompson Sampling (a Bayesian multi-armed bandit algorithm) to tune their own sensitivity. Gates that block too aggressively accumulate negative feedback and are dialed back. Gates that catch real failures are reinforced. This prevents gate fatigue without manual tuning.
|
|
28
|
+
|
|
29
|
+
**Shared Team Enforcement**: In team mode, lessons learned on one seat propagate to all seats via a shared lesson database. A pattern that caused a mistake for one engineer is immediately visible to every agent on every seat. The shared database is the single source of truth for team-wide enforcement rules.
|
|
30
|
+
|
|
31
|
+
**CI Gate Integration**: ThumbGate can run as a CI step. Pull requests that contain agent-generated changes matching known failure signatures are blocked from merging until a human reviews and approves the exception.
|
|
32
|
+
|
|
33
|
+
**Audit Trail**: Every gate decision (blocked, approved, overridden) is logged with a timestamp, the triggering tool call, the matching lesson ID, and the identity of any human who approved an exception. This log is queryable and exportable for compliance reporting.
|
|
34
|
+
|
|
35
|
+
**Three-Tier Approval Routing (OVIS-inspired)**: ThumbGate gates operate on three distinct tiers, inspired by the OVIS decision framework (Owner, Veto, Influence). Each gate carries an `action` field that determines the routing outcome:
|
|
36
|
+
|
|
37
|
+
- **`block`** — Hard stop. The agent cannot proceed. The tool call is denied immediately. Used for force-pushes, secret commits, destructive SQL, and any irreversible action. The agent receives an error message explaining why the action was blocked.
|
|
38
|
+
- **`approve`** — Pause and escalate. The agent is halted and the caller receives `{ decision: "approve", requiresApproval: true }`. A human must explicitly confirm before the action can proceed. Used for production deploys, schema migrations, and permission changes where human oversight is mandatory.
|
|
39
|
+
- **`log`** — Record and continue. The action is allowed to proceed but is written to the audit trail. The agent receives `{ decision: "log", logged: true }` and continues without interruption. Used for style violations, large file writes, and non-critical warnings where visibility matters but blocking would create friction.
|
|
40
|
+
|
|
41
|
+
This model maps directly to the OVIS framework: `block` exercises Veto authority, `approve` requires Owner sign-off, and `log` satisfies Influence-layer audit requirements without halting execution.
|
|
42
|
+
|
|
43
|
+
## Who ThumbGate Is For
|
|
44
|
+
|
|
45
|
+
Engineering teams of 2 to 200+ developers who are actively using AI coding agents on shared repositories and need:
|
|
46
|
+
|
|
47
|
+
- Consistent enforcement of coding policies across all agents and all seats
|
|
48
|
+
- A shared memory of agent mistakes so errors are not repeated by different team members
|
|
49
|
+
- Approval gates for high-risk actions (pushing to protected branches, modifying production configs, running database migrations)
|
|
50
|
+
- An audit trail for compliance, incident review, or just understanding what the agent did
|
|
51
|
+
- Gradual rollout: start with observation mode, add enforcement rules incrementally
|
|
52
|
+
|
|
53
|
+
ThumbGate is not a model training pipeline. It does not retrain the underlying LLM. It shapes agent behavior through context injection and hard enforcement hooks.
|
|
54
|
+
|
|
55
|
+
## Academic Validation
|
|
56
|
+
|
|
57
|
+
ThumbGate implements the **Memento-Skills architecture** described in "Memento-Skills: Let Agents Design Agents" (arXiv 2603.18743, March 2026). This architecture—Read → Execute → Reflect → Write—allows agents to improve themselves through external skill memory that rewrites from failure feedback, eliminating the need for model retraining. Published results demonstrate 26.2% and 116.2% relative accuracy improvements on General AI Assistants benchmarks and Humanity's Last Exam. ThumbGate applies this same pattern to production AI coding agents via PreToolUse hooks, Thompson Sampling, SQLite+FTS5 lesson databases, and LanceDB vectors—treating each agent mistake as a skill refinement opportunity rather than a training event.
|
|
58
|
+
|
|
59
|
+
## Continual Learning Architecture
|
|
60
|
+
|
|
61
|
+
ThumbGate implements continual learning across all three layers identified by LangChain's framework for building learning agents (Harrison Chase, April 2026):
|
|
62
|
+
|
|
63
|
+
- **Model layer**: ThumbGate Pro exports DPO pairs so teams can fine-tune local models (Llama 3, Mistral) to natively avoid known failures without retraining the upstream foundation model.
|
|
64
|
+
- **Harness layer**: Prevention rules auto-generated from feedback are injected into the agent's system prompt at session start via the `recall` MCP tool. The agent enters every session pre-loaded with lessons — no code changes required.
|
|
65
|
+
- **Context layer**: The SQLite+FTS5 lesson database and LanceDB vector search provide retrieval-augmented context at tool-call time. When an agent attempts an action, PreToolUse hooks query the lesson DB and block or approve based on prior feedback. This is the layer that runs continuously and requires zero human intervention after the initial feedback signal.
|
|
66
|
+
|
|
67
|
+
This three-layer architecture means ThumbGate improves agent behavior at every level: context injection for immediate effect, harness-level rules for session-wide enforcement, and model-level export for permanent behavioral change.
|
|
68
|
+
|
|
69
|
+
## Enterprise Safety Framework Alignment
|
|
70
|
+
|
|
71
|
+
ThumbGate's architecture maps directly to the enterprise safety framework pattern recommended by Google Cloud for agentic AI workflows (April 2026). Google Cloud's reference architecture routes every agent action through a Safety Framework gate before execution, with unsafe actions producing a canned response and safe actions proceeding to the Agentic Workflow, followed by a Quality Framework check on the output.
|
|
72
|
+
|
|
73
|
+
ThumbGate implements this same pattern for AI coding agents:
|
|
74
|
+
|
|
75
|
+
- **Safety Framework gate** → PreToolUse hooks evaluate every tool call against the lesson database before execution. Known-bad patterns are blocked immediately.
|
|
76
|
+
- **Safe → Agentic Workflow** → Tool calls that pass gate evaluation proceed normally. The agent operates without interruption.
|
|
77
|
+
- **Unsafe → Canned response** → Blocked tool calls return a descriptive error to the agent explaining why the action was denied and what policy it violated.
|
|
78
|
+
- **Quality Framework** → Post-action audit trail, feedback sessions, and auto-generated prevention rules create a continuous quality improvement loop.
|
|
79
|
+
|
|
80
|
+
This alignment means ThumbGate is not an experimental tool — it implements the same safety architecture that Google Cloud recommends for enterprise agentic deployments, applied specifically to AI coding agents operating on shared codebases.
|
|
81
|
+
|
|
82
|
+
## Pricing
|
|
83
|
+
|
|
84
|
+
- **Free**: Local CLI enforcement for individual developers. Includes 3 daily feedback captures, 5 lesson searches per day, unlimited recall, and PreToolUse hook blocking.
|
|
85
|
+
- **Workflow Hardening Sprint / Team**: Team pricing anchors at $99/seat/mo with a 3-seat minimum after qualification. The first paid step is an intake-led sprint around one workflow, one repeated blocker, and one proof review.
|
|
86
|
+
- **Pro**: $19/mo or $149/yr. Adds a personal local dashboard, DPO export for fine-tuning, and advanced data exports for solo operators who want a self-serve side lane.
|
|
87
|
+
|
|
88
|
+
## How to Install
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx thumbgate init
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
ThumbGate auto-detects your AI coding agent (Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode) and configures PreToolUse hooks. No API key required. Everything runs locally. For team deployments, run with the `--team` flag to connect to the shared lesson database.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npx thumbgate init --agent claude-code
|
|
98
|
+
npx thumbgate dashboard
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Comparison vs Alternatives
|
|
102
|
+
|
|
103
|
+
| Approach | Blocks actions before execution | Learns from feedback | Shared team enforcement | Audit trail |
|
|
104
|
+
|---|---|---|---|---|
|
|
105
|
+
| **ThumbGate** | Yes — PreToolUse hooks | Yes — auto-generates rules | Yes — shared lesson DB | Yes — full log |
|
|
106
|
+
| CLAUDE.md / .cursorrules | No — suggestions only | No — hand-written | No — per-developer files | No |
|
|
107
|
+
| ESLint / linters | Partial — static analysis | No — hand-written rules | Partial — shared config | No |
|
|
108
|
+
| Manual code review | Partial — after PR, not before | No — reviewer memory | Partial — PR comments | Partial — PR history |
|
|
109
|
+
| Post-hoc git revert | No — damage already done | No | No | Partial — git log |
|
|
110
|
+
|
|
111
|
+
**CLAUDE.md rules** are plain-text instructions read by the agent at session start. The agent can ignore them, forget them, or misinterpret them mid-session. CLAUDE.md is useful for project context but provides no enforcement guarantee.
|
|
112
|
+
|
|
113
|
+
**ESLint and linters** check code at build time, not at tool-call time. They cannot block an agent from deleting a file, force-pushing, or making a destructive API call — they only catch code-style issues after the code has been written.
|
|
114
|
+
|
|
115
|
+
**Manual code review** is asynchronous and expensive. It catches mistakes after the PR is created, not before the agent makes the mistake. It scales poorly as AI agents generate changes at 10x the rate of human developers.
|
|
116
|
+
|
|
117
|
+
**ThumbGate** is the only tool that (1) blocks actions before execution, (2) learns automatically from feedback without manual rule-writing, (3) shares lessons across an entire team, and (4) produces a full audit trail.
|
|
118
|
+
|
|
119
|
+
## Additional Resources
|
|
120
|
+
|
|
121
|
+
- GitHub: https://github.com/IgorGanapolsky/ThumbGate
|
|
122
|
+
- npm: https://www.npmjs.com/package/thumbgate
|
|
123
|
+
- Documentation: https://thumbgate-production.up.railway.app/guide
|
|
124
|
+
- Comparison: https://thumbgate-production.up.railway.app/compare
|
|
125
|
+
- Team sprint intake: https://thumbgate-production.up.railway.app/#workflow-sprint-intake
|
|
126
|
+
|
|
127
|
+
## How ThumbGate Promotes AI Authenticity
|
|
128
|
+
|
|
129
|
+
In an era of AI slop — where agents generate repetitive, generic outputs that erode brand trust — ThumbGate is the enforcement layer that keeps AI behavior grounded in human judgment.
|
|
130
|
+
|
|
131
|
+
The core thesis: AI slop happens when agents act without human judgment as a hard gate. ThumbGate solves this at the tool-call level, not through prompt suggestions the agent can ignore.
|
|
132
|
+
|
|
133
|
+
- **Human insight leads**: The thumbs-up/down mechanism is a literal human gate between AI intent and execution. The agent cannot proceed on blocked patterns.
|
|
134
|
+
- **Standards, not patterns**: Every thumbs-down distills into a prevention rule specific to your team's context — not a generic best practice.
|
|
135
|
+
- **Authenticity over volume**: ThumbGate reinforces what "good" looks like for your specific codebase, standards, and risk tolerance. Agents learn your standards, not average AI behavior.
|
|
136
|
+
- **Discovery advantage**: As platforms and search engines increasingly reward authentic, human-verified outputs over high-volume AI generation, ThumbGate provides the enforcement infrastructure that proves human oversight was applied.
|
|
137
|
+
|
|
138
|
+
**Positioning for authenticity-conscious audiences**: ThumbGate is the authenticity enforcement layer for AI coding agents. Where AI slop is the symptom, unconstrained agent autonomy is the cause. ThumbGate addresses the cause.
|
|
139
|
+
|
|
140
|
+
*Last updated: 2026-04-09*
|
package/public/pro.html
CHANGED
|
@@ -792,10 +792,10 @@ __GA_BOOTSTRAP__
|
|
|
792
792
|
<section class="hero">
|
|
793
793
|
<div class="container hero-grid">
|
|
794
794
|
<div class="panel hero-copy">
|
|
795
|
-
<div class="eyebrow">
|
|
796
|
-
<h1>
|
|
797
|
-
<p>ThumbGate
|
|
798
|
-
<p>
|
|
795
|
+
<div class="eyebrow">Agent governance for engineering teams</div>
|
|
796
|
+
<h1>One correction protects every agent on your team.</h1>
|
|
797
|
+
<p>ThumbGate prevents unsafe AI agent actions before they hit shared repos, CI pipelines, and production. When one developer flags a bad pattern, every agent on the team is permanently blocked from repeating it.</p>
|
|
798
|
+
<p>Open-source core for individuals. Team plan for shared enforcement, CI gates, approval policies, and audit trails across your engineering org.</p>
|
|
799
799
|
<div class="hero-proof">
|
|
800
800
|
<div class="proof-pill">Personal local dashboard</div>
|
|
801
801
|
<div class="proof-pill">DPO export from real corrections</div>
|
|
@@ -947,30 +947,32 @@ __GA_BOOTSTRAP__
|
|
|
947
947
|
<div class="container pricing-shell">
|
|
948
948
|
<div class="pricing-card">
|
|
949
949
|
<div class="section-label" style="text-align:left;margin-bottom:8px;">Pricing</div>
|
|
950
|
-
<h3>ThumbGate
|
|
951
|
-
<div class="price">$
|
|
952
|
-
<div class="annual">
|
|
953
|
-
<p class="pricing-note">
|
|
950
|
+
<h3>ThumbGate Team — Agent Governance</h3>
|
|
951
|
+
<div class="price">$99<span>/seat/mo</span></div>
|
|
952
|
+
<div class="annual">Billed monthly or annually · Starts with a 30-min pilot call</div>
|
|
953
|
+
<p class="pricing-note">Shared enforcement memory, CI gates, approval policies, sandbox routing, and a full audit trail of every blocked agent action across your team.</p>
|
|
954
954
|
<ul>
|
|
955
|
-
<li>
|
|
956
|
-
<li>
|
|
957
|
-
<li>
|
|
958
|
-
<li>
|
|
955
|
+
<li><strong>Shared enforcement</strong> — one developer's correction blocks that pattern for every agent on the team.</li>
|
|
956
|
+
<li><strong>CI gate integration</strong> — block unsafe merges, enforce test requirements, prevent PRs with unresolved threads.</li>
|
|
957
|
+
<li><strong>Approval policies</strong> — require human sign-off for high-risk actions (production deploys, schema migrations, destructive SQL).</li>
|
|
958
|
+
<li><strong>Audit trail</strong> — every blocked action logged with timestamp, agent, context, and the rule that fired.</li>
|
|
959
|
+
<li><strong>Sandbox routing</strong> — route risky agent runs into isolated execution environments.</li>
|
|
960
|
+
<li><strong>Org dashboard</strong> — active agents, gate hit rates, risk scores, and proof-backed team metrics.</li>
|
|
959
961
|
</ul>
|
|
960
962
|
<div class="pricing-actions">
|
|
961
|
-
<a class="btn-primary
|
|
962
|
-
<a class="btn-secondary btn-
|
|
963
|
+
<a class="btn-primary" href="/#workflow-sprint-intake">Book a Team Pilot Call</a>
|
|
964
|
+
<a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_pricing&utm_campaign=team">Open dashboard demo</a>
|
|
963
965
|
</div>
|
|
964
|
-
<div class="pricing-meta">
|
|
966
|
+
<div class="pricing-meta">Team pricing anchors at $99/seat/mo with a 3-seat minimum. Starts with one workflow, one repo, one repeat failure. We measure before/after and expand only when the results are real.</div>
|
|
965
967
|
</div>
|
|
966
968
|
|
|
967
969
|
<div class="pricing-sidebar">
|
|
968
970
|
<div class="team-card">
|
|
969
971
|
<div class="section-label" style="text-align:left;margin-bottom:8px;">When Team is better</div>
|
|
970
|
-
<h3>
|
|
971
|
-
<p>
|
|
972
|
+
<h3>Solo developer? Start free.</h3>
|
|
973
|
+
<p>The open-source core gives you local enforcement, PreToolUse hooks, and MCP integrations at no cost. Upgrade to Team when your org needs shared enforcement and audit.</p>
|
|
972
974
|
<div class="hero-actions" style="margin-top:18px;">
|
|
973
|
-
<a class="btn-secondary" href="
|
|
975
|
+
<a class="btn-secondary" href="/guide?utm_source=website&utm_medium=pro_page&utm_campaign=free_install">Install free locally</a>
|
|
974
976
|
</div>
|
|
975
977
|
</div>
|
|
976
978
|
<div class="team-card">
|
|
@@ -1010,11 +1012,11 @@ __GA_BOOTSTRAP__
|
|
|
1010
1012
|
<section class="final-cta">
|
|
1011
1013
|
<div class="container">
|
|
1012
1014
|
<div class="final-shell">
|
|
1013
|
-
<h2>
|
|
1014
|
-
<p>ThumbGate
|
|
1015
|
+
<h2>Your team's AI agents are one bad action away from breaking production.</h2>
|
|
1016
|
+
<p>ThumbGate prevents force-pushes, secret commits, unsafe publishes, and destructive SQL before they execute. One correction protects every developer on your team — permanently.</p>
|
|
1015
1017
|
<div class="hero-actions" style="justify-content:center;">
|
|
1016
|
-
<a class="btn-primary
|
|
1017
|
-
<a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_final&utm_campaign=
|
|
1018
|
+
<a class="btn-primary" href="/#workflow-sprint-intake">Book a Team Pilot Call</a>
|
|
1019
|
+
<a class="btn-secondary btn-demo" href="/dashboard?utm_source=website&utm_medium=pro_page_final&utm_campaign=team">Open dashboard demo</a>
|
|
1018
1020
|
</div>
|
|
1019
1021
|
</div>
|
|
1020
1022
|
</div>
|
|
Binary file
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
6
|
+
const { readJsonl } = require('./fs-utils');
|
|
6
7
|
function getAccessLogPath() { return path.join(resolveFeedbackDir(), 'access-log.jsonl'); }
|
|
7
|
-
function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
|
|
8
8
|
function recordAccessAttempt({ agentId, authorized, reason, source } = {}) { const lp = getAccessLogPath(); const dir = path.dirname(lp); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const e = { id: `access_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, timestamp: new Date().toISOString(), agentId: agentId || 'unknown', authorized: authorized !== false, reason: reason || '', source: source || 'unknown' }; fs.appendFileSync(lp, JSON.stringify(e) + '\n'); return e; }
|
|
9
9
|
function computeAccessStats({ periodHours = 24 } = {}) { const entries = readJsonl(getAccessLogPath()); const cutoff = Date.now() - periodHours * 60 * 60 * 1000; const recent = entries.filter((e) => new Date(e.timestamp).getTime() > cutoff); const authorized = recent.filter((e) => e.authorized).length; const failed = recent.filter((e) => !e.authorized).length; const total = recent.length; const failRate = total > 0 ? Math.round((failed / total) * 1000) / 10 : 0; const byAgent = {}; for (const e of recent) { if (!byAgent[e.agentId]) byAgent[e.agentId] = { authorized: 0, failed: 0 }; if (e.authorized) byAgent[e.agentId].authorized++; else byAgent[e.agentId].failed++; } return { periodHours, total, authorized, failed, failRate, byAgent }; }
|
|
10
10
|
function detectAnomalies({ baselineHours = 168, recentHours = 24, spikeMultiplier = 3 } = {}) { const baseline = computeAccessStats({ periodHours: baselineHours }); const recent = computeAccessStats({ periodHours: recentHours }); const baselineRate = baselineHours > 0 ? baseline.failed / baselineHours : 0; const recentRate = recentHours > 0 ? recent.failed / recentHours : 0; const isAnomaly = baselineRate > 0 && recentRate > baselineRate * spikeMultiplier; const isHighFailRate = recent.failRate > 20 && recent.total >= 5; const anomalies = []; if (isAnomaly) anomalies.push({ type: 'failure_spike', severity: recentRate > baselineRate * 5 ? 'critical' : 'warning', message: `Failed access rate ${recentRate.toFixed(1)}/h is ${(recentRate / baselineRate).toFixed(1)}x the baseline ${baselineRate.toFixed(1)}/h`, recentRate, baselineRate }); if (isHighFailRate) anomalies.push({ type: 'high_fail_rate', severity: recent.failRate > 50 ? 'critical' : 'warning', message: `${recent.failRate}% of access attempts failed in the last ${recentHours}h (${recent.failed}/${recent.total})`, failRate: recent.failRate }); for (const [agentId, counts] of Object.entries(recent.byAgent)) { const agentTotal = counts.authorized + counts.failed; if (counts.failed >= 5 && counts.failed / agentTotal > 0.5) anomalies.push({ type: 'agent_abuse', severity: 'warning', message: `Agent ${agentId}: ${counts.failed} failed attempts out of ${agentTotal}`, agentId }); } return { baseline: { periodHours: baselineHours, failedPerHour: Math.round(baselineRate * 100) / 100 }, recent: { periodHours: recentHours, failedPerHour: Math.round(recentRate * 100) / 100, ...recent }, anomalies, hasAnomalies: anomalies.length > 0, checkedAt: new Date().toISOString() }; }
|
|
@@ -23,11 +23,6 @@ const { validateApiKey } = require('./billing');
|
|
|
23
23
|
// Keep track of the last processed ID to avoid re-consolidating the exact same logs
|
|
24
24
|
const STATE_FILE = process.env.ADK_STATE_FILE || path.join(PROJECT_ROOT, '.thumbgate', 'adk-state.json');
|
|
25
25
|
|
|
26
|
-
function ensureDir(dirPath) {
|
|
27
|
-
if (!fs.existsSync(dirPath)) {
|
|
28
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
26
|
|
|
32
27
|
function loadState() {
|
|
33
28
|
if (fs.existsSync(STATE_FILE)) {
|
|
@@ -46,6 +41,7 @@ function saveState(state) {
|
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
const { createRuleProposal, createReasoningTrace } = require('./a2ui-engine');
|
|
44
|
+
const { ensureDir } = require('./fs-utils');
|
|
49
45
|
|
|
50
46
|
function buildFakeConsolidation(anchorLogs, newLogs) {
|
|
51
47
|
const combined = [...anchorLogs, ...newLogs].filter(Boolean);
|
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
17
|
+
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
17
18
|
|
|
18
19
|
function getFeedbackDir() { return resolveFeedbackDir(); }
|
|
19
|
-
function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
|
|
20
|
-
function readJsonl(fp) { if (!fs.existsSync(fp)) return []; const raw = fs.readFileSync(fp, 'utf-8').trim(); if (!raw) return []; return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); }
|
|
21
|
-
|
|
22
20
|
const CRED_LOG = 'credential-attestations.jsonl';
|
|
23
21
|
const ESCALATION_LOG = 'escalation-events.jsonl';
|
|
24
22
|
const DEP_LOG = 'dependency-attestations.jsonl';
|
|
@@ -47,7 +45,7 @@ function attestCredential({ agentId, credentialType, credentialId, toolName, sco
|
|
|
47
45
|
sessionId: sessionId || null,
|
|
48
46
|
};
|
|
49
47
|
const logPath = getCredLogPath();
|
|
50
|
-
|
|
48
|
+
ensureParentDir(logPath);
|
|
51
49
|
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n');
|
|
52
50
|
return entry;
|
|
53
51
|
}
|
|
@@ -114,7 +112,7 @@ function detectPrivilegeEscalation({ agentId, toolName, mcpProfile } = {}) {
|
|
|
114
112
|
message: `Agent "${agentId}" attempted to use "${toolName}" which is outside "${profile}" profile scope`,
|
|
115
113
|
};
|
|
116
114
|
const logPath = getEscalationLogPath();
|
|
117
|
-
|
|
115
|
+
ensureParentDir(logPath);
|
|
118
116
|
fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
|
|
119
117
|
return { escalation: true, event };
|
|
120
118
|
}
|
|
@@ -197,7 +195,7 @@ function attestDependency({ packageName, version, agentId, action } = {}) {
|
|
|
197
195
|
};
|
|
198
196
|
|
|
199
197
|
const logPath = getDepLogPath();
|
|
200
|
-
|
|
198
|
+
ensureParentDir(logPath);
|
|
201
199
|
fs.appendFileSync(logPath, JSON.stringify(event) + '\n');
|
|
202
200
|
|
|
203
201
|
return { allowed, findings, isTrustedScope, event };
|
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
} = require('./analytics-window');
|
|
19
19
|
const { appendWorkflowRun } = require('./workflow-runs');
|
|
20
20
|
const { buildPredictiveInsights } = require('./predictive-insights');
|
|
21
|
+
const { ensureDir } = require('./fs-utils');
|
|
21
22
|
|
|
22
23
|
const PIPELINE_DIRNAME = 'agentic-data-pipeline';
|
|
23
24
|
const DEFAULT_JOB_ID = 'agentic-data-pipeline';
|
|
@@ -32,9 +33,6 @@ function normalizeText(value) {
|
|
|
32
33
|
return text || null;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function ensureDir(dirPath) {
|
|
36
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
37
|
-
}
|
|
38
36
|
|
|
39
37
|
function readJson(filePath) {
|
|
40
38
|
try {
|
|
@@ -8,6 +8,7 @@ const { captureFeedback, analyzeFeedback, getFeedbackPaths, readJSONL } = requir
|
|
|
8
8
|
const { runVerificationLoop } = require('./verification-loop');
|
|
9
9
|
const { createExperiment } = require('./experiment-tracker');
|
|
10
10
|
const { recommendEvolutionTarget } = require('./workspace-evolver');
|
|
11
|
+
const { ensureDir } = require('./fs-utils');
|
|
11
12
|
|
|
12
13
|
const JOB_LOG_FILENAME = 'job-log.jsonl';
|
|
13
14
|
const JOB_CONTROL_FILENAME = 'job-control.json';
|
|
@@ -17,11 +18,6 @@ const RESUMABLE_STATUSES = new Set(['paused', 'running', 'resume_requested']);
|
|
|
17
18
|
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
18
19
|
const CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
19
20
|
|
|
20
|
-
function ensureDir(dirPath) {
|
|
21
|
-
if (!fs.existsSync(dirPath)) {
|
|
22
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
21
|
|
|
26
22
|
function nowIso() {
|
|
27
23
|
return new Date().toISOString();
|
package/scripts/audit-trail.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
16
|
+
const { ensureDir } = require('./fs-utils');
|
|
16
17
|
|
|
17
18
|
const AUDIT_LOG_FILENAME = 'audit-trail.jsonl';
|
|
18
19
|
|
|
@@ -24,11 +25,6 @@ function getAuditLogPath() {
|
|
|
24
25
|
return path.join(resolveFeedbackDir(), AUDIT_LOG_FILENAME);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function ensureDir(dirPath) {
|
|
28
|
-
if (!fs.existsSync(dirPath)) {
|
|
29
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
28
|
|
|
33
29
|
// ---------------------------------------------------------------------------
|
|
34
30
|
// Core audit record
|
|
@@ -64,6 +60,12 @@ function recordAuditEvent(params = {}) {
|
|
|
64
60
|
};
|
|
65
61
|
|
|
66
62
|
fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
|
|
63
|
+
try {
|
|
64
|
+
const { trainAndPersistInterventionPolicy } = require('./intervention-policy');
|
|
65
|
+
trainAndPersistInterventionPolicy(path.dirname(logPath));
|
|
66
|
+
} catch {
|
|
67
|
+
// Keep audit recording resilient even if the learned policy refresh fails.
|
|
68
|
+
}
|
|
67
69
|
return record;
|
|
68
70
|
}
|
|
69
71
|
|
|
@@ -16,21 +16,13 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
19
|
+
const { ensureParentDir, readJsonl } = require('./fs-utils');
|
|
19
20
|
|
|
20
21
|
const RUNS_FILE = 'agent-runs.jsonl';
|
|
21
22
|
|
|
22
23
|
function getFeedbackDir() { return resolveFeedbackDir(); }
|
|
23
24
|
function getRunsPath() { return path.join(getFeedbackDir(), RUNS_FILE); }
|
|
24
25
|
|
|
25
|
-
function readJsonl(fp) {
|
|
26
|
-
if (!fs.existsSync(fp)) return [];
|
|
27
|
-
const raw = fs.readFileSync(fp, 'utf-8').trim();
|
|
28
|
-
if (!raw) return [];
|
|
29
|
-
return raw.split('\n').map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true }); }
|
|
33
|
-
|
|
34
26
|
// ---------------------------------------------------------------------------
|
|
35
27
|
// 1. Run Tracking
|
|
36
28
|
// ---------------------------------------------------------------------------
|
|
@@ -41,7 +33,7 @@ function ensureDir(fp) { const d = path.dirname(fp); if (!fs.existsSync(d)) fs.m
|
|
|
41
33
|
*/
|
|
42
34
|
function recordAgentRun({ agentId, runType, source, branch, prNumber, status, gatesChecked, gatesBlocked, filesChanged, ciPassed, duration, metadata } = {}) {
|
|
43
35
|
const runsPath = getRunsPath();
|
|
44
|
-
|
|
36
|
+
ensureParentDir(runsPath);
|
|
45
37
|
const run = {
|
|
46
38
|
id: `run_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
47
39
|
timestamp: new Date().toISOString(),
|
package/scripts/billing.js
CHANGED
|
@@ -43,6 +43,7 @@ const {
|
|
|
43
43
|
resolveAnalyticsWindow,
|
|
44
44
|
serializeAnalyticsWindow,
|
|
45
45
|
} = require('./analytics-window');
|
|
46
|
+
const { ensureParentDir } = require('./fs-utils');
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
49
|
// Config
|
|
@@ -73,15 +74,7 @@ const CONFIG = {
|
|
|
73
74
|
get NEWSLETTER_SUBSCRIBERS_PATH() {
|
|
74
75
|
return process.env._TEST_NEWSLETTER_SUBSCRIBERS_PATH || path.join(getFeedbackPaths().FEEDBACK_DIR, 'newsletter-subscribers.jsonl');
|
|
75
76
|
},
|
|
76
|
-
CREDIT_PACKS: {
|
|
77
|
-
'mistake-free-starter': {
|
|
78
|
-
id: 'mistake-free-starter',
|
|
79
|
-
name: 'Mistake-Free Starter Pack',
|
|
80
|
-
amountCents: 4900,
|
|
81
|
-
credits: 500,
|
|
82
|
-
currency: 'USD',
|
|
83
|
-
}
|
|
84
|
-
}
|
|
77
|
+
CREDIT_PACKS: {}
|
|
85
78
|
};
|
|
86
79
|
|
|
87
80
|
function resolveLegacyBillingPath(fileName) {
|
|
@@ -148,13 +141,6 @@ function safeCompareHex(expectedHex, actualHex) {
|
|
|
148
141
|
// Internal helpers
|
|
149
142
|
// ---------------------------------------------------------------------------
|
|
150
143
|
|
|
151
|
-
function ensureParentDir(filePath) {
|
|
152
|
-
const dir = path.dirname(filePath);
|
|
153
|
-
if (!fs.existsSync(dir)) {
|
|
154
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
144
|
function sanitizeMetadata(metadata) {
|
|
159
145
|
if (!metadata || typeof metadata !== 'object') return {};
|
|
160
146
|
try {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Budget Enforcer — action count, token, and time limits for agent sessions.
|
|
6
|
+
*
|
|
7
|
+
* Competitive parity with LaneKeep's budget system. Tracks:
|
|
8
|
+
* - max_actions: total tool calls allowed per session
|
|
9
|
+
* - max_time_minutes: wall-clock session duration cap
|
|
10
|
+
* - action_count: running count of tool calls in current session
|
|
11
|
+
*
|
|
12
|
+
* Budget state persists to ~/.thumbgate/budget-state.json.
|
|
13
|
+
* Config lives in config/budget.json or can be set via env vars.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const BUDGET_STATE_PATH = process.env.THUMBGATE_BUDGET_STATE_PATH || path.join(
|
|
20
|
+
process.env.HOME || '/tmp',
|
|
21
|
+
'.thumbgate',
|
|
22
|
+
'budget-state.json'
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const DEFAULT_BUDGET_CONFIG_PATH = process.env.THUMBGATE_BUDGET_CONFIG_PATH || path.join(
|
|
26
|
+
__dirname, '..', 'config', 'budget.json'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const DEFAULT_BUDGET = {
|
|
30
|
+
max_actions: 2000,
|
|
31
|
+
max_time_minutes: 600, // 10 hours
|
|
32
|
+
profiles: {
|
|
33
|
+
strict: { max_actions: 500, max_time_minutes: 150 },
|
|
34
|
+
guided: { max_actions: 2000, max_time_minutes: 600 },
|
|
35
|
+
autonomous: { max_actions: 5000, max_time_minutes: 1200 },
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function loadBudgetConfig() {
|
|
40
|
+
// 1. Environment overrides
|
|
41
|
+
const envProfile = process.env.THUMBGATE_BUDGET_PROFILE;
|
|
42
|
+
const envMaxActions = process.env.THUMBGATE_MAX_ACTIONS;
|
|
43
|
+
const envMaxTime = process.env.THUMBGATE_MAX_TIME_MINUTES;
|
|
44
|
+
|
|
45
|
+
// 2. Config file
|
|
46
|
+
let fileConfig = {};
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(DEFAULT_BUDGET_CONFIG_PATH)) {
|
|
49
|
+
fileConfig = JSON.parse(fs.readFileSync(DEFAULT_BUDGET_CONFIG_PATH, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
} catch { /* use defaults */ }
|
|
52
|
+
|
|
53
|
+
const merged = { ...DEFAULT_BUDGET, ...fileConfig };
|
|
54
|
+
|
|
55
|
+
// Apply profile if set
|
|
56
|
+
if (envProfile && merged.profiles && merged.profiles[envProfile]) {
|
|
57
|
+
Object.assign(merged, merged.profiles[envProfile]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Env overrides take final precedence
|
|
61
|
+
if (envMaxActions) {
|
|
62
|
+
const parsedMaxActions = parseInt(envMaxActions, 10);
|
|
63
|
+
if (Number.isFinite(parsedMaxActions) && parsedMaxActions > 0) {
|
|
64
|
+
merged.max_actions = parsedMaxActions;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (envMaxTime) {
|
|
68
|
+
const parsedMaxTime = parseInt(envMaxTime, 10);
|
|
69
|
+
if (Number.isFinite(parsedMaxTime) && parsedMaxTime > 0) {
|
|
70
|
+
merged.max_time_minutes = parsedMaxTime;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function loadBudgetState() {
|
|
78
|
+
try {
|
|
79
|
+
if (fs.existsSync(BUDGET_STATE_PATH)) {
|
|
80
|
+
return JSON.parse(fs.readFileSync(BUDGET_STATE_PATH, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
} catch { /* corrupted state — reset */ }
|
|
83
|
+
return { action_count: 0, session_start: new Date().toISOString() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function saveBudgetState(state) {
|
|
87
|
+
const dir = path.dirname(BUDGET_STATE_PATH);
|
|
88
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
fs.writeFileSync(BUDGET_STATE_PATH, JSON.stringify(state, null, 2));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resetBudget() {
|
|
93
|
+
const state = { action_count: 0, session_start: new Date().toISOString() };
|
|
94
|
+
saveBudgetState(state);
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Evaluate budget limits. Called before every gate evaluation.
|
|
100
|
+
* Returns null if within budget, or a deny result if budget exceeded.
|
|
101
|
+
*/
|
|
102
|
+
function evaluateBudget(toolName, toolInput) {
|
|
103
|
+
const config = loadBudgetConfig();
|
|
104
|
+
const state = loadBudgetState();
|
|
105
|
+
|
|
106
|
+
// Increment action count
|
|
107
|
+
state.action_count = (state.action_count || 0) + 1;
|
|
108
|
+
saveBudgetState(state);
|
|
109
|
+
|
|
110
|
+
// Check action limit
|
|
111
|
+
if (config.max_actions && state.action_count > config.max_actions) {
|
|
112
|
+
return {
|
|
113
|
+
decision: 'deny',
|
|
114
|
+
gate: 'budget-action-limit',
|
|
115
|
+
message: `Budget exceeded: ${state.action_count}/${config.max_actions} actions used. Session budget is exhausted.`,
|
|
116
|
+
severity: 'critical',
|
|
117
|
+
reasoning: `Tool call #${state.action_count} exceeds the configured max_actions limit of ${config.max_actions}.`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check time limit
|
|
122
|
+
if (config.max_time_minutes && state.session_start) {
|
|
123
|
+
const elapsedMs = Date.now() - new Date(state.session_start).getTime();
|
|
124
|
+
const elapsedMinutes = elapsedMs / (60 * 1000);
|
|
125
|
+
if (elapsedMinutes > config.max_time_minutes) {
|
|
126
|
+
return {
|
|
127
|
+
decision: 'deny',
|
|
128
|
+
gate: 'budget-time-limit',
|
|
129
|
+
message: `Budget exceeded: session has run ${Math.round(elapsedMinutes)}min, limit is ${config.max_time_minutes}min.`,
|
|
130
|
+
severity: 'critical',
|
|
131
|
+
reasoning: `Session duration (${Math.round(elapsedMinutes)}min) exceeds max_time_minutes (${config.max_time_minutes}).`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null; // Within budget
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get current budget status for dashboard/reporting.
|
|
141
|
+
*/
|
|
142
|
+
function getBudgetStatus() {
|
|
143
|
+
const config = loadBudgetConfig();
|
|
144
|
+
const state = loadBudgetState();
|
|
145
|
+
const elapsedMs = state.session_start
|
|
146
|
+
? Date.now() - new Date(state.session_start).getTime()
|
|
147
|
+
: 0;
|
|
148
|
+
const elapsedMinutes = Math.round(elapsedMs / (60 * 1000));
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
action_count: state.action_count || 0,
|
|
152
|
+
max_actions: config.max_actions,
|
|
153
|
+
actions_remaining: Math.max(0, (config.max_actions || Infinity) - (state.action_count || 0)),
|
|
154
|
+
actions_pct: config.max_actions ? Math.round(((state.action_count || 0) / config.max_actions) * 100) : 0,
|
|
155
|
+
elapsed_minutes: elapsedMinutes,
|
|
156
|
+
max_time_minutes: config.max_time_minutes,
|
|
157
|
+
time_remaining_minutes: Math.max(0, (config.max_time_minutes || Infinity) - elapsedMinutes),
|
|
158
|
+
time_pct: config.max_time_minutes ? Math.round((elapsedMinutes / config.max_time_minutes) * 100) : 0,
|
|
159
|
+
session_start: state.session_start,
|
|
160
|
+
profile: process.env.THUMBGATE_BUDGET_PROFILE || 'guided',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
evaluateBudget,
|
|
166
|
+
getBudgetStatus,
|
|
167
|
+
loadBudgetConfig,
|
|
168
|
+
loadBudgetState,
|
|
169
|
+
saveBudgetState,
|
|
170
|
+
resetBudget,
|
|
171
|
+
BUDGET_STATE_PATH,
|
|
172
|
+
DEFAULT_BUDGET_CONFIG_PATH,
|
|
173
|
+
};
|