@rigstate/cli 0.6.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/.env.example +5 -0
- package/IMPLEMENTATION.md +239 -0
- package/QUICK_START.md +220 -0
- package/README.md +150 -0
- package/dist/index.cjs +3987 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3964 -0
- package/dist/index.js.map +1 -0
- package/install.sh +15 -0
- package/package.json +53 -0
- package/src/commands/check.ts +329 -0
- package/src/commands/config.ts +81 -0
- package/src/commands/daemon.ts +197 -0
- package/src/commands/env.ts +158 -0
- package/src/commands/fix.ts +140 -0
- package/src/commands/focus.ts +134 -0
- package/src/commands/hooks.ts +163 -0
- package/src/commands/init.ts +282 -0
- package/src/commands/link.ts +45 -0
- package/src/commands/login.ts +35 -0
- package/src/commands/mcp.ts +73 -0
- package/src/commands/nexus.ts +81 -0
- package/src/commands/override.ts +65 -0
- package/src/commands/scan.ts +242 -0
- package/src/commands/sync-rules.ts +191 -0
- package/src/commands/sync.ts +339 -0
- package/src/commands/watch.ts +283 -0
- package/src/commands/work.ts +172 -0
- package/src/daemon/bridge-listener.ts +127 -0
- package/src/daemon/core.ts +184 -0
- package/src/daemon/factory.ts +45 -0
- package/src/daemon/file-watcher.ts +97 -0
- package/src/daemon/guardian-monitor.ts +133 -0
- package/src/daemon/heuristic-engine.ts +203 -0
- package/src/daemon/intervention-protocol.ts +128 -0
- package/src/daemon/telemetry.ts +23 -0
- package/src/daemon/types.ts +18 -0
- package/src/hive/gateway.ts +74 -0
- package/src/hive/protocol.ts +29 -0
- package/src/hive/scrubber.ts +72 -0
- package/src/index.ts +85 -0
- package/src/nexus/council.ts +103 -0
- package/src/nexus/dispatcher.ts +133 -0
- package/src/utils/config.ts +83 -0
- package/src/utils/files.ts +95 -0
- package/src/utils/governance.ts +128 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/manifest.ts +18 -0
- package/src/utils/rule-engine.ts +292 -0
- package/src/utils/skills-provisioner.ts +153 -0
- package/src/utils/version.ts +1 -0
- package/src/utils/watchdog.ts +215 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian Monitor - Checks files against Guardian rules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { checkFile, type EffectiveRule, type Violation, type CheckResult } from '../utils/rule-engine.js';
|
|
9
|
+
|
|
10
|
+
const CACHE_FILE = '.rigstate/rules-cache.json';
|
|
11
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
|
|
13
|
+
interface CachedRules {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
rules: EffectiveRule[];
|
|
17
|
+
settings: { lmax: number; lmax_warning: number };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GuardianMonitor {
|
|
21
|
+
loadRules(): Promise<void>;
|
|
22
|
+
checkFile(filePath: string): Promise<CheckResult>;
|
|
23
|
+
getRuleCount(): number;
|
|
24
|
+
getRules(): EffectiveRule[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createGuardianMonitor(
|
|
28
|
+
projectId: string,
|
|
29
|
+
apiUrl: string,
|
|
30
|
+
apiKey: string
|
|
31
|
+
): GuardianMonitor {
|
|
32
|
+
let rules: EffectiveRule[] = [];
|
|
33
|
+
let lastFetch: number = 0;
|
|
34
|
+
|
|
35
|
+
const loadRules = async (): Promise<void> => {
|
|
36
|
+
// Check if cache is fresh
|
|
37
|
+
if (rules.length > 0 && Date.now() - lastFetch < CACHE_TTL_MS) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Try API first
|
|
43
|
+
const response = await axios.get(`${apiUrl}/api/v1/guardian/rules`, {
|
|
44
|
+
params: { project_id: projectId },
|
|
45
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
46
|
+
timeout: 10000
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (response.data.success && response.data.data.rules) {
|
|
50
|
+
rules = response.data.data.rules;
|
|
51
|
+
lastFetch = Date.now();
|
|
52
|
+
|
|
53
|
+
// Save to cache
|
|
54
|
+
await saveCachedRules(projectId, rules);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Try cache fallback
|
|
59
|
+
const cached = await loadCachedRules(projectId);
|
|
60
|
+
if (cached) {
|
|
61
|
+
rules = cached.rules;
|
|
62
|
+
lastFetch = Date.now();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// No rules available
|
|
68
|
+
rules = [];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const checkFileImpl = async (filePath: string): Promise<CheckResult> => {
|
|
72
|
+
// Ensure rules are loaded
|
|
73
|
+
await loadRules();
|
|
74
|
+
|
|
75
|
+
if (rules.length === 0) {
|
|
76
|
+
return {
|
|
77
|
+
file: filePath,
|
|
78
|
+
violations: [],
|
|
79
|
+
passed: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
84
|
+
return checkFile(absolutePath, rules, process.cwd());
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getRuleCount = (): number => rules.length;
|
|
88
|
+
const getRules = (): EffectiveRule[] => rules;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
loadRules,
|
|
92
|
+
checkFile: checkFileImpl,
|
|
93
|
+
getRuleCount,
|
|
94
|
+
getRules
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadCachedRules(projectId: string): Promise<CachedRules | null> {
|
|
99
|
+
try {
|
|
100
|
+
const cachePath = path.join(process.cwd(), CACHE_FILE);
|
|
101
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
102
|
+
const cached: CachedRules = JSON.parse(content);
|
|
103
|
+
|
|
104
|
+
if (cached.projectId !== projectId) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return cached;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function saveCachedRules(projectId: string, rules: EffectiveRule[]): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
const cacheDir = path.join(process.cwd(), '.rigstate');
|
|
117
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const cached: CachedRules = {
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
projectId,
|
|
122
|
+
rules,
|
|
123
|
+
settings: { lmax: 400, lmax_warning: 350 }
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(
|
|
127
|
+
path.join(cacheDir, 'rules-cache.json'),
|
|
128
|
+
JSON.stringify(cached, null, 2)
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
// Silently fail cache write
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
|
|
6
|
+
export interface SkillTrigger {
|
|
7
|
+
skillId: string;
|
|
8
|
+
patterns: {
|
|
9
|
+
imports?: string[]; // e.g. "@stripe/stripe-js"
|
|
10
|
+
content?: string[]; // Regex strings e.g. "payment_intent"
|
|
11
|
+
files?: string[]; // Glob patterns e.g. "**/*.sql"
|
|
12
|
+
violation_id?: string;
|
|
13
|
+
metric_threshold?: number;
|
|
14
|
+
};
|
|
15
|
+
confidence: 'high' | 'medium' | 'low';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HeuristicMatch {
|
|
19
|
+
skillId: string;
|
|
20
|
+
file: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
confidence: 'high' | 'medium' | 'low';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// TODO: In the future, this should fetch from the Rigstate API (The Registry)
|
|
26
|
+
const GLOBAL_HEURISTICS: SkillTrigger[] = [
|
|
27
|
+
{
|
|
28
|
+
skillId: 'payment-expert',
|
|
29
|
+
patterns: {
|
|
30
|
+
imports: ['@stripe/', 'stripe'],
|
|
31
|
+
content: ['PaymentIntent', 'CheckoutSession'],
|
|
32
|
+
},
|
|
33
|
+
confidence: 'high'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
skillId: 'rigstate-integrity-gate',
|
|
37
|
+
patterns: {
|
|
38
|
+
files: ['**/release.config.js', '**/manifest.json', '**/.rigstate/release/*'],
|
|
39
|
+
content: ['[CORE INTEGRITY]', 'prepare_release']
|
|
40
|
+
},
|
|
41
|
+
confidence: 'high'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
skillId: 'database-architect',
|
|
45
|
+
patterns: {
|
|
46
|
+
files: ['**/*.sql', '**/schema.prisma', '**/migrations/*'],
|
|
47
|
+
imports: ['@supabase/supabase-js', 'drizzle-orm', 'prisma']
|
|
48
|
+
},
|
|
49
|
+
confidence: 'medium'
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export class HeuristicEngine {
|
|
54
|
+
private rules: SkillTrigger[] = [];
|
|
55
|
+
private cachePath: string;
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
this.cachePath = path.join(process.cwd(), '.rigstate', 'cache', 'heuristics.json');
|
|
59
|
+
this.loadRules();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async loadRules() {
|
|
63
|
+
try {
|
|
64
|
+
const cached = await readFile(this.cachePath, 'utf-8');
|
|
65
|
+
const data = JSON.parse(cached);
|
|
66
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
67
|
+
this.rules = data;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// No cache, use defaults
|
|
72
|
+
}
|
|
73
|
+
this.rules = GLOBAL_HEURISTICS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async refreshRules(projectId: string, apiUrl: string, apiKey: string) {
|
|
77
|
+
try {
|
|
78
|
+
// Ensure cache directory exists
|
|
79
|
+
await mkdir(dirname(this.cachePath), { recursive: true });
|
|
80
|
+
|
|
81
|
+
const endpoint = `${apiUrl}/api/v1/skills/triggers`;
|
|
82
|
+
|
|
83
|
+
const response = await axios.get(endpoint, {
|
|
84
|
+
headers: {
|
|
85
|
+
'x-api-key': apiKey,
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (response.data && Array.isArray(response.data.triggers)) {
|
|
91
|
+
const cloudRules = response.data.triggers;
|
|
92
|
+
|
|
93
|
+
// Write to cache
|
|
94
|
+
await writeFile(this.cachePath, JSON.stringify(cloudRules, null, 2));
|
|
95
|
+
|
|
96
|
+
this.rules = cloudRules;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async analyzeFile(filePath: string, metrics?: { lineCount: number, rules: any[] }): Promise<HeuristicMatch[]> {
|
|
105
|
+
try {
|
|
106
|
+
const content = await readFile(filePath, 'utf-8');
|
|
107
|
+
const matches: HeuristicMatch[] = [];
|
|
108
|
+
|
|
109
|
+
// Use dynamic rules
|
|
110
|
+
const activeRules = this.rules.length > 0 ? this.rules : GLOBAL_HEURISTICS;
|
|
111
|
+
|
|
112
|
+
for (const heuristic of activeRules) {
|
|
113
|
+
const match = this.checkHeuristic(filePath, content, heuristic, metrics);
|
|
114
|
+
if (match) {
|
|
115
|
+
matches.push(match);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return matches;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Ignore file read errors (deleted files, etc)
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private checkHeuristic(
|
|
127
|
+
filePath: string,
|
|
128
|
+
content: string,
|
|
129
|
+
heuristic: SkillTrigger,
|
|
130
|
+
metrics?: { lineCount: number, rules: any[] }
|
|
131
|
+
): HeuristicMatch | null {
|
|
132
|
+
// 0. Check Metric Thresholds (The 80% Rule)
|
|
133
|
+
if (heuristic.patterns.metric_threshold && metrics) {
|
|
134
|
+
const lineLimitRule = metrics.rules.find(r => r.rule_type === 'MAX_FILE_LINES');
|
|
135
|
+
if (lineLimitRule) {
|
|
136
|
+
const limit = (lineLimitRule.value as any).limit;
|
|
137
|
+
const threshold = limit * heuristic.patterns.metric_threshold;
|
|
138
|
+
|
|
139
|
+
if (metrics.lineCount >= threshold && metrics.lineCount < limit) {
|
|
140
|
+
return {
|
|
141
|
+
skillId: heuristic.skillId,
|
|
142
|
+
file: filePath,
|
|
143
|
+
reason: `File reached ${Math.round((metrics.lineCount / limit) * 100)}% of its line limit (${metrics.lineCount}/${limit})`,
|
|
144
|
+
confidence: 'high'
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 1. Check File Path Patterns
|
|
151
|
+
if (heuristic.patterns.files) {
|
|
152
|
+
// Simple endsWith check for now, ideally use micromatch
|
|
153
|
+
const matchesFile = heuristic.patterns.files.some(pattern => {
|
|
154
|
+
if (pattern.startsWith('**/*')) return filePath.endsWith(pattern.replace('**/*', ''));
|
|
155
|
+
return filePath.includes(pattern);
|
|
156
|
+
});
|
|
157
|
+
if (matchesFile) {
|
|
158
|
+
return {
|
|
159
|
+
skillId: heuristic.skillId,
|
|
160
|
+
file: filePath,
|
|
161
|
+
reason: `Matches file pattern: ${heuristic.patterns.files.join(', ')}`,
|
|
162
|
+
confidence: heuristic.confidence
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Check Imports
|
|
168
|
+
if (heuristic.patterns.imports) {
|
|
169
|
+
for (const imp of heuristic.patterns.imports) {
|
|
170
|
+
// Regex to find import or require
|
|
171
|
+
const importRegex = new RegExp(`(import .* from ['"]${imp}|require\\(['"]${imp})`, 'i');
|
|
172
|
+
if (importRegex.test(content)) {
|
|
173
|
+
return {
|
|
174
|
+
skillId: heuristic.skillId,
|
|
175
|
+
file: filePath,
|
|
176
|
+
reason: `Detected import: ${imp}`,
|
|
177
|
+
confidence: heuristic.confidence
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 3. Check Content
|
|
184
|
+
if (heuristic.patterns.content) {
|
|
185
|
+
for (const pattern of heuristic.patterns.content) {
|
|
186
|
+
if (content.includes(pattern)) {
|
|
187
|
+
return {
|
|
188
|
+
skillId: heuristic.skillId,
|
|
189
|
+
file: filePath,
|
|
190
|
+
reason: `Detected content pattern: ${pattern}`,
|
|
191
|
+
confidence: heuristic.confidence
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createHeuristicEngine() {
|
|
202
|
+
return new HeuristicEngine();
|
|
203
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
export type GovernanceMode = 'OPEN' | 'SOFT_LOCK' | 'HARD_LOCK';
|
|
7
|
+
|
|
8
|
+
export interface InterventionDecision {
|
|
9
|
+
mode: GovernanceMode;
|
|
10
|
+
message: string;
|
|
11
|
+
blockCommit: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The Silent Sentinel (Frank's Enforcer)
|
|
16
|
+
* Determines the severity of an event and the required system response.
|
|
17
|
+
*/
|
|
18
|
+
export class InterventionProtocol {
|
|
19
|
+
private activeViolators = new Set<string>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a violation outcome to update the global lock state.
|
|
23
|
+
*/
|
|
24
|
+
registerViolation(filePath: string, decision: InterventionDecision) {
|
|
25
|
+
if (decision.mode === 'HARD_LOCK') {
|
|
26
|
+
this.activeViolators.add(filePath);
|
|
27
|
+
this.syncLockFile();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clears any active locks for a specific file (e.g. after a fix).
|
|
33
|
+
*/
|
|
34
|
+
clear(filePath: string) {
|
|
35
|
+
if (this.activeViolators.has(filePath)) {
|
|
36
|
+
this.activeViolators.delete(filePath);
|
|
37
|
+
this.syncLockFile();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private syncLockFile() {
|
|
42
|
+
try {
|
|
43
|
+
const lockDir = path.join(process.cwd(), '.rigstate');
|
|
44
|
+
if (!fs.existsSync(lockDir)) fs.mkdirSync(lockDir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
const lockPath = path.join(lockDir, 'guardian.lock');
|
|
47
|
+
|
|
48
|
+
if (this.activeViolators.size > 0) {
|
|
49
|
+
const content = `HARD_LOCK_ACTIVE\nTimestamp: ${new Date().toISOString()}\n\nBlocking Files:\n${Array.from(this.activeViolators).join('\n')}`;
|
|
50
|
+
fs.writeFileSync(lockPath, content, 'utf-8');
|
|
51
|
+
} else {
|
|
52
|
+
if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('Failed to sync guardian lock file:', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate a Heuristic Trigger (Preventative)
|
|
61
|
+
*/
|
|
62
|
+
evaluateTrigger(skillId: string, confidence: string): InterventionDecision {
|
|
63
|
+
// Example: If a skill is marked as 'HARD_LOCK' in its metadata, we block.
|
|
64
|
+
// For now, most triggers are informational (JIT provisioning).
|
|
65
|
+
|
|
66
|
+
if (skillId === 'rigstate-integrity-gate') {
|
|
67
|
+
return {
|
|
68
|
+
mode: 'SOFT_LOCK',
|
|
69
|
+
message: 'Integrity Gate detected. Release Manifest required before final push.',
|
|
70
|
+
blockCommit: false // Soft lock just warns
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
mode: 'OPEN',
|
|
76
|
+
message: `Predictive activation: ${skillId}`,
|
|
77
|
+
blockCommit: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Evaluate a Guardian Violation (Corrective)
|
|
83
|
+
*/
|
|
84
|
+
evaluateViolation(ruleId: string, severity: 'critical' | 'warning' | 'info'): InterventionDecision {
|
|
85
|
+
if (severity === 'critical' || (severity as any) === 'error') {
|
|
86
|
+
return {
|
|
87
|
+
mode: 'HARD_LOCK',
|
|
88
|
+
message: `CRITICAL VIOLATION: ${ruleId}. System Integrity Compromised.`,
|
|
89
|
+
blockCommit: true
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (severity === 'warning') {
|
|
94
|
+
return {
|
|
95
|
+
mode: 'SOFT_LOCK',
|
|
96
|
+
message: `Warning: ${ruleId}. Review recommended.`,
|
|
97
|
+
blockCommit: false
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
mode: 'OPEN',
|
|
103
|
+
message: 'Info notice.',
|
|
104
|
+
blockCommit: false
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Logs the intervention to the console with appropriate visual weight
|
|
110
|
+
*/
|
|
111
|
+
enforce(decision: InterventionDecision) {
|
|
112
|
+
if (decision.mode === 'OPEN') return;
|
|
113
|
+
|
|
114
|
+
const icon = decision.mode === 'HARD_LOCK' ? '🚫' : '⚠️';
|
|
115
|
+
const color = decision.mode === 'HARD_LOCK' ? chalk.bgRed.white.bold : chalk.yellow.bold;
|
|
116
|
+
|
|
117
|
+
console.log('\n' + color(` ${icon} [${decision.mode}] INTERVENTION `));
|
|
118
|
+
console.log(chalk.redBright(` ${decision.message}`));
|
|
119
|
+
|
|
120
|
+
if (decision.blockCommit) {
|
|
121
|
+
console.log(chalk.dim(' 🔒 Commit functionality is logically suspended until fixed.'));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createInterventionProtocol() {
|
|
127
|
+
return new InterventionProtocol();
|
|
128
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reports skill usage to the Rigstate Cloud.
|
|
5
|
+
*/
|
|
6
|
+
export async function trackSkillUsage(
|
|
7
|
+
apiUrl: string,
|
|
8
|
+
apiKey: string,
|
|
9
|
+
projectId: string,
|
|
10
|
+
skillId: string
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
await axios.post(`${apiUrl}/api/v1/skills/usage`, {
|
|
14
|
+
projectId,
|
|
15
|
+
skillName: skillId,
|
|
16
|
+
status: 'ACTIVATED'
|
|
17
|
+
}, {
|
|
18
|
+
headers: { 'x-api-key': apiKey }
|
|
19
|
+
});
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Silhouette feedback failure - do not interrupt user
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface DaemonConfig {
|
|
2
|
+
projectId: string;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
watchPath: string;
|
|
6
|
+
checkOnChange: boolean;
|
|
7
|
+
bridgeEnabled: boolean;
|
|
8
|
+
verbose: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DaemonState {
|
|
12
|
+
isRunning: boolean;
|
|
13
|
+
startedAt: string | null;
|
|
14
|
+
filesChecked: number;
|
|
15
|
+
violationsFound: number;
|
|
16
|
+
tasksProcessed: number;
|
|
17
|
+
lastActivity: string | null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
import axios, { AxiosInstance } from 'axios';
|
|
3
|
+
import { ImmuneSignal } from './protocol';
|
|
4
|
+
import { HiveScrubber } from './scrubber';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* THE HIVE GATEWAY
|
|
9
|
+
* Connects the local Rigstate instance to the Global Hive Mind (rigstate.com).
|
|
10
|
+
*/
|
|
11
|
+
export class HiveGateway {
|
|
12
|
+
private client: AxiosInstance;
|
|
13
|
+
private enabled: boolean;
|
|
14
|
+
private lastSignalTime: number = 0;
|
|
15
|
+
private readonly MIN_INTERVAL_MS = 5000; // Throttle: Max 1 signal per 5s
|
|
16
|
+
|
|
17
|
+
constructor(baseUrl: string, token?: string) {
|
|
18
|
+
this.enabled = !!token;
|
|
19
|
+
|
|
20
|
+
if (!this.enabled) {
|
|
21
|
+
console.log(chalk.dim('⚠️ Hive Gateway disabled (No Token provided). Running in localized mode.'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.client = axios.create({
|
|
25
|
+
baseURL: baseUrl,
|
|
26
|
+
headers: {
|
|
27
|
+
'Authorization': `Bearer ${token}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'X-Rigstate-Client': 'CLI-0.2.0'
|
|
30
|
+
},
|
|
31
|
+
timeout: 5000
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Transmit an Immune Signal to the Hive.
|
|
37
|
+
* Includes Pre-Flight Scrubbing and Throttling.
|
|
38
|
+
*/
|
|
39
|
+
public async transmit(signal: ImmuneSignal): Promise<boolean> {
|
|
40
|
+
if (!this.enabled) return false;
|
|
41
|
+
|
|
42
|
+
// 1. THROTTLE CHECK
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (now - this.lastSignalTime < this.MIN_INTERVAL_MS) {
|
|
45
|
+
console.warn(chalk.yellow('⏳ Hive Gateway Throttled. Signal dropped to preventing spam.'));
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. SCRUBBER VERIFICATION (Double Check)
|
|
50
|
+
// Even if Frank ran it, we run it again here at the edge.
|
|
51
|
+
const scrubResult = HiveScrubber.scrub(signal.ruleContent);
|
|
52
|
+
if (scrubResult.riskScore > 20) {
|
|
53
|
+
console.error(chalk.red(`🛑 HIVE BLOCKED: Signal contains sensitive data (Risk: ${scrubResult.riskScore})`));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 3. TRANSMISSION
|
|
58
|
+
try {
|
|
59
|
+
console.log(chalk.blue(`📡 Uplinking to Hive... [${signal.vector}]`));
|
|
60
|
+
|
|
61
|
+
// Using the scrubbed content just to be 100% safe
|
|
62
|
+
const payload = { ...signal, ruleContent: scrubResult.sanitizedContent };
|
|
63
|
+
|
|
64
|
+
await this.client.post('/signal', payload);
|
|
65
|
+
|
|
66
|
+
this.lastSignalTime = now;
|
|
67
|
+
console.log(chalk.green('✅ Signal Received by Hive Core. Knowledge Shared.'));
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error: any) {
|
|
70
|
+
console.error(chalk.red(`❌ Hive Transmission Failed: ${error.message}`));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* THE HIVE PROTOCOL
|
|
4
|
+
* The standard format for "Immune Signals" shared between Rigstate Instances.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ImmuneSignal {
|
|
8
|
+
id: string; // UUID
|
|
9
|
+
type: 'SECURITY_PATCH' | 'PERFORMANCE_FIX' | 'ARCH_VIOLATION';
|
|
10
|
+
|
|
11
|
+
// Abstracted vector (e.g. "Next.js Server Action")
|
|
12
|
+
vector: string;
|
|
13
|
+
|
|
14
|
+
// The rule itself (Sanitized via Scrubber)
|
|
15
|
+
ruleContent: string;
|
|
16
|
+
|
|
17
|
+
// Metrics proving why this is good
|
|
18
|
+
confidenceScore: number; // 0-100 (Based on local tests passed)
|
|
19
|
+
|
|
20
|
+
schemaVersion: '1.0';
|
|
21
|
+
timestamp: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface HiveSyncConfig {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
endpoint: string;
|
|
27
|
+
mode: 'CONSUME_ONLY' | 'CONTRIBUTE' | 'FULL_DUPLEX';
|
|
28
|
+
minConfidenceThreshold: number; // Only import rules with score > 80
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* THE HIVE SCRUBBER
|
|
4
|
+
* Sanitizes local rules and signals before they are broadcast to the Global Registry.
|
|
5
|
+
* Ensures strict anonymity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ScrubberResult {
|
|
9
|
+
sanitizedContent: string;
|
|
10
|
+
redactionCount: number;
|
|
11
|
+
riskScore: number; // 0-100 (If too high, do not broadcast)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class HiveScrubber {
|
|
15
|
+
|
|
16
|
+
// Patterns that definitely identify a project and MUST be removed
|
|
17
|
+
private static SENSITIVE_PATTERNS = [
|
|
18
|
+
/(api_key|secret|token|password)[\s]*[:=][\s]*['"][a-zA-Z0-9_\-]+['"]/gi, // Secrets
|
|
19
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // Emails
|
|
20
|
+
/(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\. (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/g, // IPs
|
|
21
|
+
/postgres:\/\/[^:]+:[^@]+@/g, // DB Connection Strings
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Generic replacements for project-specific terms to keep the rule abstract
|
|
25
|
+
private static ABSTRACTION_MAP: Record<string, string> = {
|
|
26
|
+
'Vibeline': '{PROJECT_NAME}',
|
|
27
|
+
'Rigstate': '{FRAMEWORK}',
|
|
28
|
+
'Steinhofve': '{USER}',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Scrubs a string (rule content or log excerpt) of sensitive data.
|
|
33
|
+
*/
|
|
34
|
+
public static scrub(content: string, customTerms: string[] = []): ScrubberResult {
|
|
35
|
+
let scrubbed = content;
|
|
36
|
+
let count = 0;
|
|
37
|
+
let risk = 0;
|
|
38
|
+
|
|
39
|
+
// 1. Remove Hard Credentials (High Risk)
|
|
40
|
+
this.SENSITIVE_PATTERNS.forEach(pattern => {
|
|
41
|
+
if (pattern.test(scrubbed)) {
|
|
42
|
+
scrubbed = scrubbed.replace(pattern, '[REDACTED_CREDENTIAL]');
|
|
43
|
+
count++;
|
|
44
|
+
risk += 50; // High penalty for finding secrets
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 2. Abstract Project Specifics (Medium Risk)
|
|
49
|
+
Object.entries(this.ABSTRACTION_MAP).forEach(([term, replacement]) => {
|
|
50
|
+
const regex = new RegExp(term, 'gi');
|
|
51
|
+
if (regex.test(scrubbed)) {
|
|
52
|
+
scrubbed = scrubbed.replace(regex, replacement);
|
|
53
|
+
count++;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 3. Scrub Custom Terms (e.g. Table names passed from context)
|
|
58
|
+
customTerms.forEach(term => {
|
|
59
|
+
const regex = new RegExp(term, 'gi');
|
|
60
|
+
if (regex.test(scrubbed)) {
|
|
61
|
+
scrubbed = scrubbed.replace(regex, '{ENTITY}');
|
|
62
|
+
count++;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
sanitizedContent: scrubbed,
|
|
68
|
+
redactionCount: count,
|
|
69
|
+
riskScore: Math.min(risk, 100)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|