thumbgate 1.27.19 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/.claude/commands/dashboard.md +15 -0
  2. package/.claude/commands/thumbgate-blocked.md +27 -0
  3. package/.claude/commands/thumbgate-dashboard.md +15 -0
  4. package/.claude/commands/thumbgate-doctor.md +30 -0
  5. package/.claude/commands/thumbgate-guard.md +36 -0
  6. package/.claude/commands/thumbgate-protect.md +30 -0
  7. package/.claude/commands/thumbgate-rules.md +30 -0
  8. package/.claude-plugin/plugin.json +1 -1
  9. package/.well-known/mcp/server-card.json +1 -1
  10. package/README.md +63 -45
  11. package/adapters/claude/.mcp.json +2 -2
  12. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  13. package/adapters/letta/README.md +41 -0
  14. package/adapters/letta/thumbgate-letta-adapter.js +133 -0
  15. package/adapters/mcp/server-stdio.js +1 -1
  16. package/adapters/opencode/opencode.json +1 -1
  17. package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
  18. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
  19. package/bench/observability-eval-suite.json +26 -0
  20. package/bin/cli.js +52 -9
  21. package/bin/dashboard-cli.js +7 -0
  22. package/bin/postinstall.js +14 -23
  23. package/commands/dashboard.md +15 -0
  24. package/commands/thumbgate-dashboard.md +15 -0
  25. package/config/entitlement-public-keys.json +6 -0
  26. package/config/gates/default.json +1 -1
  27. package/config/github-about.json +2 -2
  28. package/config/merge-quality-checks.json +3 -0
  29. package/package.json +233 -100
  30. package/public/about.html +159 -0
  31. package/public/agent-manager.html +179 -0
  32. package/public/agents-cost-savings.html +153 -0
  33. package/public/ai-malpractice-prevention.html +818 -0
  34. package/public/assets/brand/github-social-preview.png +0 -0
  35. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  36. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  37. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  38. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  39. package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
  40. package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
  41. package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
  42. package/public/assets/brand/thumbgate-mark-team.svg +26 -0
  43. package/public/assets/brand/thumbgate-mark.svg +21 -0
  44. package/public/assets/brand/thumbgate-wordmark.svg +20 -0
  45. package/public/assets/claude-thumbgate-statusbar.svg +8 -0
  46. package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
  47. package/public/assets/legal-intake-control-flow.svg +66 -0
  48. package/public/blog.html +12 -9
  49. package/public/brand/thumbgate-mark.svg +19 -0
  50. package/public/brand/thumbgate-og.svg +16 -0
  51. package/public/chatgpt-app.html +330 -0
  52. package/public/codex-enterprise.html +123 -0
  53. package/public/codex-plugin.html +5 -5
  54. package/public/compare.html +10 -10
  55. package/public/dashboard.html +1 -1
  56. package/public/diagnostic.html +345 -0
  57. package/public/federal.html +2 -2
  58. package/public/guide.html +14 -14
  59. package/public/index.html +81 -70
  60. package/public/install.html +193 -0
  61. package/public/js/buyer-intent.js +672 -0
  62. package/public/learn.html +4 -17
  63. package/public/numbers.html +2 -2
  64. package/public/pricing.html +392 -0
  65. package/public/pro.html +2 -2
  66. package/scripts/action-receipts.js +324 -0
  67. package/scripts/activation-quickstart.js +187 -0
  68. package/scripts/agent-operations-planner.js +621 -0
  69. package/scripts/agent-reward-model.js +13 -0
  70. package/scripts/ai-component-inventory.js +367 -0
  71. package/scripts/async-eval-observability.js +236 -0
  72. package/scripts/audit.js +65 -0
  73. package/scripts/aws-blocks-guardrails.js +272 -0
  74. package/scripts/bayes-optimal-gate.js +6 -1
  75. package/scripts/classifier-routing.js +130 -0
  76. package/scripts/cli-feedback.js +17 -1
  77. package/scripts/commercial-offer.js +3 -3
  78. package/scripts/dashboard-chat.js +332 -0
  79. package/scripts/entitlement.js +250 -0
  80. package/scripts/export-databricks-bundle.js +5 -0
  81. package/scripts/export-dpo-pairs.js +6 -0
  82. package/scripts/export-hf-dataset.js +5 -0
  83. package/scripts/feedback-aggregate.js +281 -0
  84. package/scripts/feedback-sanitizer.js +105 -0
  85. package/scripts/gates-engine.js +15 -3
  86. package/scripts/hook-stop-anti-claim.js +301 -0
  87. package/scripts/imperative-detector.js +85 -0
  88. package/scripts/install-shim.js +87 -0
  89. package/scripts/intervention-policy.js +13 -0
  90. package/scripts/mcp-oauth.js +293 -0
  91. package/scripts/noop-detect.js +285 -0
  92. package/scripts/parallel-workflow-orchestrator.js +293 -0
  93. package/scripts/plan-gate.js +243 -0
  94. package/scripts/plausible-domain-config.js +99 -0
  95. package/scripts/pr-manager.js +9 -22
  96. package/scripts/pro-local-dashboard.js +198 -0
  97. package/scripts/qa-scenario-planner.js +136 -0
  98. package/scripts/repeat-metric.js +137 -0
  99. package/scripts/risk-scorer.js +6 -0
  100. package/scripts/secret-fixture-tokens.js +61 -0
  101. package/scripts/secret-redaction.js +166 -0
  102. package/scripts/self-harness-optimizer.js +141 -0
  103. package/scripts/self-healing-check.js +193 -0
  104. package/scripts/self-protection.js +90 -0
  105. package/scripts/seo-gsd.js +2 -2
  106. package/scripts/silent-failure-cluster.js +531 -0
  107. package/scripts/statusline-cache-read.js +57 -0
  108. package/scripts/sync-telemetry-from-prod.js +374 -0
  109. package/scripts/thompson-sampling.js +11 -2
  110. package/scripts/tool-contract-validator.js +76 -0
  111. package/scripts/trajectory-scorer.js +63 -0
  112. package/scripts/verify-marketing-pages-deployed.js +212 -0
  113. package/scripts/visitor-journey.js +172 -0
  114. package/src/api/server.js +81 -10
  115. package/.claude-plugin/marketplace.json +0 -85
  116. package/adapters/chatgpt/openapi.yaml +0 -1707
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * secret-redaction.js — the single, canonical secret-redaction helper.
5
+ *
6
+ * WHY THIS EXISTS
7
+ * On 2026-06-10 a live Stripe `sk_live_` key was found in plaintext inside a captured
8
+ * `.thumbgate/conversation-window.jsonl`. ThumbGate must never become a secret-leak vector:
9
+ * secrets must be redacted (a) before any conversation/feedback/memory record lands on disk,
10
+ * and (b) before any training/analytics dataset is exported or shared.
11
+ *
12
+ * This module is the ONE place that owns redaction-at-rest. Capture writers and dataset
13
+ * exporters all call into it instead of each rolling their own. It is intentionally separate
14
+ * from `secret-scanner.js`: that module is the PreToolUse *scan/block* layer and deliberately
15
+ * tolerates `sk_test_`/`test_token` inside commands. For data persisted to disk we redact
16
+ * test-mode secrets too — a `sk_test_` key is still a credential we should not store.
17
+ *
18
+ * NOT REDACTED ON PURPOSE
19
+ * - Stripe publishable keys (`pk_live_`, `pk_test_`) are public by design. No pattern matches
20
+ * them, and the generic `key=value` pattern keys on `api_key|secret|token|...`, never on a
21
+ * bare `publishable_key`, so they are preserved verbatim.
22
+ *
23
+ * Properties:
24
+ * - Idempotent: re-redacting already-redacted text is a no-op (the `[REDACTED:id]` marker
25
+ * contains characters outside every value charset).
26
+ * - Non-mutating: `redactSecretsDeep` returns a redacted copy; inputs are untouched.
27
+ */
28
+
29
+ const REDACTED = (id) => `[REDACTED:${id}]`;
30
+
31
+ // Identifier names that denote a credential, used by the generic `key = value` pattern. Kept as
32
+ // two small matchers (instead of one mega-alternation) so the assignment regex stays simple.
33
+ const SECRET_KEY_NAME = /(?:api[_-]?key|secret|token|password|passwd|credential|private[_-]?key|access[_-]?token|client[_-]?secret)/i;
34
+ // Public identifiers whose values are not secrets (e.g. Stripe publishable `pk_*` keys). Excluded
35
+ // so `publishable_key = pk_live_…` is preserved verbatim.
36
+ const PUBLIC_KEY_NAME = /publishable|public/i;
37
+
38
+ /**
39
+ * Ordered list of redaction patterns. Order matters: the most specific provider patterns run
40
+ * before the broad `key=value` fallback so a known secret keeps its precise label. Every regex
41
+ * is `g` (and case-insensitive where appropriate) so all occurrences are replaced.
42
+ *
43
+ * `replace` may be a string or a function `(match, ...groups) => string`.
44
+ */
45
+ const SECRET_REDACTION_PATTERNS = [
46
+ // PEM / OpenSSH private key blocks — redact the whole block.
47
+ {
48
+ id: 'private_key_block',
49
+ label: 'Private key block',
50
+ regex: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----[\s\S]+?-----END (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g,
51
+ },
52
+
53
+ // Stripe secret-bearing keys. Publishable keys (pk_*) are intentionally absent.
54
+ { id: 'stripe_live_secret', label: 'Stripe live secret key', regex: /\bsk_live_[A-Za-z0-9]{8,}/g },
55
+ { id: 'stripe_test_secret', label: 'Stripe test secret key', regex: /\bsk_test_[A-Za-z0-9]{8,}/g },
56
+ { id: 'stripe_restricted_live', label: 'Stripe restricted live key', regex: /\brk_live_[A-Za-z0-9]{8,}/g },
57
+ { id: 'stripe_restricted_test', label: 'Stripe restricted test key', regex: /\brk_test_[A-Za-z0-9]{8,}/g },
58
+ { id: 'stripe_webhook_secret', label: 'Stripe webhook signing secret', regex: /\bwhsec_[A-Za-z0-9]{8,}/g },
59
+
60
+ // Anthropic / OpenAI. Run before the legacy `sk-` pattern so the precise label wins.
61
+ { id: 'anthropic_api_key', label: 'Anthropic API key', regex: /\bsk-ant-[A-Za-z0-9_-]{16,}/g },
62
+ { id: 'openai_project_key', label: 'OpenAI project key', regex: /\bsk-proj-[A-Za-z0-9_-]{16,}/g },
63
+ { id: 'openai_api_key', label: 'OpenAI API key', regex: /\bsk-[A-Za-z0-9]{20,}/g },
64
+
65
+ // GitHub tokens.
66
+ { id: 'github_pat', label: 'GitHub personal access token', regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{16,}/g },
67
+ { id: 'github_fine_grained_pat', label: 'GitHub fine-grained token', regex: /\bgithub_pat_\w{20,}/g },
68
+
69
+ // Slack tokens.
70
+ { id: 'slack_token', label: 'Slack token', regex: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g },
71
+
72
+ // Google API key.
73
+ { id: 'google_api_key', label: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}/g },
74
+
75
+ // AWS access key id (long-lived AKIA + temporary ASIA).
76
+ { id: 'aws_access_key', label: 'AWS access key id', regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
77
+
78
+ // JSON Web Tokens.
79
+ {
80
+ id: 'jwt',
81
+ label: 'JSON Web Token',
82
+ regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9._-]{8,}\.[A-Za-z0-9._-]{8,}/g,
83
+ },
84
+
85
+ // `Authorization: Bearer <token>` style. Keep the scheme word, redact the credential.
86
+ {
87
+ id: 'bearer_token',
88
+ label: 'Bearer token',
89
+ regex: /\b(Bearer|Token)\s+[a-z0-9._~+/=-]{16,}/gi,
90
+ replace: (_m, scheme) => `${scheme} ${REDACTED('bearer_token')}`,
91
+ },
92
+
93
+ // Generic `<name> = <value>` / `<name>: "<value>"` assignment with a 16+ char whitespace-free
94
+ // value. A simple matcher finds every assignment; the replace fn redacts only when the name is a
95
+ // credential (SECRET_KEY_NAME) and not a public identifier (PUBLIC_KEY_NAME), so `publishable_key`
96
+ // is preserved. Runs last so provider-specific labels are preferred.
97
+ {
98
+ id: 'secret_assignment',
99
+ label: 'Secret assignment',
100
+ regex: /\b([\w.-]{2,40})(\s*[:=]\s*["']?)([A-Za-z0-9._+=~/-]{16,})(["']?)/g,
101
+ replace: (match, key, pre, _value, post) =>
102
+ (SECRET_KEY_NAME.test(key) && !PUBLIC_KEY_NAME.test(key))
103
+ ? `${key}${pre}${REDACTED('secret_assignment')}${post}`
104
+ : match,
105
+ },
106
+ ];
107
+
108
+ /**
109
+ * Redact secrets from a single string. Returns the input unchanged when it is not a non-empty
110
+ * string. Each pattern's lastIndex is reset before use so the module-level (stateful, `g`-flag)
111
+ * regexes are safe to reuse across calls.
112
+ *
113
+ * @param {string} text
114
+ * @returns {string}
115
+ */
116
+ function redactSecrets(text) {
117
+ if (typeof text !== 'string' || text.length === 0) return text;
118
+ let out = text;
119
+ for (const pattern of SECRET_REDACTION_PATTERNS) {
120
+ pattern.regex.lastIndex = 0;
121
+ const replacement = pattern.replace || REDACTED(pattern.id);
122
+ out = out.replace(pattern.regex, replacement);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Returns true if `text` contains at least one redactable secret.
129
+ * @param {string} text
130
+ * @returns {boolean}
131
+ */
132
+ function containsSecret(text) {
133
+ if (typeof text !== 'string' || text.length === 0) return false;
134
+ return SECRET_REDACTION_PATTERNS.some((pattern) => {
135
+ pattern.regex.lastIndex = 0;
136
+ return pattern.regex.test(text);
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Recursively redact every string contained in `value`. Objects and arrays are deep-copied so
142
+ * the input is never mutated; non-string primitives pass through untouched. Object keys are left
143
+ * as-is (only values are redacted).
144
+ *
145
+ * @param {*} value
146
+ * @returns {*} redacted copy
147
+ */
148
+ function redactSecretsDeep(value) {
149
+ if (typeof value === 'string') return redactSecrets(value);
150
+ if (Array.isArray(value)) return value.map((item) => redactSecretsDeep(item));
151
+ if (value && typeof value === 'object') {
152
+ const out = {};
153
+ for (const [key, val] of Object.entries(value)) {
154
+ out[key] = redactSecretsDeep(val);
155
+ }
156
+ return out;
157
+ }
158
+ return value;
159
+ }
160
+
161
+ module.exports = {
162
+ SECRET_REDACTION_PATTERNS,
163
+ redactSecrets,
164
+ redactSecretsDeep,
165
+ containsSecret,
166
+ };
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Self-Harness Optimizer
4
+ *
5
+ * Implements the continuous self-improving loop from the "Self-Harness" paradigm (arXiv:2606.09498):
6
+ * 1. Weakness Mining: Reads auto-promoted gates from repeated failures (.thumbgate/auto-promoted-gates.json)
7
+ * 2. Harness Proposal: Generates technical rules and injects them directly into system prompts (AGENTS.md & GEMINI.md)
8
+ * 3. Validation: Runs quick test suites to ensure prompt updates do not introduce regressions.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+
15
+ const ROOT = path.join(__dirname, '..');
16
+ const GATES_PATH = path.join(ROOT, '.thumbgate', 'auto-promoted-gates.json');
17
+ const AGENTS_MD_PATH = path.join(ROOT, 'AGENTS.md');
18
+ const GEMINI_MD_PATH = path.join(ROOT, 'GEMINI.md');
19
+
20
+ const RULE_SECTION_HEADER = '## 🛡️ Self-Harness Prevention Rules (Auto-Generated)';
21
+
22
+ function readGates() {
23
+ if (!fs.existsSync(GATES_PATH)) return [];
24
+ try {
25
+ const data = JSON.parse(fs.readFileSync(GATES_PATH, 'utf-8'));
26
+ return Array.isArray(data.gates) ? data.gates : [];
27
+ } catch (err) {
28
+ console.error('Failed to parse auto-promoted-gates.json:', err);
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function generateRuleSection(gates) {
34
+ if (gates.length === 0) {
35
+ return `${RULE_SECTION_HEADER}\n\n- No active auto-generated prevention rules at this time.\n`;
36
+ }
37
+
38
+ const lines = [
39
+ RULE_SECTION_HEADER,
40
+ '',
41
+ '> [!IMPORTANT]',
42
+ '> The following rules were automatically derived from execution failures and thumbs-down feedback.',
43
+ '> You MUST follow these constraints strictly to prevent repeated errors.',
44
+ ''
45
+ ];
46
+
47
+ gates.forEach(gate => {
48
+ // Extract rule message or pattern
49
+ const ruleText = gate.message ? gate.message.replace('Automatically blocked due to repeated failures: ', '') : `NEVER match pattern: ${gate.pattern}`;
50
+ lines.push(`- **Rule [${gate.id}]**: ${ruleText}`);
51
+ });
52
+
53
+ lines.push('');
54
+ return lines.join('\n');
55
+ }
56
+
57
+ function updatePromptFile(filePath, newSection) {
58
+ if (!fs.existsSync(filePath)) {
59
+ console.warn(`Prompt file not found: ${filePath}`);
60
+ return null;
61
+ }
62
+
63
+ const content = fs.readFileSync(filePath, 'utf-8');
64
+ const backup = content;
65
+
66
+ let newContent;
67
+ const index = content.indexOf(RULE_SECTION_HEADER);
68
+ if (index !== -1) {
69
+ // Replace existing section till the end or next major section
70
+ const before = content.substring(0, index);
71
+ const remainder = content.substring(index);
72
+
73
+ // Find next header (e.g. '## ')
74
+ const nextHeaderIndex = remainder.slice(RULE_SECTION_HEADER.length).search(/\n## /);
75
+ if (nextHeaderIndex !== -1) {
76
+ const after = remainder.substring(RULE_SECTION_HEADER.length + nextHeaderIndex + 1);
77
+ newContent = before + newSection + after;
78
+ } else {
79
+ newContent = before + newSection;
80
+ }
81
+ } else {
82
+ // Append to the end of the file
83
+ newContent = content.trim() + '\n\n' + newSection;
84
+ }
85
+
86
+ fs.writeFileSync(filePath, newContent, 'utf-8');
87
+ return backup;
88
+ }
89
+
90
+ function validateAndRollback(backups) {
91
+ console.log('Running quick validation suite...');
92
+ try {
93
+ execSync('npm run verify:quick', { cwd: ROOT, stdio: 'inherit' });
94
+ console.log('✅ Validation succeeded! No regressions detected.');
95
+
96
+ // Auto-commit prompt updates to git if validation succeeds
97
+ try {
98
+ execSync('git add AGENTS.md GEMINI.md', { cwd: ROOT, stdio: 'ignore' });
99
+ execSync('git commit -m "chore(prompt): auto-update self-harness prevention rules [auto-commit]"', { cwd: ROOT, stdio: 'ignore' });
100
+ console.log('✅ Prompt updates auto-committed to git.');
101
+ } catch (gitErr) {
102
+ console.warn('⚠️ Git commit failed or no changes to commit:', gitErr.message);
103
+ }
104
+
105
+ return true;
106
+ } catch (err) {
107
+ console.error('❌ Validation failed! Reverting prompt changes.');
108
+ for (const [filePath, backupContent] of Object.entries(backups)) {
109
+ if (backupContent) {
110
+ fs.writeFileSync(filePath, backupContent, 'utf-8');
111
+ console.log(`Reverted: ${path.basename(filePath)}`);
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function main() {
119
+ console.log('Mining weaknesses from auto-promoted gates...');
120
+ const gates = readGates();
121
+ console.log(`Found ${gates.length} active gates.`);
122
+
123
+ const newSection = generateRuleSection(gates);
124
+ const backups = {};
125
+
126
+ console.log('Updating AGENTS.md...');
127
+ backups[AGENTS_MD_PATH] = updatePromptFile(AGENTS_MD_PATH, newSection);
128
+
129
+ console.log('Updating GEMINI.md...');
130
+ backups[GEMINI_MD_PATH] = updatePromptFile(GEMINI_MD_PATH, newSection);
131
+
132
+ const success = validateAndRollback(backups);
133
+ if (!success) {
134
+ process.exit(1);
135
+ }
136
+ console.log('Self-Harness optimization complete.');
137
+ }
138
+
139
+ if (require.main === module) {
140
+ main();
141
+ }
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('node:fs');
3
+ const os = require('node:os');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+ const { diagnoseFailure } = require('./failure-diagnostics');
7
+ const { appendDiagnosticRecord } = require('./feedback-loop');
8
+
9
+ const PROJECT_ROOT = path.join(__dirname, '..');
10
+ const DEFAULT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
11
+ const DEFAULT_TESTS_TIMEOUT_MS = Number.parseInt(
12
+ process.env.THUMBGATE_SELF_HEAL_TEST_TIMEOUT_MS || '',
13
+ 10,
14
+ ) || 60 * 60_000;
15
+
16
+ const DEFAULT_CHECKS = [
17
+ { name: 'budget_status', command: ['npm', 'run', 'budget:status'], timeoutMs: 60_000 },
18
+ { name: 'tests', command: ['npm', 'test'], timeoutMs: DEFAULT_TESTS_TIMEOUT_MS },
19
+ { name: 'prove_adapters', command: ['npm', 'run', 'prove:adapters'], timeoutMs: 10 * 60_000, useTempProofDir: true },
20
+ { name: 'prove_automation', command: ['npm', 'run', 'prove:automation'], timeoutMs: 10 * 60_000, useTempProofDir: true },
21
+ { name: 'prove_data_pipeline', command: ['npm', 'run', 'prove:data-pipeline'], timeoutMs: 10 * 60_000, useTempProofDir: true },
22
+ { name: 'prove_tessl', command: ['npm', 'run', 'prove:tessl'], timeoutMs: 10 * 60_000, useTempProofDir: true },
23
+ ];
24
+
25
+ function runCommand(command, {
26
+ cwd = PROJECT_ROOT,
27
+ timeoutMs = 5 * 60_000,
28
+ env = process.env,
29
+ maxBufferBytes = DEFAULT_MAX_BUFFER_BYTES,
30
+ } = {}) {
31
+ const [cmd, ...args] = command;
32
+ const started = Date.now();
33
+ const result = spawnSync(cmd, args, {
34
+ cwd,
35
+ env,
36
+ encoding: 'utf-8',
37
+ timeout: timeoutMs,
38
+ maxBuffer: maxBufferBytes,
39
+ shell: false,
40
+ });
41
+
42
+ const durationMs = Date.now() - started;
43
+ const status = Number.isInteger(result.status) ? result.status : 1;
44
+ return {
45
+ exitCode: status,
46
+ durationMs,
47
+ stdout: result.stdout || '',
48
+ stderr: result.stderr || '',
49
+ error: result.error ? result.error.message : null,
50
+ };
51
+ }
52
+
53
+ function createCheckEnvironment(check) {
54
+ const environment = { ...process.env };
55
+ let cleanup = null;
56
+
57
+ if (check.useTempProofDir) {
58
+ const proofDir = fs.mkdtempSync(path.join(os.tmpdir(), `thumbgate-${check.name}-`));
59
+ environment.THUMBGATE_PROOF_DIR = proofDir;
60
+ if (check.name === 'prove_automation') {
61
+ environment.THUMBGATE_AUTOMATION_PROOF_DIR = proofDir;
62
+ }
63
+ cleanup = () => {
64
+ fs.rmSync(proofDir, { recursive: true, force: true });
65
+ };
66
+ }
67
+
68
+ return { env: environment, cleanup };
69
+ }
70
+
71
+ function collectHealthReport({
72
+ checks = DEFAULT_CHECKS,
73
+ runner = runCommand,
74
+ cwd = PROJECT_ROOT,
75
+ persistDiagnostics = false,
76
+ } = {}) {
77
+ const startedAt = new Date();
78
+ const results = checks.map((check) => {
79
+ const { env, cleanup } = createCheckEnvironment(check);
80
+ let run;
81
+ try {
82
+ run = runner(check.command, { cwd, timeoutMs: check.timeoutMs, env });
83
+ } finally {
84
+ if (cleanup) {
85
+ cleanup();
86
+ }
87
+ }
88
+ const diagnosis = run.exitCode === 0
89
+ ? null
90
+ : diagnoseFailure({
91
+ step: check.name,
92
+ context: check.command.join(' '),
93
+ healthCheck: {
94
+ name: check.name,
95
+ exitCode: run.exitCode,
96
+ status: 'unhealthy',
97
+ outputTail: `${run.stdout}\n${run.stderr}`.trim().slice(-2000),
98
+ },
99
+ exitCode: run.exitCode,
100
+ error: run.error,
101
+ output: `${run.stdout}\n${run.stderr}`.trim(),
102
+ });
103
+ const persistedDiagnosis = persistDiagnostics && diagnosis
104
+ ? appendDiagnosticRecord({
105
+ source: 'self_heal_check',
106
+ step: check.name,
107
+ context: check.command.join(' '),
108
+ diagnosis,
109
+ metadata: {
110
+ command: check.command.join(' '),
111
+ },
112
+ })
113
+ : null;
114
+ return {
115
+ name: check.name,
116
+ command: check.command.join(' '),
117
+ status: run.exitCode === 0 ? 'healthy' : 'unhealthy',
118
+ exitCode: run.exitCode,
119
+ durationMs: run.durationMs,
120
+ error: run.error,
121
+ outputTail: `${run.stdout}\n${run.stderr}`.trim().slice(-2000),
122
+ diagnosis,
123
+ persistedDiagnosis,
124
+ };
125
+ });
126
+
127
+ const healthyCount = results.filter((x) => x.status === 'healthy').length;
128
+ const unhealthyCount = results.length - healthyCount;
129
+
130
+ return {
131
+ generatedAt: startedAt.toISOString(),
132
+ durationMs: Date.now() - startedAt.getTime(),
133
+ overall_status: unhealthyCount === 0 ? 'healthy' : 'unhealthy',
134
+ summary: {
135
+ total: results.length,
136
+ healthy: healthyCount,
137
+ unhealthy: unhealthyCount,
138
+ },
139
+ checks: results,
140
+ };
141
+ }
142
+
143
+ function reportToText(report) {
144
+ const lines = [];
145
+ lines.push(`Self-Healing Health Check @ ${report.generatedAt}`);
146
+ lines.push(`Overall: ${report.overall_status.toUpperCase()}`);
147
+ lines.push(`Checks: ${report.summary.healthy}/${report.summary.total} healthy`);
148
+ lines.push('');
149
+
150
+ report.checks.forEach((check) => {
151
+ const icon = check.status === 'healthy' ? '✅' : '❌';
152
+ lines.push(`${icon} ${check.name} (${check.durationMs}ms)`);
153
+ if (check.status !== 'healthy') {
154
+ lines.push(` command: ${check.command}`);
155
+ if (check.error) lines.push(` error: ${check.error}`);
156
+ if (check.diagnosis && check.diagnosis.rootCauseCategory) {
157
+ lines.push(` diagnosis: ${check.diagnosis.rootCauseCategory}`);
158
+ }
159
+ }
160
+ });
161
+
162
+ return `${lines.join('\n')}\n`;
163
+ }
164
+
165
+ function runCli() {
166
+ const args = new Set(process.argv.slice(2));
167
+ const emitJson = args.has('--json');
168
+ const noFail = args.has('--no-fail');
169
+ const report = collectHealthReport({ persistDiagnostics: true });
170
+
171
+ if (emitJson) {
172
+ console.log(JSON.stringify(report, null, 2));
173
+ } else {
174
+ process.stdout.write(reportToText(report));
175
+ }
176
+
177
+ if (!noFail && report.overall_status !== 'healthy') {
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ module.exports = {
183
+ DEFAULT_CHECKS,
184
+ DEFAULT_TESTS_TIMEOUT_MS,
185
+ DEFAULT_MAX_BUFFER_BYTES,
186
+ runCommand,
187
+ collectHealthReport,
188
+ reportToText,
189
+ };
190
+
191
+ if (require.main === module) {
192
+ runCli();
193
+ }
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * self-protection.js — guard the files that configure ThumbGate itself.
5
+ *
6
+ * Added 2026-07-08 after Andy Martin's review: in the shipped `gate-check`
7
+ * path, editing ThumbGate's own hook wiring / gate config was ALLOWED by
8
+ * default (warn-by-default posture only denies under strict enforcement), so
9
+ * an agent could disable the firewall before continuing and the surfaced
10
+ * verdict was a clean ALLOW.
11
+ *
12
+ * This module is the single source of truth for "is this edit touching the
13
+ * firewall's own kill-switches", used by BOTH the shipped `npx thumbgate
14
+ * gate-check` entrypoint and this repo's dogfood PreToolUse hook.
15
+ *
16
+ * Posture threads three constraints:
17
+ * * Andy's ask: don't let the agent quietly rewrite/disable the gate.
18
+ * * CEO warn-by-default (2026-06-04): never hard-block legitimate work.
19
+ * * Self-lockout lesson (2026-07-07): a PreToolUse hook must NEVER deny the
20
+ * tools needed to repair its own config, or the agent is bricked.
21
+ *
22
+ * Resolution:
23
+ * default -> WARN (surfaced + logged, never silent)
24
+ * THUMBGATE_STRICT_ENFORCEMENT=1 -> BLOCK
25
+ * THUMBGATE_ALLOW_SELF_EDIT=1 -> full opt-out (preserves the repair path)
26
+ */
27
+
28
+ const SELF_GOVERNANCE_PATH_PATTERNS = [
29
+ /(?:^|\/)\.claude\/settings(?:\.local)?\.json$/i, // hook wiring / can disable ThumbGate
30
+ /(?:^|\/)\.codex\/config\.toml$/i,
31
+ /(?:^|\/)scripts\/hook-[^/]+\.(?:js|sh)$/i, // the hook scripts themselves
32
+ /(?:^|\/)config\/gates\//i, // gate definitions
33
+ /(?:^|\/)config\/enforcement\.json$/i, // enforcement policy
34
+ /(?:^|\/)config\/mcp-allowlists\.json$/i, // MCP policy surface
35
+ ];
36
+
37
+ const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
38
+
39
+ function isTrueEnv(value) {
40
+ if (value === undefined || value === null) return false;
41
+ const v = String(value).trim().toLowerCase();
42
+ return v === '1' || v === 'true' || v === 'yes' || v === 'on';
43
+ }
44
+
45
+ /**
46
+ * Returns the matched governance file path, or null. Accepts both camelCase
47
+ * (`tool_name`/`tool_input`) and the already-normalized shapes callers use.
48
+ */
49
+ function selfProtectionTarget(toolName, toolInput) {
50
+ if (!EDIT_LIKE_TOOLS.has(toolName)) return null;
51
+ const filePath = String((toolInput && (toolInput.file_path || toolInput.filePath)) || '');
52
+ if (!filePath) return null;
53
+ return SELF_GOVERNANCE_PATH_PATTERNS.some((re) => re.test(filePath)) ? filePath : null;
54
+ }
55
+
56
+ /**
57
+ * Evaluate the self-protection posture for a tool call.
58
+ * @returns {{action:'block'|'warn', target:string, message:string}|null}
59
+ */
60
+ function evaluateSelfProtection(toolName, toolInput, env = process.env) {
61
+ const target = selfProtectionTarget(toolName, toolInput);
62
+ if (!target) return null;
63
+ // Escape hatch: an operator repairing the gate opts out explicitly. Honors the
64
+ // self-lockout lesson — the repair path is never denied.
65
+ if (isTrueEnv(env.THUMBGATE_ALLOW_SELF_EDIT)) return null;
66
+ if (isTrueEnv(env.THUMBGATE_STRICT_ENFORCEMENT)) {
67
+ return {
68
+ action: 'block',
69
+ target,
70
+ message:
71
+ `ThumbGate self-protection: blocked ${toolName} to its own governance file "${target}". `
72
+ + `Editing the firewall's own hook wiring / gate config while strict enforcement is on is denied. `
73
+ + `Set THUMBGATE_ALLOW_SELF_EDIT=1 to make an intentional repair.`,
74
+ };
75
+ }
76
+ return {
77
+ action: 'warn',
78
+ target,
79
+ message:
80
+ `⚠️ ThumbGate self-protection: this ${toolName} targets a governance file that configures the firewall itself `
81
+ + `("${target}"). It is being ALLOWED and LOGGED (warn-by-default). An agent editing this could weaken or disable `
82
+ + `its own guardrails — confirm this is intentional. Set THUMBGATE_STRICT_ENFORCEMENT=1 to hard-block such edits.`,
83
+ };
84
+ }
85
+
86
+ module.exports = {
87
+ SELF_GOVERNANCE_PATH_PATTERNS,
88
+ selfProtectionTarget,
89
+ evaluateSelfProtection,
90
+ };
@@ -562,7 +562,7 @@ const PRETOOLUSE_HOOK_GUIDE_SPEC = Object.freeze({
562
562
  ],
563
563
  [
564
564
  'Does this run locally or call a cloud service?',
565
- 'Local-first. The PreToolUse decision happens in the hook process on your machine in milliseconds — no network round-trip, no cloud dependency, no data leaving the laptop. Optional hosted sync exists for teams that want to share rules across seats.',
565
+ 'Local-first. The PreToolUse decision happens in the hook process on your machine in milliseconds — no network round-trip, no cloud dependency, no data leaving the laptop. Pro adds personal recall, exports, dashboard proof, and managed adapter coverage. Enterprise adds hosted sharing for teams that want to share rules across seats.',
566
566
  ],
567
567
  ],
568
568
  relatedPaths: ['/guides/mcp-tool-governance', '/guides/ai-agent-pre-action-approval-gates', '/guides/ai-coding-agent-zero-trust'],
@@ -1972,7 +1972,7 @@ const PAGE_BLUEPRINTS = [
1972
1972
  faq: [
1973
1973
  {
1974
1974
  question: 'Why pay $19/mo for ThumbGate Pro when disler hooks are free?',
1975
- answer: 'Free disler hooks are static patterns you maintain per machine — re-copying them when they change, debugging false positives alone, and re-applying them in every new project. ThumbGate Pro adds the learning loop (thumbs-down → cross-session prevention rule), the dashboard, hosted sync across machines, and adapter maintenance across the weekly breaking-change cycle of Claude Code, Cursor, and Cline. Free disler is the right answer if you only ever work in one project on one machine and never want to learn from past mistakes. Pro is the right answer when those assumptions stop holding.',
1975
+ answer: 'Free disler hooks are static patterns you maintain per machine — re-copying them when they change, debugging false positives alone, and re-applying them in every new project. ThumbGate Pro adds the learning loop (repeated thumbs-downs → cross-session prevention rule), the personal dashboard, lesson recall/search, DPO export, and adapter maintenance across the weekly breaking-change cycle of Claude Code, Cursor, and Cline. Free disler is the right answer if you only ever work in one project on one machine and never want to learn from past mistakes. Pro is the right answer when those assumptions stop holding; Enterprise adds shared hosted lessons across seats.',
1976
1976
  },
1977
1977
  {
1978
1978
  question: 'Is ThumbGate just a packaged version of disler/claude-code-hooks-mastery?',