thumbgate 1.34.3 → 1.37.1
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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +373 -5
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +653 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +21 -2
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +10 -1
- package/config/model-candidates.json +382 -24
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +401 -147
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/auto-promote-gates.js +178 -27
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-progress.js +111 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard-limits.js +27 -0
- package/scripts/dashboard.js +279 -9
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gate-stats.js +2 -2
- package/scripts/gates-engine.js +859 -58
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +802 -185
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
package/scripts/audit-trail.js
CHANGED
|
@@ -27,6 +27,200 @@ function getAuditLogPath() {
|
|
|
27
27
|
return path.join(resolveFeedbackDir(), AUDIT_LOG_FILENAME);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the acting non-human identity for audit attribution. Sessions and
|
|
32
|
+
* worktree leases export THUMBGATE_SESSION_AGENT; THUMBGATE_AGENT_ID is the
|
|
33
|
+
* generic override. Null when the caller cannot be attributed — audit records
|
|
34
|
+
* must never invent an identity.
|
|
35
|
+
*/
|
|
36
|
+
function resolveAuditAgentId() {
|
|
37
|
+
return process.env.THUMBGATE_SESSION_AGENT
|
|
38
|
+
|| process.env.THUMBGATE_AGENT_ID
|
|
39
|
+
|| null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Agent identity store — registry + observed-agent stream
|
|
44
|
+
//
|
|
45
|
+
// Lives here (not in org-dashboard) because the gates-engine identity gate and
|
|
46
|
+
// audit attribution are public runtime surfaces, while org-dashboard stays out
|
|
47
|
+
// of the npm tarball. org-dashboard re-exports these for its Pro reporting.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const REGISTRY_FILENAME = 'agent-registry.jsonl';
|
|
51
|
+
const OBSERVED_FILENAME = 'observed-agents.jsonl';
|
|
52
|
+
const OBSERVED_COMPACT_BYTES = 512 * 1024;
|
|
53
|
+
|
|
54
|
+
function getRegistryPath() {
|
|
55
|
+
return path.join(resolveFeedbackDir(), REGISTRY_FILENAME);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getObservedAgentsPath() {
|
|
59
|
+
return path.join(resolveFeedbackDir(), OBSERVED_FILENAME);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register an agent session. Called on MCP server startup or agent bootstrap.
|
|
64
|
+
*/
|
|
65
|
+
function registerAgent({ agentId, source, project, branch, metadata } = {}) {
|
|
66
|
+
const id = agentId || `agent_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
|
|
67
|
+
const record = {
|
|
68
|
+
id,
|
|
69
|
+
registeredAt: new Date().toISOString(),
|
|
70
|
+
lastSeenAt: new Date().toISOString(),
|
|
71
|
+
source: source || 'unknown',
|
|
72
|
+
project: project || path.basename(process.cwd()),
|
|
73
|
+
branch: branch || null,
|
|
74
|
+
toolCalls: 0,
|
|
75
|
+
gateBlocks: 0,
|
|
76
|
+
gateWarns: 0,
|
|
77
|
+
metadata: { lifecycleStatus: 'active', ...(metadata || {}) },
|
|
78
|
+
};
|
|
79
|
+
const registryPath = getRegistryPath();
|
|
80
|
+
ensureDir(path.dirname(registryPath));
|
|
81
|
+
fs.appendFileSync(registryPath, JSON.stringify(record) + '\n');
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Record agent activity — called after each tool call evaluation.
|
|
87
|
+
*/
|
|
88
|
+
function recordAgentActivity(agentId, decision) {
|
|
89
|
+
const registryPath = getRegistryPath();
|
|
90
|
+
if (!fs.existsSync(registryPath)) return;
|
|
91
|
+
const lines = fs.readFileSync(registryPath, 'utf-8').trim().split('\n');
|
|
92
|
+
const updated = [];
|
|
93
|
+
let found = false;
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
try {
|
|
96
|
+
const record = JSON.parse(line);
|
|
97
|
+
if (record.id === agentId && !found) {
|
|
98
|
+
record.lastSeenAt = new Date().toISOString();
|
|
99
|
+
record.toolCalls = (record.toolCalls || 0) + 1;
|
|
100
|
+
if (decision === 'deny') record.gateBlocks = (record.gateBlocks || 0) + 1;
|
|
101
|
+
if (decision === 'warn') record.gateWarns = (record.gateWarns || 0) + 1;
|
|
102
|
+
found = true;
|
|
103
|
+
}
|
|
104
|
+
updated.push(JSON.stringify(record));
|
|
105
|
+
} catch {
|
|
106
|
+
updated.push(line);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
fs.writeFileSync(registryPath, updated.join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Load all registered agent sessions.
|
|
114
|
+
*/
|
|
115
|
+
function loadAgentRegistry() {
|
|
116
|
+
const registryPath = getRegistryPath();
|
|
117
|
+
if (!fs.existsSync(registryPath)) return [];
|
|
118
|
+
const raw = fs.readFileSync(registryPath, 'utf-8').trim();
|
|
119
|
+
if (!raw) return [];
|
|
120
|
+
const records = [];
|
|
121
|
+
for (const line of raw.split('\n')) {
|
|
122
|
+
try {
|
|
123
|
+
records.push(JSON.parse(line));
|
|
124
|
+
} catch {
|
|
125
|
+
// Skip corrupt rows; identity checks fail open, never crash.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return records;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Retire an agent identity. A retired agent that keeps acting is flagged by
|
|
133
|
+
* the gates-engine identity gate (deny under strict enforcement).
|
|
134
|
+
*/
|
|
135
|
+
function retireAgent(agentId, reason) {
|
|
136
|
+
const registryPath = getRegistryPath();
|
|
137
|
+
if (!fs.existsSync(registryPath)) return false;
|
|
138
|
+
const lines = fs.readFileSync(registryPath, 'utf-8').trim().split('\n');
|
|
139
|
+
const updated = [];
|
|
140
|
+
let found = false;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
try {
|
|
143
|
+
const record = JSON.parse(line);
|
|
144
|
+
if (record.id === agentId) {
|
|
145
|
+
record.metadata = record.metadata || {};
|
|
146
|
+
record.metadata.lifecycleStatus = 'retired';
|
|
147
|
+
record.metadata.retiredAt = new Date().toISOString();
|
|
148
|
+
if (reason) record.metadata.retireReason = String(reason);
|
|
149
|
+
found = true;
|
|
150
|
+
}
|
|
151
|
+
updated.push(JSON.stringify(record));
|
|
152
|
+
} catch {
|
|
153
|
+
updated.push(line);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (found) fs.writeFileSync(registryPath, updated.join('\n') + '\n');
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Record one observation of an acting agent — the producer side of shadow-AI
|
|
162
|
+
* detection, called from the gates-engine evaluation path on every attributed
|
|
163
|
+
* tool call. Appends are plain (never lost to a busy lock); compaction runs
|
|
164
|
+
* only under the cross-process ledger lock so a concurrent agent's append can
|
|
165
|
+
* never be truncated by the read-aggregate-rewrite window.
|
|
166
|
+
*/
|
|
167
|
+
function recordObservedAgent(agentId) {
|
|
168
|
+
const id = String(agentId || '').trim();
|
|
169
|
+
if (!id) return null;
|
|
170
|
+
const observedPath = getObservedAgentsPath();
|
|
171
|
+
ensureDir(path.dirname(observedPath));
|
|
172
|
+
const event = { id, seenAt: new Date().toISOString() };
|
|
173
|
+
fs.appendFileSync(observedPath, JSON.stringify(event) + '\n');
|
|
174
|
+
try {
|
|
175
|
+
if (fs.statSync(observedPath).size > OBSERVED_COMPACT_BYTES) {
|
|
176
|
+
const { withFileLedgerLock } = require('./file-ledger-lock');
|
|
177
|
+
withFileLedgerLock(observedPath + '.lock', () => {
|
|
178
|
+
const compacted = loadObservedAgents()
|
|
179
|
+
.map((row) => JSON.stringify(row))
|
|
180
|
+
.join('\n');
|
|
181
|
+
fs.writeFileSync(observedPath, compacted + '\n');
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// Busy lock or read error: skip compaction, never drop the observation.
|
|
186
|
+
}
|
|
187
|
+
return event;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Aggregate observation events into one row per agent id:
|
|
192
|
+
* { id, firstSeenAt, lastSeenAt, observations }.
|
|
193
|
+
*/
|
|
194
|
+
function loadObservedAgents() {
|
|
195
|
+
const observedPath = getObservedAgentsPath();
|
|
196
|
+
if (!fs.existsSync(observedPath)) return [];
|
|
197
|
+
const byId = new Map();
|
|
198
|
+
const raw = fs.readFileSync(observedPath, 'utf-8').trim();
|
|
199
|
+
if (!raw) return [];
|
|
200
|
+
for (const line of raw.split('\n')) {
|
|
201
|
+
let event;
|
|
202
|
+
try {
|
|
203
|
+
event = JSON.parse(line);
|
|
204
|
+
} catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const id = String(event.id || '').trim();
|
|
208
|
+
if (!id) continue;
|
|
209
|
+
const seenAt = event.seenAt || event.lastSeenAt || new Date().toISOString();
|
|
210
|
+
const row = byId.get(id) || {
|
|
211
|
+
id,
|
|
212
|
+
firstSeenAt: event.firstSeenAt || seenAt,
|
|
213
|
+
lastSeenAt: seenAt,
|
|
214
|
+
observations: 0,
|
|
215
|
+
};
|
|
216
|
+
if (seenAt < row.firstSeenAt) row.firstSeenAt = seenAt;
|
|
217
|
+
if (seenAt > row.lastSeenAt) row.lastSeenAt = seenAt;
|
|
218
|
+
row.observations += Number(event.observations) > 0 ? Number(event.observations) : 1;
|
|
219
|
+
byId.set(id, row);
|
|
220
|
+
}
|
|
221
|
+
return [...byId.values()];
|
|
222
|
+
}
|
|
223
|
+
|
|
30
224
|
|
|
31
225
|
// ---------------------------------------------------------------------------
|
|
32
226
|
// Core audit record
|
|
@@ -53,6 +247,7 @@ function recordAuditEvent(params = {}) {
|
|
|
53
247
|
timestamp: new Date().toISOString(),
|
|
54
248
|
toolName: params.toolName || 'unknown',
|
|
55
249
|
toolInput: sanitizeToolInput(params.toolInput || {}),
|
|
250
|
+
agentId: params.agentId || resolveAuditAgentId(),
|
|
56
251
|
decision: params.decision || 'allow',
|
|
57
252
|
gateId: params.gateId || null,
|
|
58
253
|
message: params.message || null,
|
|
@@ -61,6 +256,13 @@ function recordAuditEvent(params = {}) {
|
|
|
61
256
|
source: params.source || 'gates-engine',
|
|
62
257
|
};
|
|
63
258
|
|
|
259
|
+
// Typed override payload (see scripts/override-audit.js). Carried verbatim so
|
|
260
|
+
// an override is filterable by decision === 'override' rather than having to
|
|
261
|
+
// be inferred from toolName, which cannot distinguish it from a normal call.
|
|
262
|
+
if (params.override && typeof params.override === 'object') {
|
|
263
|
+
record.override = params.override;
|
|
264
|
+
}
|
|
265
|
+
|
|
64
266
|
// Safe stringify: never let circular/toxic tool inputs crash the gate path
|
|
65
267
|
// (Antithesis-style invariant: evaluation + audit must not throw).
|
|
66
268
|
let line;
|
|
@@ -353,6 +555,16 @@ module.exports = {
|
|
|
353
555
|
recordAuditEvent,
|
|
354
556
|
auditToFeedback,
|
|
355
557
|
readAuditLog,
|
|
558
|
+
registerAgent,
|
|
559
|
+
recordAgentActivity,
|
|
560
|
+
loadAgentRegistry,
|
|
561
|
+
retireAgent,
|
|
562
|
+
recordObservedAgent,
|
|
563
|
+
loadObservedAgents,
|
|
564
|
+
getRegistryPath,
|
|
565
|
+
getObservedAgentsPath,
|
|
566
|
+
REGISTRY_FILENAME,
|
|
567
|
+
OBSERVED_FILENAME,
|
|
356
568
|
auditStats,
|
|
357
569
|
latencyStats,
|
|
358
570
|
skillAdherence,
|
|
@@ -206,13 +206,31 @@ function extractExecutableAction(entry) {
|
|
|
206
206
|
const ctx = String(entry.context || entry.whatWentWrong || '').trim();
|
|
207
207
|
if (ctx.length < 4) return null;
|
|
208
208
|
|
|
209
|
+
// Strong signal: known tool prefixes — accept immediately.
|
|
210
|
+
const known = /^(?:sudo\s+)?(?:kubectl|git|npm|npx|yarn|pnpm|python|python3|node|curl|wget|docker|podman|rm|mv|cp|chmod|chown|psql|mysql|mongo|terraform|pulumi|aws|gcloud|az|helm|ssh|scp|rsync|make|cargo|go|ruby|perl|bash|sh|zsh)\b/i.test(ctx);
|
|
211
|
+
if (known && ctx.length <= 240) return ctx;
|
|
212
|
+
|
|
213
|
+
// Reject agent-narration prose before the weak "looks like a command" heuristic.
|
|
214
|
+
// "Agent (grok) auto-sent …" was previously accepted because it starts with a
|
|
215
|
+
// single token + spaces, then became an inert force-gate pattern (AGENT-259).
|
|
216
|
+
if (/^(?:agent|the|user|ceo|claude|grok|codex|gemini|assistant|operator)\b/i.test(ctx)) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (/\b(?:without\s+(?:human\s+)?review|thumbs?-?down|auto-sent|always-approve)\b/i.test(ctx)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
// Parenthetical asides ("Agent (grok, always-approve)") are prose, not argv.
|
|
223
|
+
if (/\([^)]{0,40}\b(?:grok|claude|codex|always-approve|agent)\b/i.test(ctx)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
209
227
|
// Looks like a shell / CLI invocation (not free-form prose).
|
|
210
228
|
const looksExecutable = /^(?:sudo\s+)?(?:~\/|\.\/|\/)?(?:[A-Za-z0-9._+-]+\/)*[A-Za-z0-9._+-]+(?:\s|$)/.test(ctx)
|
|
211
229
|
&& /\s|^[a-z0-9._+-]+(?:\s|$)/i.test(ctx)
|
|
212
|
-
&& !/\s+(?:broke|failed|wrong|should|never|please|the agent)\b/i.test(ctx.slice(0,
|
|
213
|
-
//
|
|
214
|
-
const
|
|
215
|
-
if (
|
|
230
|
+
&& !/\s+(?:broke|failed|wrong|should|never|please|the agent|auto-sent|emailed)\b/i.test(ctx.slice(0, 100));
|
|
231
|
+
// Weak path: require a flag or path-like token so "Agent wrote a bad summary" fails.
|
|
232
|
+
const hasCliShape = /(?:\s-{1,2}[A-Za-z][\w-]*|\/[A-Za-z0-9._-]{2,}|\.(?:js|ts|py|sh|json|yml|yaml)\b)/.test(ctx);
|
|
233
|
+
if (looksExecutable && hasCliShape && ctx.length <= 200) {
|
|
216
234
|
return ctx;
|
|
217
235
|
}
|
|
218
236
|
return null;
|
|
@@ -307,6 +325,64 @@ function contextToPattern(context) {
|
|
|
307
325
|
return raw.slice(0, 120).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
326
|
}
|
|
309
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Known incident classes that appear as English prose in force-gate / feedback
|
|
330
|
+
* but map to a deterministic tool-call surface. Without this, force-promote of
|
|
331
|
+
* "agent emailed X without review" stores the sentence as a regex and never
|
|
332
|
+
* matches any tool call (AGENT-259, June 2026 + Aug 2026 recurrence).
|
|
333
|
+
*/
|
|
334
|
+
function deriveSurfacePattern(context) {
|
|
335
|
+
const text = String(context || '');
|
|
336
|
+
if (!text.trim()) return null;
|
|
337
|
+
|
|
338
|
+
const emailish = /\b(gmail|e-?mail|smtp|sendmail|msmtp|mailx|nodemailer|smtplib|messages\/send|send_message|send_draft|send-mail|outbound\s+email|cold\s+outreach)\b/i.test(text);
|
|
339
|
+
const sendish = /\b(send|sent|sending|emailed|mail(ed)?)\b/i.test(text);
|
|
340
|
+
if (emailish && sendish) {
|
|
341
|
+
// Mirrors config/gates/default.json outbound-email-send + the live
|
|
342
|
+
// ~/.thumbgate/bin/outbound-email-guard.js surface set.
|
|
343
|
+
return '(?:(?:^|[_.])send[_-]?(?:message|mail|email|now|draft)s?\\b|emailer[_-]?messages?[_-]?send|users\\/[^/\\s"\']+\\/messages\\/send|\\bmessages\\/send\\b|messages\\s*\\(\\s*\\)\\s*\\.\\s*send\\s*\\(|\\bsendmail\\b|\\bmsmtp\\b|\\bsmtplib\\b|\\bnodemailer\\b)';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (/\bforce[- ]?push\b|git\s+push\s+(?:-f|--force)\b/i.test(text)) {
|
|
347
|
+
return 'git\\s+push\\s+(--force|-f)';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (/\b(rm\s+-rf\s+\/|sudo\s+rm\s+-rf)\b/i.test(text)) {
|
|
351
|
+
return '(?:sudo\\s+)?rm\\s+-rf\\s+/';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* True when a gate pattern is essentially an English sentence — many words,
|
|
359
|
+
* no executable tokens — and therefore cannot fire against tool-call text.
|
|
360
|
+
*/
|
|
361
|
+
function isInertProsePattern(pattern) {
|
|
362
|
+
if (!pattern || typeof pattern !== 'string') return true;
|
|
363
|
+
// Intentional multi-alternative surface matchers (email send, force-push classes).
|
|
364
|
+
if (/\(\?:/.test(pattern) && pattern.includes('|')) return false;
|
|
365
|
+
// Unescape common contextToPattern escapes so we can inspect the words.
|
|
366
|
+
const unescaped = pattern
|
|
367
|
+
.replace(/\\([.*+?^${}()|[\]\\])/g, '$1')
|
|
368
|
+
.replace(/\\\\/g, '\\');
|
|
369
|
+
const words = (unescaped.match(/[A-Za-z]{3,}/g) || []);
|
|
370
|
+
if (words.length < 6) return false;
|
|
371
|
+
// Short CLI / known surface tokens only — a long English sentence that merely
|
|
372
|
+
// *mentions* "gmail" is still inert (it will never equal a tool-call string).
|
|
373
|
+
const trimmed = unescaped.trim();
|
|
374
|
+
const looksLikeCli =
|
|
375
|
+
/^(?:sudo\s+)?(?:kubectl|git|npm|npx|yarn|pnpm|python|python3|node|curl|wget|docker|podman|rm|bash|sh|zsh)\b/i.test(trimmed)
|
|
376
|
+
&& trimmed.length <= 160;
|
|
377
|
+
const looksLikeSurfaceToken =
|
|
378
|
+
words.length <= 8
|
|
379
|
+
&& /(?:send_message|send_draft|messages\/send|sendmail|force-push|git\s+push\s+(?:-f|--force))/i.test(trimmed);
|
|
380
|
+
if (looksLikeCli || looksLikeSurfaceToken) return false;
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
310
386
|
/**
|
|
311
387
|
* A gate that cannot match the very context that produced it is inert — it
|
|
312
388
|
* shows up in the dashboard as an active blocking rule while enforcing nothing.
|
|
@@ -322,39 +398,51 @@ function gateMatchesOwnContext(gate, context) {
|
|
|
322
398
|
}
|
|
323
399
|
|
|
324
400
|
function buildGateRule(group, actionOverride) {
|
|
325
|
-
|
|
401
|
+
// Tests and callers may pass partial groups; never crash on missing fields.
|
|
402
|
+
const g = group && typeof group === 'object' ? group : {};
|
|
403
|
+
const action = actionOverride || (g.count === 'MANUAL' ? g.manualAction || 'block' : (g.count >= BLOCK_THRESHOLD ? 'block' : 'warn'));
|
|
326
404
|
const severity = action === 'block' ? 'critical' : action === 'approve' ? 'high' : 'medium';
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
405
|
+
const fromExecutable = (g.latestExecutable
|
|
406
|
+
|| extractExecutableAction({ context: g.latestContext })
|
|
407
|
+
|| '').slice(0, 120);
|
|
408
|
+
// Prefer real executable text; else a derived surface regex (email send, force-push).
|
|
409
|
+
// NEVER fall back to free-form English prose — that produced permanently inert gates.
|
|
410
|
+
const surfacePattern = g.surfacePattern || deriveSurfacePattern(g.latestContext || g.key || '');
|
|
411
|
+
const pattern = fromExecutable
|
|
412
|
+
? contextToPattern(fromExecutable)
|
|
413
|
+
: (surfacePattern || null);
|
|
414
|
+
const context = (fromExecutable || g.latestContext || g.key || '').slice(0, 120);
|
|
415
|
+
const kind = String(g.key || '').startsWith('diagnosis:')
|
|
330
416
|
? 'repeated diagnosis'
|
|
331
|
-
:
|
|
417
|
+
: String(g.key || '').startsWith('constraint:')
|
|
332
418
|
? 'repeated constraint violation'
|
|
333
|
-
: 'repeated executable action';
|
|
419
|
+
: (fromExecutable ? 'repeated executable action' : 'derived surface guard');
|
|
334
420
|
|
|
335
|
-
const occurrencesText =
|
|
421
|
+
const occurrencesText = g.count === 'MANUAL' ? 'manual' : `${g.count == null ? 0 : g.count} occurrences`;
|
|
336
422
|
const suggestedMessage = `Auto-promoted ${kind}: "${context}" (${occurrencesText} in ${WINDOW_DAYS} days)`;
|
|
337
423
|
|
|
338
424
|
// TTL: auto-promoted rules expire after the configured window unless
|
|
339
425
|
// refreshed by a fresh fire. Manual force-promote bypasses TTL — operator
|
|
340
426
|
// says "permanent" by going through the force path.
|
|
341
427
|
const nowMs = Date.now();
|
|
342
|
-
const isManual =
|
|
428
|
+
const isManual = g.count === 'MANUAL';
|
|
343
429
|
const expiresAt = isManual ? null : new Date(nowMs + getRuleTtlMs()).toISOString();
|
|
344
430
|
|
|
345
431
|
return {
|
|
346
|
-
id: patternToGateId(
|
|
347
|
-
trigger: `auto:${
|
|
348
|
-
// Derived from
|
|
349
|
-
pattern
|
|
432
|
+
id: patternToGateId(String(g.key || 'unknown')),
|
|
433
|
+
trigger: `auto:${g.key || 'unknown'}`,
|
|
434
|
+
// Derived from executable action OR known surface class — never raw prose.
|
|
435
|
+
pattern,
|
|
350
436
|
action,
|
|
351
437
|
message: suggestedMessage,
|
|
352
438
|
severity,
|
|
353
|
-
|
|
439
|
+
// Always numeric — string 'MANUAL' concatenated into gate-stats totals (0MANUAL…).
|
|
440
|
+
occurrences: g.count === 'MANUAL' ? 1 : Number(g.count) || 0,
|
|
354
441
|
promotedAt: new Date().toISOString(),
|
|
355
442
|
expiresAt,
|
|
356
443
|
lastFiredAt: null,
|
|
357
|
-
source:
|
|
444
|
+
source: g.source || 'auto-promote',
|
|
445
|
+
...(surfacePattern && !fromExecutable ? { surfaceDerived: true } : {}),
|
|
358
446
|
};
|
|
359
447
|
}
|
|
360
448
|
|
|
@@ -430,21 +518,51 @@ function recordGateFire(data, gateId, now = Date.now()) {
|
|
|
430
518
|
|
|
431
519
|
function forcePromote(context, action = 'block') {
|
|
432
520
|
if (!context) throw new Error('context is required for force-promote');
|
|
521
|
+
let executable = extractExecutableAction({ context });
|
|
522
|
+
// If extractExecutableAction returned narration that would become inert prose,
|
|
523
|
+
// drop it and fall through to surface derivation (email send, force-push, …).
|
|
524
|
+
if (executable && isInertProsePattern(contextToPattern(executable))) {
|
|
525
|
+
executable = null;
|
|
526
|
+
}
|
|
527
|
+
const surfacePattern = deriveSurfacePattern(context);
|
|
528
|
+
// Prefer a known surface class over a weak "command-shaped" string when both exist
|
|
529
|
+
// and the command is not a known CLI prefix — prevents dual wrong-pattern promotion.
|
|
530
|
+
if (executable && surfacePattern && !/^(?:sudo\s+)?(?:kubectl|git|npm|npx|curl|python|python3|node|bash|sh|zsh|rm)\b/i.test(executable)) {
|
|
531
|
+
executable = null;
|
|
532
|
+
}
|
|
533
|
+
if (!executable && !surfacePattern) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
'force-promote refused: context is prose without a matchable tool surface. '
|
|
536
|
+
+ 'Pass an executable command (e.g. "git push --force") or describe a known '
|
|
537
|
+
+ 'class (email send / Gmail messages/send, force-push, rm -rf /).',
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
433
541
|
const data = loadAutoGates();
|
|
434
542
|
const gateId = patternToGateId(context);
|
|
435
|
-
|
|
543
|
+
|
|
436
544
|
// Remove existing if any
|
|
437
545
|
data.gates = data.gates.filter(g => g.id !== gateId);
|
|
438
|
-
|
|
546
|
+
|
|
439
547
|
const gate = buildGateRule({
|
|
440
548
|
key: context,
|
|
441
549
|
latestContext: context,
|
|
550
|
+
latestExecutable: executable || '',
|
|
551
|
+
surfacePattern: surfacePattern || undefined,
|
|
442
552
|
count: 'MANUAL',
|
|
443
553
|
manualAction: action,
|
|
444
|
-
source: 'force-promote'
|
|
554
|
+
source: 'force-promote',
|
|
445
555
|
});
|
|
556
|
+
|
|
557
|
+
if (!gate.pattern || isInertProsePattern(gate.pattern)) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
'force-promote refused: derived pattern is inert prose and would never fire. '
|
|
560
|
+
+ `pattern=${JSON.stringify(gate.pattern)}`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
446
564
|
data.gates.unshift(gate);
|
|
447
|
-
|
|
565
|
+
|
|
448
566
|
if (data.gates.length > MAX_AUTO_GATES) {
|
|
449
567
|
data.gates = data.gates.slice(0, MAX_AUTO_GATES);
|
|
450
568
|
}
|
|
@@ -454,12 +572,20 @@ function forcePromote(context, action = 'block') {
|
|
|
454
572
|
gateId,
|
|
455
573
|
context,
|
|
456
574
|
action,
|
|
575
|
+
pattern: gate.pattern,
|
|
576
|
+
surfaceDerived: Boolean(gate.surfaceDerived),
|
|
457
577
|
promotedAt: new Date().toISOString(),
|
|
458
|
-
source: 'force-promote'
|
|
578
|
+
source: 'force-promote',
|
|
459
579
|
});
|
|
460
580
|
|
|
461
581
|
saveAutoGates(data);
|
|
462
|
-
return {
|
|
582
|
+
return {
|
|
583
|
+
gateId,
|
|
584
|
+
action,
|
|
585
|
+
pattern: gate.pattern,
|
|
586
|
+
surfaceDerived: Boolean(gate.surfaceDerived),
|
|
587
|
+
totalGates: data.gates.length,
|
|
588
|
+
};
|
|
463
589
|
}
|
|
464
590
|
|
|
465
591
|
function promote(feedbackLogPath, options) {
|
|
@@ -472,10 +598,22 @@ function promote(feedbackLogPath, options) {
|
|
|
472
598
|
// path rather than carrying a near-stale expiresAt.
|
|
473
599
|
const { data: expiredData, expired } = expireGates(loadAutoGates());
|
|
474
600
|
const data = expiredData;
|
|
475
|
-
|
|
601
|
+
// Drop permanently inert prose gates left by older force-promote / promotion bugs.
|
|
602
|
+
const inertRemoved = [];
|
|
603
|
+
data.gates = (data.gates || []).filter((g) => {
|
|
604
|
+
if (g && isInertProsePattern(g.pattern)) {
|
|
605
|
+
inertRemoved.push(g);
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
});
|
|
610
|
+
if (expired.length > 0 || inertRemoved.length > 0) {
|
|
476
611
|
saveAutoGates(data);
|
|
477
612
|
}
|
|
478
|
-
const promotions =
|
|
613
|
+
const promotions = [
|
|
614
|
+
...expired.map((e) => ({ type: 'expired', gateId: e.id, expiredAt: e.expiresAt })),
|
|
615
|
+
...inertRemoved.map((e) => ({ type: 'quarantined-inert-prose', gateId: e.id, pattern: e.pattern })),
|
|
616
|
+
];
|
|
479
617
|
|
|
480
618
|
for (const group of Object.values(groups)) {
|
|
481
619
|
if (group.count < WARN_THRESHOLD) continue;
|
|
@@ -520,7 +658,9 @@ function promote(feedbackLogPath, options) {
|
|
|
520
658
|
// Never persist a gate that cannot match the context that produced it. Such a
|
|
521
659
|
// gate renders in the dashboard as an active blocking rule while enforcing
|
|
522
660
|
// nothing, which reads as "the agent learned" when it did not.
|
|
523
|
-
|
|
661
|
+
// Surface-derived patterns (email send, force-push) intentionally do NOT match
|
|
662
|
+
// the English prose incident text — they match the tool surface instead.
|
|
663
|
+
if (!gate.surfaceDerived && !gateMatchesOwnContext(gate, group.latestContext)) {
|
|
524
664
|
promotions.push({
|
|
525
665
|
type: 'skipped-unmatchable',
|
|
526
666
|
gateId: gate.id,
|
|
@@ -529,6 +669,15 @@ function promote(feedbackLogPath, options) {
|
|
|
529
669
|
});
|
|
530
670
|
continue;
|
|
531
671
|
}
|
|
672
|
+
if (!gate.pattern || isInertProsePattern(gate.pattern)) {
|
|
673
|
+
promotions.push({
|
|
674
|
+
type: 'skipped-inert-prose',
|
|
675
|
+
gateId: gate.id,
|
|
676
|
+
reason: 'pattern is English prose and would never match a tool call',
|
|
677
|
+
occurrences: group.count,
|
|
678
|
+
});
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
532
681
|
|
|
533
682
|
// Self-Harness stage 3: before a feedback rule goes live as a hard block,
|
|
534
683
|
// regression-test it against prior allowed actions. If it would have blocked
|
|
@@ -619,6 +768,8 @@ module.exports = {
|
|
|
619
768
|
patternToGateId,
|
|
620
769
|
buildGateRule,
|
|
621
770
|
contextToPattern,
|
|
771
|
+
deriveSurfacePattern,
|
|
772
|
+
isInertProsePattern,
|
|
622
773
|
gateMatchesOwnContext,
|
|
623
774
|
regressionCheck,
|
|
624
775
|
getAuditTrailPath,
|
package/scripts/billing.js
CHANGED
|
@@ -110,7 +110,7 @@ const CONFIG = {
|
|
|
110
110
|
},
|
|
111
111
|
RESEND_API_KEY: process.env.RESEND_API_KEY || process.env.THUMBGATE_RESEND_API_KEY || '',
|
|
112
112
|
TRIAL_EMAIL_FROM: process.env.THUMBGATE_TRIAL_EMAIL_FROM || process.env.RESEND_FROM_EMAIL || process.env.RESEND_FROM || 'onboarding@resend.dev',
|
|
113
|
-
TRIAL_EMAIL_REPLY_TO: process.env.THUMBGATE_TRIAL_EMAIL_REPLY_TO || '
|
|
113
|
+
TRIAL_EMAIL_REPLY_TO: process.env.THUMBGATE_TRIAL_EMAIL_REPLY_TO || 'support@thumbgate.ai',
|
|
114
114
|
CREDIT_PACKS: {}
|
|
115
115
|
};
|
|
116
116
|
|