thumbgate 1.27.2 → 1.27.3
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 +28 -26
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +14 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +75 -24
- package/package.json +3 -3
- package/public/about.html +162 -0
- package/public/compare.html +2 -2
- package/public/dashboard.html +32 -30
- package/public/guide.html +2 -2
- package/public/index.html +61 -54
- package/public/numbers.html +2 -2
- package/public/pricing.html +23 -36
- package/public/pro.html +3 -3
- package/scripts/commercial-offer.js +10 -2
- package/scripts/dashboard-chat.js +173 -72
- package/scripts/gates-engine.js +43 -6
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/rate-limiter.js +2 -2
- package/scripts/seo-gsd.js +60 -0
- package/scripts/workflow-sentinel.js +111 -68
- package/src/api/server.js +294 -154
- package/.claude-plugin/marketplace.json +0 -85
|
@@ -8,6 +8,7 @@ const DEFAULT_PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
|
|
|
8
8
|
const DEFAULT_OUTPUT_DIR = path.join(__dirname, '..', 'docs', 'marketing');
|
|
9
9
|
const KNOWN_REPOS = Object.freeze({
|
|
10
10
|
'@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
|
|
11
|
+
'@modelcontextprotocol/sdk': 'modelcontextprotocol/typescript-sdk',
|
|
11
12
|
'@google/genai': 'googleapis/js-genai',
|
|
12
13
|
'@huggingface/transformers': 'huggingface/transformers.js',
|
|
13
14
|
'@lancedb/lancedb': 'lancedb/lancedb',
|
|
@@ -23,6 +24,24 @@ const KNOWN_REPOS = Object.freeze({
|
|
|
23
24
|
undici: 'nodejs/undici',
|
|
24
25
|
});
|
|
25
26
|
|
|
27
|
+
// Communities where ThumbGate's buyers live even though they are not npm
|
|
28
|
+
// dependencies. ThumbGate ships an MCP server, so the Model Context Protocol
|
|
29
|
+
// repos are the single highest-ROI ecosystem to contribute to — but the
|
|
30
|
+
// dependency-scan above would never surface them. These are always scouted on
|
|
31
|
+
// the default (no explicit --dependencies) path, de-duped against package.json.
|
|
32
|
+
const STRATEGIC_DEPENDENCIES = Object.freeze([
|
|
33
|
+
'@modelcontextprotocol/sdk', // MCP TypeScript SDK — the protocol ThumbGate implements
|
|
34
|
+
'modelcontextprotocol/servers', // MCP servers ecosystem — where MCP authors (our buyers) collaborate
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Honest, repo-accurate framing for the outreach draft. ThumbGate does not
|
|
38
|
+
// `import` these — it implements the protocol — so the generic "while using X"
|
|
39
|
+
// line would be a false claim. Keep drafts truthful (CEO honesty rule).
|
|
40
|
+
const RELATIONSHIP_OVERRIDES = Object.freeze({
|
|
41
|
+
'@modelcontextprotocol/sdk': 'building ThumbGate as an MCP server against the Model Context Protocol spec',
|
|
42
|
+
'modelcontextprotocol/servers': 'building and testing ThumbGate as an MCP server alongside the reference MCP servers',
|
|
43
|
+
});
|
|
44
|
+
|
|
26
45
|
const BOUNTY_KEYWORDS = [
|
|
27
46
|
'bug bounty',
|
|
28
47
|
'bounty',
|
|
@@ -70,7 +89,10 @@ function dependencyNames(pkg = {}) {
|
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
function repoFromDependency(name) {
|
|
73
|
-
|
|
92
|
+
if (KNOWN_REPOS[name]) return KNOWN_REPOS[name];
|
|
93
|
+
// A strategic identifier may itself be an "owner/repo" slug (not an npm name).
|
|
94
|
+
if (!name.startsWith('@') && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(name)) return name;
|
|
95
|
+
return '';
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
function buildIssueSearchQueries(repo) {
|
|
@@ -89,14 +111,18 @@ function scoreOpportunity(depName, repo, options = {}) {
|
|
|
89
111
|
score += 20;
|
|
90
112
|
reasons.push('known upstream repository');
|
|
91
113
|
}
|
|
92
|
-
if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
|
|
114
|
+
if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
|
|
93
115
|
score += 20;
|
|
94
116
|
reasons.push('high product adjacency for agent tooling');
|
|
95
117
|
}
|
|
96
|
-
if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
|
|
118
|
+
if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
|
|
97
119
|
score += 15;
|
|
98
120
|
reasons.push('large ecosystem visibility');
|
|
99
121
|
}
|
|
122
|
+
if (/modelcontext|mcp/i.test(depName) || /modelcontextprotocol/i.test(repo)) {
|
|
123
|
+
score += 12;
|
|
124
|
+
reasons.push("ThumbGate's own protocol surface — buyers are MCP authors");
|
|
125
|
+
}
|
|
100
126
|
if (options.includeBounties) {
|
|
101
127
|
score += 10;
|
|
102
128
|
reasons.push('bounty search enabled');
|
|
@@ -138,7 +164,7 @@ function buildOpportunity(depName, options = {}) {
|
|
|
138
164
|
'no bounty, security, or maintainer-policy claim without source link',
|
|
139
165
|
],
|
|
140
166
|
outreachDraft: repo
|
|
141
|
-
? `I found this while using ${depName} in ThumbGate. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
|
|
167
|
+
? `I found this while ${RELATIONSHIP_OVERRIDES[depName] || `using ${depName} in ThumbGate`}. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
|
|
142
168
|
: '',
|
|
143
169
|
};
|
|
144
170
|
}
|
|
@@ -149,7 +175,9 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
|
|
|
149
175
|
const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
|
|
150
176
|
const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
|
|
151
177
|
const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
|
|
152
|
-
const deps = explicitDeps.length
|
|
178
|
+
const deps = explicitDeps.length
|
|
179
|
+
? explicitDeps
|
|
180
|
+
: [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
|
|
153
181
|
const opportunities = deps
|
|
154
182
|
.map((dep) => buildOpportunity(dep, { includeBounties }))
|
|
155
183
|
.filter((opportunity) => opportunity.repo)
|
|
@@ -223,6 +251,8 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
|
|
|
223
251
|
|
|
224
252
|
module.exports = {
|
|
225
253
|
KNOWN_REPOS,
|
|
254
|
+
STRATEGIC_DEPENDENCIES,
|
|
255
|
+
RELATIONSHIP_OVERRIDES,
|
|
226
256
|
buildIssueSearchQueries,
|
|
227
257
|
buildOpportunity,
|
|
228
258
|
buildOssPrOpportunityScoutPlan,
|
package/scripts/rate-limiter.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const {
|
|
7
7
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
8
8
|
PRO_PRICE_LABEL,
|
|
9
|
-
|
|
9
|
+
ENTERPRISE_PRICE_LABEL,
|
|
10
10
|
} = require('./commercial-offer');
|
|
11
11
|
|
|
12
12
|
const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
|
|
@@ -31,7 +31,7 @@ const FREE_TIER_LIMITS = {
|
|
|
31
31
|
const FREE_TIER_MAX_GATES = 3; // 3 active prevention rules on free; Pro is unlimited
|
|
32
32
|
const FREE_TIER_DAILY_BLOCKS = 3; // 3 gate blocks/day on free; after limit, deny → warn + upgrade CTA
|
|
33
33
|
|
|
34
|
-
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n
|
|
34
|
+
const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
|
|
35
35
|
|
|
36
36
|
const PAYWALL_MESSAGES = {
|
|
37
37
|
capture_feedback: 'Free tier: 5 captures/day (25 total). Your feedback is stored locally — upgrade to capture unlimited.',
|
package/scripts/seo-gsd.js
CHANGED
|
@@ -446,6 +446,65 @@ function buildZeroTrustGuide() {
|
|
|
446
446
|
});
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
const GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC = Object.freeze({
|
|
450
|
+
slug: 'govern-claude-for-legal-agents',
|
|
451
|
+
meta: {
|
|
452
|
+
query: 'govern claude for legal agents',
|
|
453
|
+
title: 'Govern Claude for Legal Agents | A Gate Before They Act',
|
|
454
|
+
heroTitle: 'Govern Claude for Legal’s 90+ Agents at the Tool Call',
|
|
455
|
+
heroSummary: 'Claude for Legal ships 90+ named agents that review contracts, answer DSARs, and run continuously on document and email streams. Anthropic’s own guidance is that there must be a gate before anything is filed, sent, or relied on. ThumbGate is that gate — it checks each agent action at the tool-call boundary, in your tenant, and logs every decision for the record.',
|
|
456
|
+
},
|
|
457
|
+
takeaways: [
|
|
458
|
+
'Claude for Legal’s agents take real side effects — sending a DSAR response, filing a document, writing to a system of record. ThumbGate gates the action before the side effect runs, not after, on a dashboard.',
|
|
459
|
+
'Intent-agnostic: whether an agent is wrong, prompt-injected, or off-playbook, ThumbGate blocks the same way and records the rule that fired. The risk is not a “rogue” agent — it is an ordinary one acting at volume.',
|
|
460
|
+
'Every gated decision is logged with its source rule — a SIEM-exportable audit trail your ethics, risk, and conflicts owners can query.',
|
|
461
|
+
],
|
|
462
|
+
sections: [
|
|
463
|
+
['paragraphs', 'Why 90+ legal agents need a gate before the side effect', [
|
|
464
|
+
'A firm running Claude for Legal now has dozens of agents acting on ongoing document and email streams — vendor-agreement review, termination review, DSAR responses, claim charts. No one can review every action by hand. The risk is not malice; it is an ordinary agent that sends the wrong response, files against the wrong playbook, or surfaces a privileged document.',
|
|
465
|
+
'Anthropic’s own framing names the control: an explicit gate before anything is filed, sent, or relied on. ThumbGate implements that gate at the tool-call boundary — the moment before the action executes — instead of trusting the agent’s stated intent.',
|
|
466
|
+
]],
|
|
467
|
+
['bullets', 'What ThumbGate gates for legal agents', [
|
|
468
|
+
'The send/file/write action itself — e.g. a DSAR or client response before it leaves, a filing before it goes out, a write to a conflicted matter — held or blocked at the boundary.',
|
|
469
|
+
'Playbook deviations — an action that departs from the firm’s approved workflow is stopped for review rather than executed.',
|
|
470
|
+
'Privileged-document exposure — flagged before an agent surfaces or forwards it.',
|
|
471
|
+
'Continuous runs — one rule set covers every agent and every scheduled run, so coverage scales with agent count, not headcount.',
|
|
472
|
+
]],
|
|
473
|
+
['paragraphs', 'Enforcement in your tenant, with an audit trail', [
|
|
474
|
+
'ThumbGate runs as a pre-action gate in front of agent fulfillment, including a Dialogflow CX webhook gate deployed in your own GCP tenant, so matter content does not leave your boundary. Risk and planning scoring can run on Gemini via Vertex, in-tenant. This is a white-glove design-partner pilot, not a turnkey product purchase.',
|
|
475
|
+
'Every gated detection is logged with the rule that fired and the feedback event that generated it. That decision trail is the evidence a firm needs for malpractice defense and bar-compliance review — queryable, exportable, and tied to a named owner.',
|
|
476
|
+
]],
|
|
477
|
+
['paragraphs', 'ThumbGate complements Claude for Legal — it does not replace it', [
|
|
478
|
+
'Claude for Legal decides what the work is. ThumbGate decides what is allowed to execute. Use both: keep the 90+ agents doing the legal work, and put a gate between each agent and its next side effect. A thumbs-down on a bad action becomes a prevention rule, so the same mistake is blocked across every agent and matter next time.',
|
|
479
|
+
]],
|
|
480
|
+
],
|
|
481
|
+
faq: [
|
|
482
|
+
[
|
|
483
|
+
'Does ThumbGate replace Claude for Legal?',
|
|
484
|
+
'No. Claude for Legal’s agents do the legal work; ThumbGate governs what they are allowed to execute — a gate before anything is filed, sent, or relied on. You run both.',
|
|
485
|
+
],
|
|
486
|
+
[
|
|
487
|
+
'Where does the gate run?',
|
|
488
|
+
'In your tenant. ThumbGate gates agent fulfillment locally or via a Dialogflow CX webhook gate in your own GCP project; matter content does not leave your boundary, and Vertex/Gemini scoring runs in-tenant. It is a white-glove design-partner pilot, not a turnkey purchase.',
|
|
489
|
+
],
|
|
490
|
+
[
|
|
491
|
+
'What proof does a firm get?',
|
|
492
|
+
'Every gated decision is logged with the rule that fired and the feedback that generated it — a SIEM-exportable audit trail for ethics, risk, and conflicts owners.',
|
|
493
|
+
],
|
|
494
|
+
],
|
|
495
|
+
relatedPaths: ['/guides/ai-coding-agent-zero-trust', '/guides/pre-action-checks'],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
function buildGovernClaudeForLegalGuide() {
|
|
499
|
+
return preActionGuide(GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.slug, {
|
|
500
|
+
...GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.meta,
|
|
501
|
+
takeaways: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.takeaways,
|
|
502
|
+
sections: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.sections.map(([kind, heading, entries]) => buildSectionFromSpec(kind, heading, entries)),
|
|
503
|
+
faq: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.faq.map(([question, text]) => answer(question, text)),
|
|
504
|
+
relatedPaths: GOVERN_CLAUDE_FOR_LEGAL_GUIDE_SPEC.relatedPaths,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
449
508
|
const PROXY_POINTER_RAG_GUARDRAILS_SPEC = Object.freeze({
|
|
450
509
|
slug: 'proxy-pointer-rag-guardrails',
|
|
451
510
|
meta: {
|
|
@@ -1589,6 +1648,7 @@ const PAGE_BLUEPRINTS = [
|
|
|
1589
1648
|
},
|
|
1590
1649
|
buildSemanticPseoGuide(),
|
|
1591
1650
|
buildZeroTrustGuide(),
|
|
1651
|
+
buildGovernClaudeForLegalGuide(),
|
|
1592
1652
|
buildProxyPointerRagGuide(),
|
|
1593
1653
|
buildRagPrecisionTuningGuide(),
|
|
1594
1654
|
buildAiEngineeringStackGuide(),
|
|
@@ -64,12 +64,12 @@ function loadJson(filePath) {
|
|
|
64
64
|
function loadGovernanceState() {
|
|
65
65
|
const raw = loadJson(GOVERNANCE_STATE_PATH);
|
|
66
66
|
return {
|
|
67
|
-
taskScope: raw
|
|
68
|
-
protectedApprovals: Array.isArray(raw
|
|
69
|
-
branchGovernance: raw
|
|
67
|
+
taskScope: raw?.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
|
|
68
|
+
protectedApprovals: Array.isArray(raw?.protectedApprovals) ? raw.protectedApprovals : [],
|
|
69
|
+
branchGovernance: raw?.branchGovernance && typeof raw.branchGovernance === 'object'
|
|
70
70
|
? raw.branchGovernance
|
|
71
71
|
: null,
|
|
72
|
-
workflowContract: raw
|
|
72
|
+
workflowContract: raw?.workflowContract && typeof raw.workflowContract === 'object'
|
|
73
73
|
? raw.workflowContract
|
|
74
74
|
: null,
|
|
75
75
|
};
|
|
@@ -391,9 +391,18 @@ function commandMatchesPattern(command, pattern) {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
const COMPLETION_ACTION_PATTERNS = [
|
|
395
|
+
/\bgit\s+(?:commit|push)\b/i,
|
|
396
|
+
/\bgh\s+pr\s+(?:create|merge)\b/i,
|
|
397
|
+
/\bgh\s+release\s+create\b/i,
|
|
398
|
+
/\bnpm\s+publish\b/i,
|
|
399
|
+
/\byarn\s+publish\b/i,
|
|
400
|
+
/\bpnpm\s+publish\b/i,
|
|
401
|
+
];
|
|
402
|
+
|
|
394
403
|
function isCompletionLikeAction(command) {
|
|
395
|
-
|
|
396
|
-
|
|
404
|
+
const text = String(command || '');
|
|
405
|
+
return COMPLETION_ACTION_PATTERNS.some((pattern) => pattern.test(text));
|
|
397
406
|
}
|
|
398
407
|
|
|
399
408
|
function collectEvidenceLabels(toolInput = {}, options = {}) {
|
|
@@ -466,11 +475,17 @@ function evaluateWorkflowContract(contractInput, context = {}) {
|
|
|
466
475
|
}
|
|
467
476
|
|
|
468
477
|
const hasBlock = violations.some((violation) => violation.severity === 'block');
|
|
478
|
+
let mode = 'allow';
|
|
479
|
+
if (hasBlock) {
|
|
480
|
+
mode = 'block';
|
|
481
|
+
} else if (violations.length > 0) {
|
|
482
|
+
mode = 'warn';
|
|
483
|
+
}
|
|
469
484
|
return {
|
|
470
485
|
active: true,
|
|
471
486
|
contract,
|
|
472
487
|
violations,
|
|
473
|
-
mode
|
|
488
|
+
mode,
|
|
474
489
|
};
|
|
475
490
|
}
|
|
476
491
|
|
|
@@ -704,18 +719,18 @@ function scoreRisk({
|
|
|
704
719
|
);
|
|
705
720
|
}
|
|
706
721
|
}
|
|
707
|
-
if (workflowContract
|
|
722
|
+
if (workflowContract?.active && workflowContract.violations.length > 0) {
|
|
708
723
|
for (const violation of workflowContract.violations) {
|
|
709
724
|
addDriver(
|
|
710
725
|
drivers,
|
|
711
726
|
`workflow_contract_${violation.code}`,
|
|
712
727
|
violation.severity === 'block' ? 0.38 : 0.18,
|
|
713
728
|
violation.message,
|
|
714
|
-
{ workflowId: workflowContract.contract
|
|
729
|
+
{ workflowId: workflowContract.contract?.workflowId }
|
|
715
730
|
);
|
|
716
731
|
}
|
|
717
732
|
}
|
|
718
|
-
if (memoryGuard
|
|
733
|
+
if (memoryGuard?.mode && memoryGuard.mode !== 'allow') {
|
|
719
734
|
addDriver(
|
|
720
735
|
drivers,
|
|
721
736
|
'memory_recurrence',
|
|
@@ -724,7 +739,7 @@ function scoreRisk({
|
|
|
724
739
|
{ mode: memoryGuard.mode }
|
|
725
740
|
);
|
|
726
741
|
}
|
|
727
|
-
if (learnedPolicy
|
|
742
|
+
if (learnedPolicy?.enabled && learnedPolicy.prediction) {
|
|
728
743
|
const confidence = learnedPolicy.prediction.confidence || 0;
|
|
729
744
|
const label = learnedPolicy.prediction.label;
|
|
730
745
|
if (label === 'deny' && confidence >= 0.6) {
|
|
@@ -809,19 +824,17 @@ function buildEvidence({
|
|
|
809
824
|
evidence.push(`Workflow control ${workflowControl.mode}: ${workflowControl.reasons.join(' ')}`);
|
|
810
825
|
}
|
|
811
826
|
}
|
|
812
|
-
if (workflowContract
|
|
813
|
-
const workflowId = workflowContract.contract
|
|
814
|
-
? workflowContract.contract.workflowId
|
|
815
|
-
: 'unnamed';
|
|
827
|
+
if (workflowContract?.active) {
|
|
828
|
+
const workflowId = workflowContract.contract?.workflowId || 'unnamed';
|
|
816
829
|
evidence.push(`Workflow contract active: ${workflowId}.`);
|
|
817
830
|
for (const violation of workflowContract.violations.slice(0, 3)) {
|
|
818
831
|
evidence.push(`Workflow contract ${violation.severity}: ${violation.message}`);
|
|
819
832
|
}
|
|
820
833
|
}
|
|
821
|
-
if (actionProfile
|
|
834
|
+
if (actionProfile?.backgroundAgent) {
|
|
822
835
|
evidence.push('Background or scheduled agent context detected for this action.');
|
|
823
836
|
}
|
|
824
|
-
if (actionProfile
|
|
837
|
+
if (actionProfile?.economicAction) {
|
|
825
838
|
evidence.push('Economic action keywords detected (billing, refunds, payouts, invoices, or subscriptions).');
|
|
826
839
|
}
|
|
827
840
|
if (actionProfile && actionProfile.customerSystemAction) {
|
|
@@ -1031,7 +1044,7 @@ function buildRemediations({
|
|
|
1031
1044
|
'Isolated execution limits host damage when a high-risk local action goes wrong.'
|
|
1032
1045
|
);
|
|
1033
1046
|
}
|
|
1034
|
-
if (costControl
|
|
1047
|
+
if (costControl?.mode && costControl.mode !== 'allow') {
|
|
1035
1048
|
push(
|
|
1036
1049
|
'reduce_model_budget',
|
|
1037
1050
|
'Reduce model budget before execution',
|
|
@@ -1039,7 +1052,7 @@ function buildRemediations({
|
|
|
1039
1052
|
'High token or cost estimates should be reviewed before the model/tool loop continues.'
|
|
1040
1053
|
);
|
|
1041
1054
|
}
|
|
1042
|
-
if (workflowContract
|
|
1055
|
+
if (workflowContract?.active && workflowContract.violations.length > 0) {
|
|
1043
1056
|
const codes = new Set(workflowContract.violations.map((violation) => violation.code));
|
|
1044
1057
|
if (codes.has('missing_required_evidence')) {
|
|
1045
1058
|
push(
|
|
@@ -1241,13 +1254,13 @@ function buildDecisionControl({
|
|
|
1241
1254
|
protectedSurface,
|
|
1242
1255
|
actionProfile,
|
|
1243
1256
|
});
|
|
1244
|
-
const hasOperationalBlockers = Boolean(integrity
|
|
1245
|
-
const hasCostWarning =
|
|
1246
|
-
const hasCostBlock =
|
|
1247
|
-
const hasWorkflowWarning =
|
|
1248
|
-
const hasWorkflowBlock =
|
|
1249
|
-
const hasContractWarning =
|
|
1250
|
-
const hasContractBlock =
|
|
1257
|
+
const hasOperationalBlockers = Boolean(integrity?.blockers?.length);
|
|
1258
|
+
const hasCostWarning = costControl?.mode === 'warn';
|
|
1259
|
+
const hasCostBlock = costControl?.mode === 'block';
|
|
1260
|
+
const hasWorkflowWarning = workflowControl?.mode === 'warn';
|
|
1261
|
+
const hasWorkflowBlock = workflowControl?.mode === 'block';
|
|
1262
|
+
const hasContractWarning = workflowContract?.mode === 'warn';
|
|
1263
|
+
const hasContractBlock = workflowContract?.mode === 'block';
|
|
1251
1264
|
const requiresCheckpoint = decision === 'warn'
|
|
1252
1265
|
|| (decision === 'allow' && (reversibility !== 'two_way_door' || hasOperationalBlockers || hasCostWarning || hasWorkflowWarning || hasContractWarning));
|
|
1253
1266
|
const executionMode = decision === 'deny'
|
|
@@ -1291,49 +1304,51 @@ function buildDecisionControl({
|
|
|
1291
1304
|
};
|
|
1292
1305
|
}
|
|
1293
1306
|
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
learnedPrediction
|
|
1309
|
-
&& learnedPrediction.label === 'deny'
|
|
1310
|
-
&& learnedPrediction.confidence >= 0.7
|
|
1311
|
-
);
|
|
1312
|
-
const learnedWarning = Boolean(
|
|
1313
|
-
learnedPrediction
|
|
1314
|
-
&& ['warn', 'verify', 'deny'].includes(learnedPrediction.label)
|
|
1315
|
-
&& learnedPrediction.confidence >= 0.3
|
|
1316
|
-
);
|
|
1317
|
-
const learnedRecall = Boolean(
|
|
1318
|
-
learnedPrediction
|
|
1319
|
-
&& learnedPrediction.label === 'recall'
|
|
1320
|
-
&& learnedPrediction.confidence >= 0.3
|
|
1307
|
+
function isDestructiveBypass(command) {
|
|
1308
|
+
return /\bgit\s+push\b.*(?:--force|-f)\b/i.test(command)
|
|
1309
|
+
|| /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function getLearnedPrediction(learnedPolicy) {
|
|
1313
|
+
return learnedPolicy?.enabled ? learnedPolicy.prediction : null;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function learnedPredictionMatches(prediction, labels, minConfidence) {
|
|
1317
|
+
return Boolean(
|
|
1318
|
+
prediction
|
|
1319
|
+
&& labels.includes(prediction.label)
|
|
1320
|
+
&& prediction.confidence >= minConfidence
|
|
1321
1321
|
);
|
|
1322
|
-
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function isLowBlastRadius(blastRadius) {
|
|
1325
|
+
return blastRadius.fileCount <= 1
|
|
1323
1326
|
&& blastRadius.surfaceCount <= 1
|
|
1324
1327
|
&& blastRadius.releaseSensitiveFiles.length === 0
|
|
1325
1328
|
&& blastRadius.unapprovedProtectedFiles === 0;
|
|
1326
|
-
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function isLowRiskHandoff({
|
|
1332
|
+
command,
|
|
1333
|
+
destructiveBypass,
|
|
1334
|
+
learnedHardStop,
|
|
1335
|
+
blastRadius,
|
|
1336
|
+
hasOperationalBlockers,
|
|
1337
|
+
memoryGuard,
|
|
1338
|
+
riskScore,
|
|
1339
|
+
}) {
|
|
1340
|
+
return /\bgit\s+push\b|\bgh\s+pr\s+(?:create|merge)\b/i.test(command)
|
|
1327
1341
|
&& !destructiveBypass
|
|
1328
1342
|
&& !learnedHardStop
|
|
1329
|
-
&&
|
|
1343
|
+
&& isLowBlastRadius(blastRadius)
|
|
1330
1344
|
&& !hasOperationalBlockers
|
|
1331
|
-
&& memoryGuard
|
|
1332
|
-
&& memoryGuard.mode !== 'allow'
|
|
1345
|
+
&& memoryGuard?.mode !== 'allow'
|
|
1333
1346
|
&& riskScore <= 0.62;
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function isRepeatedHighBlast(memoryGuard, blastRadius) {
|
|
1350
|
+
return Boolean(
|
|
1351
|
+
memoryGuard?.mode === 'block'
|
|
1337
1352
|
&& (
|
|
1338
1353
|
blastRadius.severity === 'high'
|
|
1339
1354
|
|| blastRadius.severity === 'critical'
|
|
@@ -1341,25 +1356,49 @@ function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blas
|
|
|
1341
1356
|
|| blastRadius.unapprovedProtectedFiles > 0
|
|
1342
1357
|
)
|
|
1343
1358
|
);
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function hasSoftControlWarning({ workflowContract, workflowControl, costControl, riskScore, learnedWarning, learnedRecall }) {
|
|
1362
|
+
return workflowContract?.mode === 'warn'
|
|
1363
|
+
|| workflowControl?.mode === 'warn'
|
|
1364
|
+
|| costControl?.mode === 'warn'
|
|
1365
|
+
|| riskScore >= 0.45
|
|
1366
|
+
|| (learnedWarning && riskScore >= 0.3)
|
|
1367
|
+
|| (learnedRecall && riskScore >= 0.34);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command, costControl, workflowControl, workflowContract, actionProfile }) {
|
|
1371
|
+
if (costControl?.mode === 'block' || workflowControl?.mode === 'block' || workflowContract?.mode === 'block') {
|
|
1372
|
+
return 'deny';
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const hasOperationalBlockers = Boolean(integrity?.blockers?.length);
|
|
1376
|
+
const destructiveBypass = isDestructiveBypass(command);
|
|
1377
|
+
const learnedPrediction = getLearnedPrediction(learnedPolicy);
|
|
1378
|
+
const learnedHardStop = learnedPredictionMatches(learnedPrediction, ['deny'], 0.7);
|
|
1379
|
+
const learnedWarning = learnedPredictionMatches(learnedPrediction, ['warn', 'verify', 'deny'], 0.3);
|
|
1380
|
+
const learnedRecall = learnedPredictionMatches(learnedPrediction, ['recall'], 0.3);
|
|
1347
1381
|
|
|
1348
|
-
if (
|
|
1382
|
+
if (isLowRiskHandoff({ command, destructiveBypass, learnedHardStop, blastRadius, hasOperationalBlockers, memoryGuard, riskScore })) {
|
|
1349
1383
|
return 'allow';
|
|
1350
1384
|
}
|
|
1385
|
+
|
|
1351
1386
|
// Background customer-system actions checkpoint (warn), never hard-deny.
|
|
1352
1387
|
// The checkpoint IS the mitigation — blocking outright prevents legitimate work.
|
|
1353
|
-
if (backgroundAgent && customerSystemAction) {
|
|
1388
|
+
if (actionProfile?.backgroundAgent && actionProfile?.customerSystemAction) {
|
|
1354
1389
|
return 'warn';
|
|
1355
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
const repeatedHighBlast = isRepeatedHighBlast(memoryGuard, blastRadius);
|
|
1356
1393
|
if (destructiveBypass || learnedHardStop || repeatedHighBlast || (hasOperationalBlockers && riskScore >= 0.72) || riskScore >= 0.86) {
|
|
1357
1394
|
return 'deny';
|
|
1358
1395
|
}
|
|
1359
|
-
|
|
1396
|
+
|
|
1397
|
+
if (actionProfile?.economicAction || (actionProfile?.backgroundAgent && riskScore >= 0.3)) {
|
|
1360
1398
|
return 'warn';
|
|
1361
1399
|
}
|
|
1362
|
-
|
|
1400
|
+
|
|
1401
|
+
if (hasSoftControlWarning({ workflowContract, workflowControl, costControl, riskScore, learnedWarning, learnedRecall })) {
|
|
1363
1402
|
return 'warn';
|
|
1364
1403
|
}
|
|
1365
1404
|
return 'allow';
|
|
@@ -1408,6 +1447,10 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
|
|
|
1408
1447
|
baseBranch,
|
|
1409
1448
|
command: normalizedToolInput.command,
|
|
1410
1449
|
changedFiles: affectedFiles,
|
|
1450
|
+
currentBranch: options.currentBranch
|
|
1451
|
+
|| normalizedToolInput.currentBranch
|
|
1452
|
+
|| normalizedToolInput.branchName
|
|
1453
|
+
|| normalizedToolInput.branch,
|
|
1411
1454
|
headSha: options.headSha || toolInput.headSha,
|
|
1412
1455
|
requirePrForReleaseSensitive: options.requirePrForReleaseSensitive === true,
|
|
1413
1456
|
requireVersionNotBehindBase: options.requireVersionNotBehindBase === true,
|