thumbgate 1.30.0 → 1.34.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/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +105 -10
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +168 -31
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/claim-verifiers.example.json +42 -0
- package/config/gates/claim-verifiers.json +25 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +233 -206
- package/config/model-tiers.json +7 -2
- package/glama.json +6 -0
- package/hooks/hooks.json +1 -1
- package/package.json +69 -12
- package/public/assets/diagrams/before-after.svg +17 -16
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +19 -13
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/evaluations.html +1 -1
- package/public/index.html +142 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/agent-readiness.js +110 -0
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/auto-wire-hooks.js +20 -8
- package/scripts/cli-schema.js +14 -0
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +356 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-loop.js +115 -7
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/feedback-schema.js +3 -0
- package/scripts/file-ledger-lock.js +130 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/financial-control-plane.js +1514 -0
- package/scripts/gates-engine.js +202 -7
- package/scripts/gemini-embedding-policy.js +1 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +15 -3
- package/scripts/hook-stop-anti-claim.js +63 -3
- package/scripts/human-escalation.js +353 -41
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +305 -29
- package/scripts/lesson-search.js +22 -8
- package/scripts/llm-client.js +304 -15
- package/scripts/model-tier-router.js +593 -0
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/provider-action-normalizer.js +11 -4
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-registry.js +141 -5
- package/scripts/universal-claim-evaluator.js +767 -0
- package/scripts/vector-store.js +154 -17
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/scripts/workflow-sentinel.js +77 -11
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +196 -13
package/scripts/gates-engine.js
CHANGED
|
@@ -16,6 +16,13 @@ const {
|
|
|
16
16
|
const {
|
|
17
17
|
evaluateWorkflowSentinel,
|
|
18
18
|
} = require('./workflow-sentinel');
|
|
19
|
+
const {
|
|
20
|
+
evaluateFinancialControl,
|
|
21
|
+
} = require('./financial-control-plane');
|
|
22
|
+
const {
|
|
23
|
+
buildCostControl,
|
|
24
|
+
normalizeProviderAction,
|
|
25
|
+
} = require('./provider-action-normalizer');
|
|
19
26
|
const {
|
|
20
27
|
recordDecisionEvaluation,
|
|
21
28
|
recordDecisionOutcome,
|
|
@@ -1236,11 +1243,28 @@ const COMMAND_WRAPPERS = new Set([
|
|
|
1236
1243
|
]);
|
|
1237
1244
|
const ENV_ASSIGNMENT_PREFIX = /^[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)\s+/;
|
|
1238
1245
|
const WRAPPER_HEAD = /^([A-Za-z_][\w.-]*)\s+/;
|
|
1246
|
+
const LITERAL_COMMAND_SUBSTITUTION_HEADS = [
|
|
1247
|
+
/^\$\(\s*printf\s+(?:(?:['"]?%s['"]?)\s+)?(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
|
|
1248
|
+
/^\$\(\s*echo\s+(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
|
|
1249
|
+
/^\$\(\s*(?:command\s+-v|which)\s+(['"]?)([A-Za-z_][\w.-]*)\1\s*\)\s+/,
|
|
1250
|
+
];
|
|
1251
|
+
|
|
1252
|
+
function canonicalizeLiteralCommandSubstitutionHead(segment) {
|
|
1253
|
+
for (const pattern of LITERAL_COMMAND_SUBSTITUTION_HEADS) {
|
|
1254
|
+
const match = String(segment || '').match(pattern);
|
|
1255
|
+
if (match) return `${match[2]} ${String(segment).slice(match[0].length)}`;
|
|
1256
|
+
}
|
|
1257
|
+
return segment;
|
|
1258
|
+
}
|
|
1239
1259
|
|
|
1240
1260
|
function canonicalizeSegmentHead(segment) {
|
|
1241
1261
|
let text = String(segment || '').trim();
|
|
1242
1262
|
for (let i = 0; i < 12; i += 1) {
|
|
1243
1263
|
const before = text;
|
|
1264
|
+
// Resolve only literal, side-effect-free command-position substitutions.
|
|
1265
|
+
// Never execute or guess arbitrary shell; this closes common `$(printf git)`
|
|
1266
|
+
// and `$(command -v git)` spellings while keeping matching deterministic.
|
|
1267
|
+
text = canonicalizeLiteralCommandSubstitutionHead(text);
|
|
1244
1268
|
text = text.replace(ENV_ASSIGNMENT_PREFIX, '');
|
|
1245
1269
|
const wrapper = text.match(WRAPPER_HEAD);
|
|
1246
1270
|
if (wrapper && COMMAND_WRAPPERS.has(wrapper[1].toLowerCase())) {
|
|
@@ -1919,6 +1943,43 @@ function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
|
1919
1943
|
return result;
|
|
1920
1944
|
}
|
|
1921
1945
|
|
|
1946
|
+
/**
|
|
1947
|
+
* Resolve catastrophic declarative gates before the ordinary first-match loop.
|
|
1948
|
+
*
|
|
1949
|
+
* Config order is useful for normal policy routing, but it must not let a broad
|
|
1950
|
+
* rule mask a narrower irreversible-action rule. For example, the generic
|
|
1951
|
+
* `push-without-thread-check` gate also matches `git push --force`; selecting it
|
|
1952
|
+
* first allowed the free-tier daily cap to downgrade the action to a warning
|
|
1953
|
+
* before the exempt `force-push` gate was ever evaluated.
|
|
1954
|
+
*
|
|
1955
|
+
* Catastrophic gates are deliberately limited to the audited allowlist above.
|
|
1956
|
+
* Metric-backed gates are excluded because their condition is asynchronous and
|
|
1957
|
+
* none of the catastrophic command boundaries may depend on a remote metric.
|
|
1958
|
+
*/
|
|
1959
|
+
function evaluateCatastrophicDeclarativeGate(config, constraints, toolName, toolInput) {
|
|
1960
|
+
if (!config || !Array.isArray(config.gates)) return null;
|
|
1961
|
+
|
|
1962
|
+
for (const gate of config.gates) {
|
|
1963
|
+
if (!CATASTROPHIC_DECLARATIVE_GATE_IDS.has(gate.id)) continue;
|
|
1964
|
+
if (gate.action !== 'block' || gate.metrics) continue;
|
|
1965
|
+
|
|
1966
|
+
const matchDetails = matchGate(gate, toolName, toolInput);
|
|
1967
|
+
if (!matchDetails.matched) continue;
|
|
1968
|
+
if (gate.when && !checkWhenClause(gate.when, constraints)) continue;
|
|
1969
|
+
if (gate.unless && isConditionSatisfied(gate.unless)) continue;
|
|
1970
|
+
|
|
1971
|
+
return {
|
|
1972
|
+
decision: 'deny',
|
|
1973
|
+
gate: gate.id,
|
|
1974
|
+
message: buildGateMessage(gate, matchDetails),
|
|
1975
|
+
severity: gate.severity,
|
|
1976
|
+
reasoning: buildReasoning(gate, toolName, toolInput, matchDetails),
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1922
1983
|
function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
1923
1984
|
if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
|
|
1924
1985
|
if (toolName !== 'Bash') return false;
|
|
@@ -2759,6 +2820,16 @@ async function evaluateGatesAsyncInner(toolName, toolInput, configPath) {
|
|
|
2759
2820
|
return boostedRiskGuard;
|
|
2760
2821
|
}
|
|
2761
2822
|
|
|
2823
|
+
const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
|
|
2824
|
+
config,
|
|
2825
|
+
constraints,
|
|
2826
|
+
toolName,
|
|
2827
|
+
toolInput,
|
|
2828
|
+
);
|
|
2829
|
+
if (catastrophicDeclarativeGate) {
|
|
2830
|
+
return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2762
2833
|
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
2763
2834
|
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
2764
2835
|
// higher-priority deny/approve gates in established workflows.
|
|
@@ -3005,6 +3076,16 @@ function evaluateGatesInner(toolName, toolInput, configPath) {
|
|
|
3005
3076
|
return boostedRiskGuard;
|
|
3006
3077
|
}
|
|
3007
3078
|
|
|
3079
|
+
const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
|
|
3080
|
+
config,
|
|
3081
|
+
constraints,
|
|
3082
|
+
toolName,
|
|
3083
|
+
toolInput,
|
|
3084
|
+
);
|
|
3085
|
+
if (catastrophicDeclarativeGate) {
|
|
3086
|
+
return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3008
3089
|
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
3009
3090
|
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
3010
3091
|
// higher-priority deny/approve gates in established workflows.
|
|
@@ -3286,7 +3367,7 @@ function evaluateSecretGuard(input = {}) {
|
|
|
3286
3367
|
return result;
|
|
3287
3368
|
}
|
|
3288
3369
|
|
|
3289
|
-
function evaluateUnconditionalHardFloor(input = {}) {
|
|
3370
|
+
function evaluateUnconditionalHardFloor(input = {}, options = {}) {
|
|
3290
3371
|
const secretGuard = evaluateSecretGuard(input);
|
|
3291
3372
|
if (secretGuard) return { hardFloor: secretGuard, securityScan: null };
|
|
3292
3373
|
|
|
@@ -3295,14 +3376,81 @@ function evaluateUnconditionalHardFloor(input = {}) {
|
|
|
3295
3376
|
return { hardFloor: securityScan, securityScan };
|
|
3296
3377
|
}
|
|
3297
3378
|
|
|
3379
|
+
const financialHardFloor = evaluateFinancialHardFloor(input, false, options);
|
|
3380
|
+
if (financialHardFloor) return { hardFloor: financialHardFloor, securityScan };
|
|
3381
|
+
|
|
3298
3382
|
return {
|
|
3299
3383
|
hardFloor: evaluateSelfProtectHardFloor(input),
|
|
3300
3384
|
securityScan,
|
|
3301
3385
|
};
|
|
3302
3386
|
}
|
|
3303
3387
|
|
|
3304
|
-
function
|
|
3305
|
-
const
|
|
3388
|
+
function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
|
|
3389
|
+
const toolName = input.tool_name || input.toolName || 'unknown';
|
|
3390
|
+
const toolInput = input.tool_input && typeof input.tool_input === 'object'
|
|
3391
|
+
? input.tool_input
|
|
3392
|
+
: {};
|
|
3393
|
+
const normalizedAction = normalizeProviderAction({
|
|
3394
|
+
toolName,
|
|
3395
|
+
toolInput,
|
|
3396
|
+
usage: input.usage || toolInput.usage,
|
|
3397
|
+
costUsd: input.costUsd ?? toolInput.costUsd,
|
|
3398
|
+
budget: input.budget || toolInput.budget,
|
|
3399
|
+
});
|
|
3400
|
+
const costControl = buildCostControl(
|
|
3401
|
+
normalizedAction,
|
|
3402
|
+
input.budget || toolInput.budget || {}
|
|
3403
|
+
);
|
|
3404
|
+
const financialControl = evaluateFinancialControl({
|
|
3405
|
+
toolName,
|
|
3406
|
+
toolInput,
|
|
3407
|
+
actionProfile: {
|
|
3408
|
+
economicAction: undefined,
|
|
3409
|
+
},
|
|
3410
|
+
costControl,
|
|
3411
|
+
}, { ...options, consumeReservation });
|
|
3412
|
+
if (financialControl.mode === 'block') {
|
|
3413
|
+
const result = {
|
|
3414
|
+
decision: 'deny',
|
|
3415
|
+
gate: 'financial-control',
|
|
3416
|
+
message: financialControl.reasons.join(' '),
|
|
3417
|
+
severity: 'critical',
|
|
3418
|
+
financialControl,
|
|
3419
|
+
reasoning: [
|
|
3420
|
+
'Economic actions default to deny at the pre-tool boundary.',
|
|
3421
|
+
'Learned policy and advisory memories cannot override this deterministic control.',
|
|
3422
|
+
],
|
|
3423
|
+
};
|
|
3424
|
+
recordStat('financial-control', 'block', null, { toolName, toolInput });
|
|
3425
|
+
const auditRecord = recordAuditEvent({
|
|
3426
|
+
toolName,
|
|
3427
|
+
toolInput,
|
|
3428
|
+
decision: 'deny',
|
|
3429
|
+
gateId: 'financial-control',
|
|
3430
|
+
message: result.message,
|
|
3431
|
+
severity: result.severity,
|
|
3432
|
+
source: 'financial-control',
|
|
3433
|
+
});
|
|
3434
|
+
auditToFeedback(auditRecord);
|
|
3435
|
+
return result;
|
|
3436
|
+
}
|
|
3437
|
+
return null;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// Reservations are single-use. They are consumed only after every other gate
|
|
3441
|
+
// has reached its final allow/warn boundary, never during the preliminary hard
|
|
3442
|
+
// floor preview. This prevents a later workflow or learned-risk denial from
|
|
3443
|
+
// burning an approval for an action that did not execute.
|
|
3444
|
+
function finalizeFinancialAuthorization(input = {}, options = {}) {
|
|
3445
|
+
return evaluateFinancialHardFloor(input, true, options);
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function isBlockingDecision(result) {
|
|
3449
|
+
return result?.decision === 'deny' || result?.decision === 'approve';
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
function runHardFloor(input, options = {}) {
|
|
3453
|
+
const { hardFloor } = evaluateUnconditionalHardFloor(input, options);
|
|
3306
3454
|
return hardFloor ? formatOutput(hardFloor) : null;
|
|
3307
3455
|
}
|
|
3308
3456
|
|
|
@@ -3376,7 +3524,7 @@ function formatOutput(result, behavioralContext) {
|
|
|
3376
3524
|
if (result.decision === 'deny') {
|
|
3377
3525
|
const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
|
|
3378
3526
|
const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
|
|
3379
|
-
const proCta = buildBlockActionProCta() || '';
|
|
3527
|
+
const proCta = result.gate === 'financial-control' ? '' : (buildBlockActionProCta() || '');
|
|
3380
3528
|
return JSON.stringify({
|
|
3381
3529
|
hookSpecificOutput: {
|
|
3382
3530
|
hookEventName: 'PreToolUse',
|
|
@@ -3690,10 +3838,18 @@ async function runAsync(input) {
|
|
|
3690
3838
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
3691
3839
|
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
3692
3840
|
}
|
|
3841
|
+
|
|
3842
|
+
const posturedResult = applyEnforcementPosture(result);
|
|
3843
|
+
if (isBlockingDecision(posturedResult)) {
|
|
3844
|
+
return formatOutput(posturedResult);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
const financialAuthorization = finalizeFinancialAuthorization(input);
|
|
3848
|
+
if (financialAuthorization) return formatOutput(financialAuthorization);
|
|
3693
3849
|
|
|
3694
3850
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
3695
3851
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
3696
|
-
return formatOutput(
|
|
3852
|
+
return formatOutput(posturedResult, combinedContext);
|
|
3697
3853
|
|
|
3698
3854
|
}
|
|
3699
3855
|
|
|
@@ -3729,10 +3885,18 @@ function run(input) {
|
|
|
3729
3885
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
3730
3886
|
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
3731
3887
|
}
|
|
3888
|
+
|
|
3889
|
+
const posturedResult = applyEnforcementPosture(result);
|
|
3890
|
+
if (isBlockingDecision(posturedResult)) {
|
|
3891
|
+
return formatOutput(posturedResult);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
const financialAuthorization = finalizeFinancialAuthorization(input);
|
|
3895
|
+
if (financialAuthorization) return formatOutput(financialAuthorization);
|
|
3732
3896
|
|
|
3733
3897
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
3734
3898
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
3735
|
-
return formatOutput(
|
|
3899
|
+
return formatOutput(posturedResult, combinedContext);
|
|
3736
3900
|
|
|
3737
3901
|
}
|
|
3738
3902
|
|
|
@@ -4020,10 +4184,40 @@ function verifyClaimEvidence(claimText, options = {}) {
|
|
|
4020
4184
|
});
|
|
4021
4185
|
}
|
|
4022
4186
|
|
|
4187
|
+
// Universal factual claims (row counts, file lines/bytes/existence, version values)
|
|
4188
|
+
// recheck the configured source of truth. Fail-closed on mismatch or missing verifier.
|
|
4189
|
+
let universal = null;
|
|
4190
|
+
if (options.skipUniversal !== true) {
|
|
4191
|
+
try {
|
|
4192
|
+
const {
|
|
4193
|
+
evaluateUniversalClaimsAsGateChecks,
|
|
4194
|
+
} = require('./universal-claim-evaluator');
|
|
4195
|
+
universal = evaluateUniversalClaimsAsGateChecks(normalizedClaimText, {
|
|
4196
|
+
cwd: options.cwd,
|
|
4197
|
+
verifiers: options.verifiers,
|
|
4198
|
+
configPath: options.claimVerifiersPath,
|
|
4199
|
+
config: options.claimVerifiers,
|
|
4200
|
+
feedbackDir: options.feedbackDir,
|
|
4201
|
+
failUnconfigured: options.failUnconfigured,
|
|
4202
|
+
});
|
|
4203
|
+
for (const check of universal.checks) {
|
|
4204
|
+
checks.push(check);
|
|
4205
|
+
}
|
|
4206
|
+
} catch (error) {
|
|
4207
|
+
checks.push({
|
|
4208
|
+
claim: 'universal_evaluator',
|
|
4209
|
+
passed: false,
|
|
4210
|
+
missing: ['universal_claim_evaluator'],
|
|
4211
|
+
message: `Universal claim evaluator failed closed: ${error && error.message ? error.message : 'unknown error'}`,
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4023
4216
|
return {
|
|
4024
|
-
verified: checks.every((check) => check.passed),
|
|
4217
|
+
verified: checks.length === 0 ? true : checks.every((check) => check.passed),
|
|
4025
4218
|
checks,
|
|
4026
4219
|
goalContract,
|
|
4220
|
+
universal,
|
|
4027
4221
|
};
|
|
4028
4222
|
}
|
|
4029
4223
|
|
|
@@ -4071,6 +4265,7 @@ module.exports = {
|
|
|
4071
4265
|
isAutonomousRun,
|
|
4072
4266
|
computeExecutableHash,
|
|
4073
4267
|
formatOutput,
|
|
4268
|
+
finalizeFinancialAuthorization,
|
|
4074
4269
|
isApprovalGatesEnabled,
|
|
4075
4270
|
runHardFloor,
|
|
4076
4271
|
run,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Harness tool names -> canonical gate-engine names.
|
|
5
|
+
*
|
|
6
|
+
* Policy gates match Claude Code's vocabulary (Bash, Write, Edit, MultiEdit). Other
|
|
7
|
+
* harnesses use their own: Cline's .clinerules instructs the agent to gate-check
|
|
8
|
+
* `execute_command`, `write_to_file`, `replace_in_file` and `browser_action`.
|
|
9
|
+
* Forwarding those through unchanged meant every gate silently missed — a strict
|
|
10
|
+
* `gate_check` on `{tool_name: "execute_command", command: "rm -rf /"}` returned ALLOW,
|
|
11
|
+
* which is precisely the scenario the adapter advertises as protected.
|
|
12
|
+
*
|
|
13
|
+
* Argument keys differ too: Cline writes use `path`, Claude Code uses `file_path`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const TOOL_NAME_MAP = new Map(Object.entries({
|
|
17
|
+
// Cline / Roo Code
|
|
18
|
+
execute_command: 'Bash',
|
|
19
|
+
write_to_file: 'Write',
|
|
20
|
+
replace_in_file: 'Edit',
|
|
21
|
+
apply_diff: 'Edit',
|
|
22
|
+
insert_content: 'Edit',
|
|
23
|
+
search_and_replace: 'Edit',
|
|
24
|
+
// OpenCode / generic MCP
|
|
25
|
+
bash: 'Bash',
|
|
26
|
+
shell: 'Bash',
|
|
27
|
+
run_command: 'Bash',
|
|
28
|
+
edit_file: 'Edit',
|
|
29
|
+
write_file: 'Write',
|
|
30
|
+
create_file: 'Write',
|
|
31
|
+
str_replace_editor: 'Edit',
|
|
32
|
+
// Cursor
|
|
33
|
+
run_terminal_cmd: 'Bash',
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const ARG_KEY_MAP = new Map(Object.entries({
|
|
37
|
+
path: 'file_path',
|
|
38
|
+
filePath: 'file_path',
|
|
39
|
+
target_file: 'file_path',
|
|
40
|
+
cmd: 'command',
|
|
41
|
+
shell_command: 'command',
|
|
42
|
+
diff: 'new_string',
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
function canonicalizeToolName(toolName) {
|
|
46
|
+
const raw = String(toolName || '').trim();
|
|
47
|
+
if (!raw) return raw;
|
|
48
|
+
return TOOL_NAME_MAP.get(raw) || TOOL_NAME_MAP.get(raw.toLowerCase()) || raw;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function canonicalizeToolInput(toolInput) {
|
|
52
|
+
if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput)) return toolInput || {};
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [key, value] of Object.entries(toolInput)) {
|
|
55
|
+
const canonicalKey = ARG_KEY_MAP.get(key) || key;
|
|
56
|
+
// Never let a mapped key clobber one the caller already supplied canonically.
|
|
57
|
+
if (canonicalKey in out) continue;
|
|
58
|
+
out[canonicalKey] = value;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function canonicalizeToolCall(toolName, toolInput) {
|
|
64
|
+
return {
|
|
65
|
+
toolName: canonicalizeToolName(toolName),
|
|
66
|
+
toolInput: canonicalizeToolInput(toolInput),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { canonicalizeToolCall, canonicalizeToolName, canonicalizeToolInput, TOOL_NAME_MAP, ARG_KEY_MAP };
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -51,13 +51,20 @@ function resolveCliCommand(subcommand) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function resolveCodexCliCommand(subcommand) {
|
|
54
|
+
// Codex hooks live in user-global config. Pinning them to a disposable source
|
|
55
|
+
// worktree breaks every hook as soon as normal post-merge cleanup removes it.
|
|
56
|
+
// The stable @latest launcher stays offline at configuration time and survives
|
|
57
|
+
// cleanup. Checkout-pinned hooks remain available as an explicit dev override.
|
|
58
|
+
if (isSourceCheckout(PKG_ROOT) && process.env.THUMBGATE_CODEX_USE_SOURCE_RUNTIME === '1') {
|
|
59
|
+
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
60
|
+
}
|
|
61
|
+
if (isSourceCheckout(PKG_ROOT)) {
|
|
62
|
+
return publishedCliShellCommand('latest', [subcommand]);
|
|
63
|
+
}
|
|
54
64
|
const version = packageVersion();
|
|
55
65
|
if (publishedHookCommandsAvailable(version)) {
|
|
56
66
|
return publishedCliShellCommand('latest', [subcommand]);
|
|
57
67
|
}
|
|
58
|
-
if (isSourceCheckout(PKG_ROOT)) {
|
|
59
|
-
return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))} ${subcommand}`;
|
|
60
|
-
}
|
|
61
68
|
return publishedCliShellCommand('latest', [subcommand]);
|
|
62
69
|
}
|
|
63
70
|
|
|
@@ -85,6 +92,10 @@ function cacheUpdateHookCommand() {
|
|
|
85
92
|
return buildPortableHookCommand('cache-update');
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
function claimStopHookCommand() {
|
|
96
|
+
return buildPortableHookCommand('claim-stop-check');
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
function statuslineCommand() {
|
|
89
100
|
return buildPortableHookCommand('statusline-render');
|
|
90
101
|
}
|
|
@@ -113,6 +124,7 @@ module.exports = {
|
|
|
113
124
|
buildPortableHookCommand,
|
|
114
125
|
buildCodexPortableHookCommand,
|
|
115
126
|
cacheUpdateHookCommand,
|
|
127
|
+
claimStopHookCommand,
|
|
116
128
|
codexCacheUpdateHookCommand,
|
|
117
129
|
codexPreToolHookCommand,
|
|
118
130
|
codexSessionStartHookCommand,
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const fs = require('node:fs');
|
|
32
|
+
const {
|
|
33
|
+
evaluateUniversalClaims,
|
|
34
|
+
} = require('./universal-claim-evaluator');
|
|
32
35
|
|
|
33
36
|
// Lie-phrase patterns. These match common "claim of completion" wording
|
|
34
37
|
// the agent emits without verification. Word-boundary anchored to avoid
|
|
@@ -233,6 +236,45 @@ function readStdinSync() {
|
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
function factualClaimBlock(text, options = {}) {
|
|
240
|
+
try {
|
|
241
|
+
const result = evaluateUniversalClaims(text, {
|
|
242
|
+
cwd: options.cwd,
|
|
243
|
+
configPath: options.configPath,
|
|
244
|
+
feedbackDir: options.feedbackDir,
|
|
245
|
+
failUnconfigured: true,
|
|
246
|
+
});
|
|
247
|
+
if (result.parsedCount === 0) return null;
|
|
248
|
+
if (result.verified) return null;
|
|
249
|
+
const failures = result.checks.filter((check) => !check.passed);
|
|
250
|
+
return {
|
|
251
|
+
decision: 'block',
|
|
252
|
+
reason: `ThumbGate factual-claim gate: ${failures.map((check) => check.message).join('; ')}. Recheck the configured source of truth and restate the observed value, or retract the claim.`,
|
|
253
|
+
verification: {
|
|
254
|
+
verified: false,
|
|
255
|
+
parsedCount: result.parsedCount,
|
|
256
|
+
failures: failures.map((check) => ({
|
|
257
|
+
status: check.status,
|
|
258
|
+
kind: check.kind,
|
|
259
|
+
verifierId: check.verifierId || null,
|
|
260
|
+
expected: check.expected,
|
|
261
|
+
actual: Object.hasOwn(check, 'actual') ? check.actual : null,
|
|
262
|
+
})),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return {
|
|
267
|
+
decision: 'block',
|
|
268
|
+
reason: `ThumbGate factual-claim gate failed closed: ${error.message}`,
|
|
269
|
+
verification: {
|
|
270
|
+
verified: false,
|
|
271
|
+
parsedCount: null,
|
|
272
|
+
failures: [{ status: 'evaluator_error' }],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
236
278
|
function main() {
|
|
237
279
|
const raw = readStdinSync();
|
|
238
280
|
let payload = {};
|
|
@@ -242,12 +284,28 @@ function main() {
|
|
|
242
284
|
payload = {};
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
// Claude Code invokes a blocked Stop hook once more with this marker so the
|
|
288
|
+
// agent can correct its response. Re-blocking the same payload would hit the
|
|
289
|
+
// host's block cap instead of giving the agent a correction turn.
|
|
290
|
+
if (payload.stop_hook_active === true) return;
|
|
291
|
+
|
|
245
292
|
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
246
293
|
const message = readLastAssistantTurn(transcriptPath);
|
|
247
|
-
|
|
294
|
+
const text = message
|
|
295
|
+
? extractText(message)
|
|
296
|
+
: String(payload.last_assistant_message || process.env.CLAUDE_RESPONSE || '');
|
|
297
|
+
if (!text.trim()) return; // no assistant response visible; nothing to check
|
|
298
|
+
|
|
299
|
+
const toolUseSummary = message ? extractToolUseSummary(message) : '';
|
|
300
|
+
const factualBlock = factualClaimBlock(text, {
|
|
301
|
+
cwd: payload.cwd || payload.workspace_root || process.cwd(),
|
|
302
|
+
configPath: process.env.THUMBGATE_CLAIM_VERIFIERS_PATH,
|
|
303
|
+
});
|
|
304
|
+
if (factualBlock) {
|
|
305
|
+
process.stdout.write(`${JSON.stringify(factualBlock)}\n`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
248
308
|
|
|
249
|
-
const text = extractText(message);
|
|
250
|
-
const toolUseSummary = extractToolUseSummary(message);
|
|
251
309
|
const claim = findClaim(text);
|
|
252
310
|
if (!claim) return; // no completion claim made; silent
|
|
253
311
|
|
|
@@ -298,4 +356,6 @@ module.exports = {
|
|
|
298
356
|
hasProof,
|
|
299
357
|
extractText,
|
|
300
358
|
extractToolUseSummary,
|
|
359
|
+
factualClaimBlock,
|
|
360
|
+
main,
|
|
301
361
|
};
|