@rigour-labs/core 5.5.4 → 6.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/automatic-index.d.ts +18 -0
- package/dist/context/automatic-index.js +142 -0
- package/dist/context/automatic-index.test.d.ts +1 -0
- package/dist/context/automatic-index.test.js +45 -0
- package/dist/context/cache-engine.d.ts +25 -0
- package/dist/context/dependency-graph.d.ts +13 -0
- package/dist/context/dependency-graph.js +97 -0
- package/dist/context/dependency-graph.test.d.ts +1 -0
- package/dist/context/dependency-graph.test.js +13 -0
- package/dist/context/index-bridge.js +21 -2
- package/dist/firewall/adversarial.d.ts +13 -0
- package/dist/firewall/adversarial.js +125 -0
- package/dist/firewall/arbitration-token.d.ts +8 -0
- package/dist/firewall/arbitration-token.js +58 -0
- package/dist/firewall/attestation.d.ts +38 -0
- package/dist/firewall/attestation.js +197 -0
- package/dist/firewall/capability-broker.d.ts +47 -0
- package/dist/firewall/capability-broker.js +209 -0
- package/dist/firewall/firewall.test.d.ts +1 -0
- package/dist/firewall/firewall.test.js +102 -0
- package/dist/firewall/index.d.ts +10 -0
- package/dist/firewall/index.js +10 -0
- package/dist/firewall/mcp-gateway.d.ts +40 -0
- package/dist/firewall/mcp-gateway.js +64 -0
- package/dist/firewall/policy-hash.d.ts +1 -0
- package/dist/firewall/policy-hash.js +5 -0
- package/dist/firewall/scope-enforcement.d.ts +26 -0
- package/dist/firewall/scope-enforcement.js +155 -0
- package/dist/firewall/transaction.d.ts +43 -0
- package/dist/firewall/transaction.js +216 -0
- package/dist/firewall/typed-command.d.ts +16 -0
- package/dist/firewall/typed-command.js +129 -0
- package/dist/firewall/types.d.ts +96 -0
- package/dist/firewall/types.js +5 -0
- package/dist/hooks/checker.d.ts +2 -0
- package/dist/hooks/checker.js +25 -1
- package/dist/hooks/standalone-checker.js +5 -1
- package/dist/hooks/templates.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/pattern-index/embeddings.js +10 -3
- package/dist/pattern-index/indexer.d.ts +2 -0
- package/dist/pattern-index/indexer.js +72 -0
- package/dist/services/agent-history.d.ts +76 -0
- package/dist/services/agent-history.js +201 -0
- package/dist/services/agent-history.test.d.ts +1 -0
- package/dist/services/agent-history.test.js +69 -0
- package/dist/services/context-telemetry-service.d.ts +21 -0
- package/dist/services/context-telemetry-service.js +36 -7
- package/dist/services/context-telemetry-service.test.js +13 -0
- package/dist/services/engineering-knowledge-graph.d.ts +55 -0
- package/dist/services/engineering-knowledge-graph.js +177 -0
- package/dist/services/engineering-knowledge-graph.test.d.ts +1 -0
- package/dist/services/engineering-knowledge-graph.test.js +59 -0
- package/dist/services/model-pricing.d.ts +2 -0
- package/dist/services/model-pricing.js +2 -0
- package/dist/services/system-health.d.ts +34 -0
- package/dist/services/system-health.js +53 -0
- package/dist/storage/db.js +130 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/lessons.d.ts +60 -0
- package/dist/storage/lessons.js +322 -0
- package/dist/storage/lessons.test.d.ts +1 -0
- package/dist/storage/lessons.test.js +50 -0
- package/dist/storage/local-encryption.d.ts +2 -0
- package/dist/storage/local-encryption.js +41 -0
- package/dist/storage/team-store.d.ts +75 -0
- package/dist/storage/team-store.js +321 -0
- package/dist/storage/team-store.test.d.ts +1 -0
- package/dist/storage/team-store.test.js +11 -0
- package/dist/storage/team-vector-store.d.ts +25 -0
- package/dist/storage/team-vector-store.js +183 -0
- package/dist/storage/team-vector-store.test.d.ts +1 -0
- package/dist/storage/team-vector-store.test.js +42 -0
- package/package.json +8 -6
package/dist/storage/db.js
CHANGED
|
@@ -45,7 +45,7 @@ function loadSqlite3() {
|
|
|
45
45
|
const RIGOUR_DIR = path.join(os.homedir(), '.rigour');
|
|
46
46
|
const DB_PATH = path.join(RIGOUR_DIR, 'rigour.db');
|
|
47
47
|
/** Current schema version — bump when adding migrations. */
|
|
48
|
-
const SCHEMA_VERSION =
|
|
48
|
+
const SCHEMA_VERSION = 7;
|
|
49
49
|
const SCHEMA_SQL = `
|
|
50
50
|
-- Schema version tracking
|
|
51
51
|
CREATE TABLE IF NOT EXISTS meta (
|
|
@@ -177,6 +177,61 @@ CREATE TABLE IF NOT EXISTS checkpoint_metrics (
|
|
|
177
177
|
created_at INTEGER NOT NULL
|
|
178
178
|
);
|
|
179
179
|
|
|
180
|
+
-- Evidence-gated learning produced during normal agent interaction.
|
|
181
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
182
|
+
id TEXT PRIMARY KEY,
|
|
183
|
+
repository_id TEXT NOT NULL,
|
|
184
|
+
actor_id TEXT,
|
|
185
|
+
team_id TEXT,
|
|
186
|
+
visibility TEXT NOT NULL DEFAULT 'personal',
|
|
187
|
+
state TEXT NOT NULL DEFAULT 'candidate',
|
|
188
|
+
kind TEXT NOT NULL,
|
|
189
|
+
subject TEXT NOT NULL,
|
|
190
|
+
evidence_json TEXT NOT NULL,
|
|
191
|
+
confidence REAL NOT NULL DEFAULT 0.3,
|
|
192
|
+
source TEXT NOT NULL,
|
|
193
|
+
supersedes_id TEXT,
|
|
194
|
+
created_at INTEGER NOT NULL,
|
|
195
|
+
updated_at INTEGER NOT NULL
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS sync_outbox (
|
|
199
|
+
id TEXT PRIMARY KEY,
|
|
200
|
+
operation TEXT NOT NULL,
|
|
201
|
+
entity_type TEXT NOT NULL,
|
|
202
|
+
entity_id TEXT NOT NULL,
|
|
203
|
+
payload_json TEXT NOT NULL,
|
|
204
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
205
|
+
last_error TEXT,
|
|
206
|
+
created_at INTEGER NOT NULL,
|
|
207
|
+
synced_at INTEGER
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
-- Stable repository registry used by cross-project learning and graph views.
|
|
211
|
+
CREATE TABLE IF NOT EXISTS repositories (
|
|
212
|
+
id TEXT PRIMARY KEY,
|
|
213
|
+
canonical_uri TEXT NOT NULL,
|
|
214
|
+
display_name TEXT NOT NULL,
|
|
215
|
+
last_seen INTEGER NOT NULL
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
-- Append-only observations from normal governed agent work. These are evidence,
|
|
219
|
+
-- not reusable rules; lesson promotion remains a separate gated decision.
|
|
220
|
+
CREATE TABLE IF NOT EXISTS interaction_events (
|
|
221
|
+
id TEXT PRIMARY KEY,
|
|
222
|
+
repository_id TEXT NOT NULL,
|
|
223
|
+
actor_id TEXT,
|
|
224
|
+
task_id TEXT,
|
|
225
|
+
session_id TEXT,
|
|
226
|
+
request_id TEXT NOT NULL,
|
|
227
|
+
tool_name TEXT NOT NULL,
|
|
228
|
+
phase TEXT NOT NULL,
|
|
229
|
+
outcome TEXT NOT NULL,
|
|
230
|
+
deterministic INTEGER NOT NULL DEFAULT 0,
|
|
231
|
+
evidence_json TEXT NOT NULL,
|
|
232
|
+
created_at INTEGER NOT NULL
|
|
233
|
+
);
|
|
234
|
+
|
|
180
235
|
-- Indexes for performance
|
|
181
236
|
CREATE INDEX IF NOT EXISTS idx_scans_repo ON scans(repo);
|
|
182
237
|
CREATE INDEX IF NOT EXISTS idx_scans_timestamp ON scans(timestamp);
|
|
@@ -192,6 +247,11 @@ CREATE INDEX IF NOT EXISTS idx_model_usage_task ON model_usage(task_id);
|
|
|
192
247
|
CREATE INDEX IF NOT EXISTS idx_model_usage_created ON model_usage(created_at);
|
|
193
248
|
CREATE INDEX IF NOT EXISTS idx_context_cache_type ON context_cache(cache_type);
|
|
194
249
|
CREATE INDEX IF NOT EXISTS idx_checkpoint_metrics_task ON checkpoint_metrics(task_id);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_lookup ON lessons(repository_id, actor_id, team_id, state);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_sync_outbox_pending ON sync_outbox(synced_at, created_at);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_repositories_last_seen ON repositories(last_seen);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_repo_created ON interaction_events(repository_id, created_at);
|
|
254
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_request ON interaction_events(request_id);
|
|
195
255
|
`;
|
|
196
256
|
function wrapDatabase(raw) {
|
|
197
257
|
const db = {
|
|
@@ -369,6 +429,75 @@ async function runMigrations(db) {
|
|
|
369
429
|
`);
|
|
370
430
|
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '4')");
|
|
371
431
|
}
|
|
432
|
+
if (current < 5) {
|
|
433
|
+
await db.exec(`
|
|
434
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
435
|
+
id TEXT PRIMARY KEY,
|
|
436
|
+
repository_id TEXT NOT NULL,
|
|
437
|
+
actor_id TEXT,
|
|
438
|
+
team_id TEXT,
|
|
439
|
+
visibility TEXT NOT NULL DEFAULT 'personal',
|
|
440
|
+
state TEXT NOT NULL DEFAULT 'candidate',
|
|
441
|
+
kind TEXT NOT NULL,
|
|
442
|
+
subject TEXT NOT NULL,
|
|
443
|
+
evidence_json TEXT NOT NULL,
|
|
444
|
+
confidence REAL NOT NULL DEFAULT 0.3,
|
|
445
|
+
source TEXT NOT NULL,
|
|
446
|
+
supersedes_id TEXT,
|
|
447
|
+
created_at INTEGER NOT NULL,
|
|
448
|
+
updated_at INTEGER NOT NULL
|
|
449
|
+
);
|
|
450
|
+
CREATE TABLE IF NOT EXISTS sync_outbox (
|
|
451
|
+
id TEXT PRIMARY KEY,
|
|
452
|
+
operation TEXT NOT NULL,
|
|
453
|
+
entity_type TEXT NOT NULL,
|
|
454
|
+
entity_id TEXT NOT NULL,
|
|
455
|
+
payload_json TEXT NOT NULL,
|
|
456
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
457
|
+
last_error TEXT,
|
|
458
|
+
created_at INTEGER NOT NULL,
|
|
459
|
+
synced_at INTEGER
|
|
460
|
+
);
|
|
461
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_lookup ON lessons(repository_id, actor_id, team_id, state);
|
|
462
|
+
CREATE INDEX IF NOT EXISTS idx_sync_outbox_pending ON sync_outbox(synced_at, created_at);
|
|
463
|
+
`);
|
|
464
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '5')");
|
|
465
|
+
}
|
|
466
|
+
if (current < 6) {
|
|
467
|
+
await db.exec(`
|
|
468
|
+
CREATE TABLE IF NOT EXISTS repositories (
|
|
469
|
+
id TEXT PRIMARY KEY,
|
|
470
|
+
canonical_uri TEXT NOT NULL,
|
|
471
|
+
display_name TEXT NOT NULL,
|
|
472
|
+
last_seen INTEGER NOT NULL
|
|
473
|
+
);
|
|
474
|
+
CREATE INDEX IF NOT EXISTS idx_repositories_last_seen ON repositories(last_seen);
|
|
475
|
+
`);
|
|
476
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '6')");
|
|
477
|
+
}
|
|
478
|
+
if (current < 7) {
|
|
479
|
+
await db.exec(`
|
|
480
|
+
CREATE TABLE IF NOT EXISTS interaction_events (
|
|
481
|
+
id TEXT PRIMARY KEY,
|
|
482
|
+
repository_id TEXT NOT NULL,
|
|
483
|
+
actor_id TEXT,
|
|
484
|
+
task_id TEXT,
|
|
485
|
+
session_id TEXT,
|
|
486
|
+
request_id TEXT NOT NULL,
|
|
487
|
+
tool_name TEXT NOT NULL,
|
|
488
|
+
phase TEXT NOT NULL,
|
|
489
|
+
outcome TEXT NOT NULL,
|
|
490
|
+
deterministic INTEGER NOT NULL DEFAULT 0,
|
|
491
|
+
evidence_json TEXT NOT NULL,
|
|
492
|
+
created_at INTEGER NOT NULL
|
|
493
|
+
);
|
|
494
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_repo_created
|
|
495
|
+
ON interaction_events(repository_id, created_at);
|
|
496
|
+
CREATE INDEX IF NOT EXISTS idx_interaction_events_request
|
|
497
|
+
ON interaction_events(request_id);
|
|
498
|
+
`);
|
|
499
|
+
await db.run("INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', '7')");
|
|
500
|
+
}
|
|
372
501
|
}
|
|
373
502
|
/**
|
|
374
503
|
* Compact the database — prune old data, reclaim disk space.
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -11,3 +11,7 @@ export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './loca
|
|
|
11
11
|
export type { ProjectStats } from './local-memory.js';
|
|
12
12
|
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
13
13
|
export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
|
|
14
|
+
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './lessons.js';
|
|
15
|
+
export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility } from './lessons.js';
|
|
16
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './team-store.js';
|
|
17
|
+
export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './team-store.js';
|
package/dist/storage/index.js
CHANGED
|
@@ -8,3 +8,5 @@ export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.
|
|
|
8
8
|
export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
|
|
9
9
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
|
|
10
10
|
export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
|
|
11
|
+
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId } from './lessons.js';
|
|
12
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './team-store.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type LessonState = 'candidate' | 'validated' | 'promoted' | 'rejected' | 'superseded';
|
|
2
|
+
export type LessonVisibility = 'personal' | 'team';
|
|
3
|
+
export interface LessonRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
repositoryId: string;
|
|
6
|
+
actorId?: string;
|
|
7
|
+
teamId?: string;
|
|
8
|
+
visibility: LessonVisibility;
|
|
9
|
+
state: LessonState;
|
|
10
|
+
kind: string;
|
|
11
|
+
subject: string;
|
|
12
|
+
evidence: Record<string, unknown>;
|
|
13
|
+
confidence: number;
|
|
14
|
+
source: string;
|
|
15
|
+
supersedesId?: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
repositoryName?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface InteractionEvidence {
|
|
21
|
+
tool: string;
|
|
22
|
+
outcome: 'success' | 'error' | 'rejected';
|
|
23
|
+
requestId: string;
|
|
24
|
+
taskId?: string;
|
|
25
|
+
agentId?: string;
|
|
26
|
+
deterministic?: boolean;
|
|
27
|
+
verifiedOutcome?: boolean;
|
|
28
|
+
phase?: 'request' | 'response';
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
files?: string[];
|
|
31
|
+
summary?: string;
|
|
32
|
+
/** A concise, evidence-backed proposition that could be reused beyond this call. */
|
|
33
|
+
reusableClaim?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function shouldValidateInteractionLesson(evidence: InteractionEvidence, verifiedSuccessfulOutcomes?: number): boolean;
|
|
36
|
+
export declare function recordInteractionEvidence(cwd: string, evidence: InteractionEvidence): Promise<string | null>;
|
|
37
|
+
export declare function countInteractionEvidence(cwd: string): Promise<number>;
|
|
38
|
+
export declare function getRepositoryId(cwd: string): Promise<string>;
|
|
39
|
+
export declare function recordInteractionLesson(cwd: string, evidence: InteractionEvidence): Promise<string | null>;
|
|
40
|
+
export declare function listLessons(cwd: string, limit?: number): Promise<LessonRecord[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the evidence visible to the current workspace for exploration. Cross-repository
|
|
43
|
+
* records are candidates only; getApplicableLessons remains the enforcement boundary.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listKnowledgeLessons(cwd: string, limit?: number): Promise<LessonRecord[]>;
|
|
46
|
+
export interface ApplicableLessons {
|
|
47
|
+
lessons: Array<LessonRecord & {
|
|
48
|
+
priority: number;
|
|
49
|
+
provenance: string;
|
|
50
|
+
}>;
|
|
51
|
+
conflicts: Array<{
|
|
52
|
+
subject: string;
|
|
53
|
+
lessonIds: string[];
|
|
54
|
+
}>;
|
|
55
|
+
}
|
|
56
|
+
export declare function getApplicableLessons(cwd: string, actorId?: string, teamId?: string): Promise<ApplicableLessons>;
|
|
57
|
+
export declare function transitionLesson(id: string, state: LessonState, options?: {
|
|
58
|
+
visibility?: LessonVisibility;
|
|
59
|
+
queueSync?: boolean;
|
|
60
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { openDatabase } from './db.js';
|
|
5
|
+
import { decryptLocalPayload, encryptLocalPayload } from './local-encryption.js';
|
|
6
|
+
import { loadTeamConfiguration } from './team-store.js';
|
|
7
|
+
export function shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes = 0) {
|
|
8
|
+
return evidence.outcome === 'success'
|
|
9
|
+
&& Boolean(evidence.reusableClaim?.trim())
|
|
10
|
+
&& (evidence.deterministic === true || verifiedSuccessfulOutcomes >= 3);
|
|
11
|
+
}
|
|
12
|
+
export async function recordInteractionEvidence(cwd, evidence) {
|
|
13
|
+
const db = await openDatabase();
|
|
14
|
+
if (!db)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
18
|
+
await registerRepository(db, cwd, repositoryId);
|
|
19
|
+
const config = await loadTeamConfiguration();
|
|
20
|
+
const id = `observation-${randomUUID()}`;
|
|
21
|
+
await db.run(`INSERT INTO interaction_events (
|
|
22
|
+
id, repository_id, actor_id, task_id, session_id, request_id,
|
|
23
|
+
tool_name, phase, outcome, deterministic, evidence_json, created_at
|
|
24
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, repositoryId, config?.actorId || process.env.RIGOUR_ACTOR_ID || null, evidence.taskId || null, evidence.sessionId || null, evidence.requestId, evidence.tool, evidence.phase || 'response', evidence.outcome, evidence.deterministic ? 1 : 0, await encryptLocalPayload({ files: evidence.files || [], summary: evidence.summary }), Date.now());
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await db.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function countInteractionEvidence(cwd) {
|
|
32
|
+
const db = await openDatabase();
|
|
33
|
+
if (!db)
|
|
34
|
+
return 0;
|
|
35
|
+
try {
|
|
36
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
37
|
+
const row = await db.get('SELECT COUNT(*) AS count FROM interaction_events WHERE repository_id = ?', repositoryId);
|
|
38
|
+
return Number(row?.count || 0);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await db.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function normalizeRemote(remote) {
|
|
45
|
+
return remote.trim().replace(/^git@([^:]+):/, 'https://$1/').replace(/\.git$/, '').toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
async function readGitConfig(cwd) {
|
|
48
|
+
const dotGit = path.join(cwd, '.git');
|
|
49
|
+
const stat = await fs.stat(dotGit);
|
|
50
|
+
if (stat.isDirectory())
|
|
51
|
+
return fs.readFile(path.join(dotGit, 'config'), 'utf8');
|
|
52
|
+
const pointer = await fs.readFile(dotGit, 'utf8');
|
|
53
|
+
const match = pointer.match(/^gitdir:\s*(.+)$/m);
|
|
54
|
+
if (!match)
|
|
55
|
+
throw new Error('Invalid Git directory pointer.');
|
|
56
|
+
const gitDir = path.resolve(cwd, match[1].trim());
|
|
57
|
+
try {
|
|
58
|
+
return await fs.readFile(path.join(gitDir, 'config'), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Linked worktrees usually keep the shared remote configuration here.
|
|
62
|
+
const commonDir = (await fs.readFile(path.join(gitDir, 'commondir'), 'utf8')).trim();
|
|
63
|
+
return fs.readFile(path.resolve(gitDir, commonDir, 'config'), 'utf8');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function getRepositoryId(cwd) {
|
|
67
|
+
let identity = path.resolve(cwd);
|
|
68
|
+
try {
|
|
69
|
+
const config = await readGitConfig(cwd);
|
|
70
|
+
const remote = config.match(/\[remote\s+"origin"\][\s\S]*?url\s*=\s*([^\n]+)/)?.[1];
|
|
71
|
+
if (remote)
|
|
72
|
+
identity = normalizeRemote(remote);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Non-git workspaces retain a stable path-derived local identity.
|
|
76
|
+
}
|
|
77
|
+
return createHash('sha256').update(identity).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
async function registerRepository(db, cwd, repositoryId) {
|
|
80
|
+
let canonicalUri = path.resolve(cwd);
|
|
81
|
+
try {
|
|
82
|
+
const config = await readGitConfig(cwd);
|
|
83
|
+
const remote = config.match(/\[remote\s+"origin"\][\s\S]*?url\s*=\s*([^\n]+)/)?.[1];
|
|
84
|
+
if (remote)
|
|
85
|
+
canonicalUri = normalizeRemote(remote);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Non-git workspaces use their resolved path as the canonical local identity.
|
|
89
|
+
}
|
|
90
|
+
const remoteName = canonicalUri.split('/').filter(Boolean).at(-1);
|
|
91
|
+
await db.run(`INSERT INTO repositories (id, canonical_uri, display_name, last_seen) VALUES (?, ?, ?, ?)
|
|
92
|
+
ON CONFLICT(id) DO UPDATE SET canonical_uri=excluded.canonical_uri,
|
|
93
|
+
display_name=excluded.display_name, last_seen=excluded.last_seen`, repositoryId, canonicalUri, remoteName || path.basename(cwd), Date.now());
|
|
94
|
+
}
|
|
95
|
+
async function rowToLesson(row) {
|
|
96
|
+
return {
|
|
97
|
+
id: String(row.id),
|
|
98
|
+
repositoryId: String(row.repository_id),
|
|
99
|
+
actorId: row.actor_id ? String(row.actor_id) : undefined,
|
|
100
|
+
teamId: row.team_id ? String(row.team_id) : undefined,
|
|
101
|
+
visibility: row.visibility,
|
|
102
|
+
state: row.state,
|
|
103
|
+
kind: String(row.kind),
|
|
104
|
+
subject: String(row.subject),
|
|
105
|
+
evidence: await decryptLocalPayload(String(row.evidence_json)),
|
|
106
|
+
confidence: Number(row.confidence),
|
|
107
|
+
source: String(row.source),
|
|
108
|
+
supersedesId: row.supersedes_id ? String(row.supersedes_id) : undefined,
|
|
109
|
+
createdAt: Number(row.created_at),
|
|
110
|
+
updatedAt: Number(row.updated_at),
|
|
111
|
+
repositoryName: row.repository_name ? String(row.repository_name) : undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export async function recordInteractionLesson(cwd, evidence) {
|
|
115
|
+
const db = await openDatabase();
|
|
116
|
+
if (!db)
|
|
117
|
+
return null;
|
|
118
|
+
try {
|
|
119
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
120
|
+
await registerRepository(db, cwd, repositoryId);
|
|
121
|
+
const id = `lesson-${randomUUID()}`;
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const verifiedSuccess = shouldValidateInteractionLesson(evidence, evidence.verifiedOutcome ? 3 : 0);
|
|
124
|
+
const confidence = verifiedSuccess ? 0.8 : 0.3;
|
|
125
|
+
const state = confidence >= 0.8 ? 'validated' : 'candidate';
|
|
126
|
+
const config = await loadTeamConfiguration();
|
|
127
|
+
const actorId = config?.actorId || process.env.RIGOUR_ACTOR_ID || null;
|
|
128
|
+
const priorRow = await db.get(`SELECT * FROM lessons
|
|
129
|
+
WHERE repository_id = ? AND actor_id IS ? AND visibility = 'personal'
|
|
130
|
+
AND state = 'candidate' AND kind = 'interaction' AND subject = ?
|
|
131
|
+
ORDER BY updated_at DESC LIMIT 1`, repositoryId, actorId, evidence.reusableClaim?.trim() || `interaction:${evidence.tool}`);
|
|
132
|
+
if (priorRow) {
|
|
133
|
+
const prior = await rowToLesson(priorRow);
|
|
134
|
+
const previousSuccesses = Number(prior.evidence.successfulOutcomes ?? 0);
|
|
135
|
+
const successfulOutcomes = previousSuccesses + (evidence.outcome === 'success' ? 1 : 0);
|
|
136
|
+
const previousVerified = Number(prior.evidence.verifiedSuccessfulOutcomes ?? 0);
|
|
137
|
+
const verifiedSuccessfulOutcomes = previousVerified + (evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0);
|
|
138
|
+
const repeatedValidation = shouldValidateInteractionLesson(evidence, verifiedSuccessfulOutcomes);
|
|
139
|
+
const mergedEvidence = {
|
|
140
|
+
...evidence,
|
|
141
|
+
observationCount: Number(prior.evidence.observationCount ?? 1) + 1,
|
|
142
|
+
successfulOutcomes,
|
|
143
|
+
verifiedSuccessfulOutcomes,
|
|
144
|
+
previousRequestId: prior.evidence.requestId,
|
|
145
|
+
};
|
|
146
|
+
const updated = {
|
|
147
|
+
...prior,
|
|
148
|
+
state: repeatedValidation ? 'validated' : 'candidate',
|
|
149
|
+
evidence: mergedEvidence,
|
|
150
|
+
confidence: repeatedValidation ? 0.8 : Math.min(0.7, prior.confidence + 0.15),
|
|
151
|
+
updatedAt: now,
|
|
152
|
+
};
|
|
153
|
+
await db.run('UPDATE lessons SET state = ?, evidence_json = ?, confidence = ?, updated_at = ? WHERE id = ?', updated.state, await encryptLocalPayload(updated.evidence), updated.confidence, updated.updatedAt, updated.id);
|
|
154
|
+
if (config)
|
|
155
|
+
await queueLessonSync(db, updated, now);
|
|
156
|
+
return prior.id;
|
|
157
|
+
}
|
|
158
|
+
const storedEvidence = {
|
|
159
|
+
...evidence,
|
|
160
|
+
observationCount: 1,
|
|
161
|
+
successfulOutcomes: evidence.outcome === 'success' ? 1 : 0,
|
|
162
|
+
verifiedSuccessfulOutcomes: evidence.verifiedOutcome && evidence.outcome === 'success' ? 1 : 0,
|
|
163
|
+
};
|
|
164
|
+
const encryptedEvidence = await encryptLocalPayload(storedEvidence);
|
|
165
|
+
await db.run(`INSERT INTO lessons (
|
|
166
|
+
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
167
|
+
evidence_json, confidence, source, created_at, updated_at
|
|
168
|
+
) VALUES (?, ?, ?, ?, 'personal', ?, 'interaction', ?, ?, ?, 'mcp', ?, ?)`, id, repositoryId, actorId, config?.teamId || process.env.RIGOUR_TEAM_ID || null, state, evidence.reusableClaim?.trim() || `interaction:${evidence.tool}`, encryptedEvidence, confidence, now, now);
|
|
169
|
+
if (config) {
|
|
170
|
+
const personal = await txSafeLesson(db, id);
|
|
171
|
+
if (personal)
|
|
172
|
+
await queueLessonSync(db, personal, now);
|
|
173
|
+
}
|
|
174
|
+
return id;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await db.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function queueLessonSync(db, lesson, now) {
|
|
181
|
+
await db.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
|
|
182
|
+
VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, lesson.id, await encryptLocalPayload(lesson), now);
|
|
183
|
+
}
|
|
184
|
+
export async function listLessons(cwd, limit = 100) {
|
|
185
|
+
let db;
|
|
186
|
+
try {
|
|
187
|
+
db = await openDatabase();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
if (!db)
|
|
193
|
+
return [];
|
|
194
|
+
try {
|
|
195
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
196
|
+
await registerRepository(db, cwd, repositoryId);
|
|
197
|
+
const rows = await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
198
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
199
|
+
WHERE repository_id = ?
|
|
200
|
+
ORDER BY updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
201
|
+
return await Promise.all(rows.map(rowToLesson));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await db.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Returns the evidence visible to the current workspace for exploration. Cross-repository
|
|
209
|
+
* records are candidates only; getApplicableLessons remains the enforcement boundary.
|
|
210
|
+
*/
|
|
211
|
+
export async function listKnowledgeLessons(cwd, limit = 500) {
|
|
212
|
+
let db;
|
|
213
|
+
try {
|
|
214
|
+
db = await openDatabase();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
if (!db)
|
|
220
|
+
return [];
|
|
221
|
+
try {
|
|
222
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
223
|
+
await registerRepository(db, cwd, repositoryId);
|
|
224
|
+
const config = await loadTeamConfiguration();
|
|
225
|
+
const rows = config
|
|
226
|
+
? await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
227
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
228
|
+
WHERE lessons.repository_id = ?
|
|
229
|
+
OR (lessons.visibility = 'personal' AND lessons.actor_id = ?)
|
|
230
|
+
OR (lessons.visibility = 'team' AND lessons.team_id = ? AND lessons.state = 'promoted')
|
|
231
|
+
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, config.actorId, config.teamId, limit)
|
|
232
|
+
: await db.all(`SELECT lessons.*, repositories.display_name AS repository_name
|
|
233
|
+
FROM lessons LEFT JOIN repositories ON repositories.id = lessons.repository_id
|
|
234
|
+
WHERE lessons.repository_id = ?
|
|
235
|
+
OR (lessons.visibility = 'personal' AND lessons.actor_id IS NULL)
|
|
236
|
+
ORDER BY lessons.updated_at DESC LIMIT ?`, repositoryId, limit);
|
|
237
|
+
return await Promise.all(rows.map(rowToLesson));
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await db.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
export async function getApplicableLessons(cwd, actorId, teamId) {
|
|
244
|
+
const repositoryId = await getRepositoryId(cwd);
|
|
245
|
+
const all = await listKnowledgeLessons(cwd, 500);
|
|
246
|
+
const applicable = all
|
|
247
|
+
.filter(lesson => lesson.state === 'validated' || lesson.state === 'promoted')
|
|
248
|
+
.filter(lesson => lesson.visibility === 'team' || !actorId || !lesson.actorId || lesson.actorId === actorId)
|
|
249
|
+
.filter(lesson => !teamId || !lesson.teamId || lesson.teamId === teamId)
|
|
250
|
+
.map(lesson => {
|
|
251
|
+
const priority = lesson.repositoryId === repositoryId ? 1 : lesson.visibility === 'personal' ? 2 : 3;
|
|
252
|
+
return { ...lesson, priority, provenance: `${lesson.visibility}:${lesson.source}:${lesson.id}` };
|
|
253
|
+
})
|
|
254
|
+
.sort((a, b) => a.priority - b.priority || b.confidence - a.confidence);
|
|
255
|
+
const bySubject = new Map();
|
|
256
|
+
for (const lesson of applicable)
|
|
257
|
+
bySubject.set(lesson.subject, [...(bySubject.get(lesson.subject) ?? []), lesson]);
|
|
258
|
+
const conflicts = [...bySubject.entries()]
|
|
259
|
+
.filter(([, lessons]) => new Set(lessons.map(lesson => JSON.stringify(lesson.evidence))).size > 1)
|
|
260
|
+
.map(([subject, lessons]) => ({ subject, lessonIds: lessons.map(lesson => lesson.id) }));
|
|
261
|
+
return { lessons: applicable, conflicts };
|
|
262
|
+
}
|
|
263
|
+
export async function transitionLesson(id, state, options = {}) {
|
|
264
|
+
const db = await openDatabase();
|
|
265
|
+
if (!db)
|
|
266
|
+
return false;
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
try {
|
|
269
|
+
return await db.transaction(async (tx) => {
|
|
270
|
+
const existing = await tx.get('SELECT * FROM lessons WHERE id = ?', id);
|
|
271
|
+
if (!existing)
|
|
272
|
+
return false;
|
|
273
|
+
const visibility = options.visibility ?? existing.visibility;
|
|
274
|
+
if (state === 'promoted' && visibility === 'team') {
|
|
275
|
+
const teamConfig = await loadTeamConfiguration();
|
|
276
|
+
if (!teamConfig) {
|
|
277
|
+
throw new Error('Configure PostgreSQL team mode before publishing a team lesson.');
|
|
278
|
+
}
|
|
279
|
+
const source = await rowToLesson(existing);
|
|
280
|
+
const publishedId = `lesson-${randomUUID()}`;
|
|
281
|
+
const published = {
|
|
282
|
+
...source,
|
|
283
|
+
id: publishedId,
|
|
284
|
+
actorId: source.actorId || teamConfig.actorId,
|
|
285
|
+
teamId: teamConfig.teamId,
|
|
286
|
+
visibility: 'team',
|
|
287
|
+
state: 'promoted',
|
|
288
|
+
source: `published:${source.id}`,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now,
|
|
291
|
+
};
|
|
292
|
+
await tx.run(`INSERT INTO lessons (
|
|
293
|
+
id, repository_id, actor_id, team_id, visibility, state, kind, subject,
|
|
294
|
+
evidence_json, confidence, source, supersedes_id, created_at, updated_at
|
|
295
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, published.id, published.repositoryId, published.actorId || null, published.teamId || null, published.visibility, published.state, published.kind, published.subject, await encryptLocalPayload(published.evidence), published.confidence, published.source, published.supersedesId || null, published.createdAt, published.updatedAt);
|
|
296
|
+
if (options.queueSync) {
|
|
297
|
+
await tx.run(`INSERT INTO sync_outbox (id, operation, entity_type, entity_id, payload_json, created_at)
|
|
298
|
+
VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, published.id, await encryptLocalPayload(published), now);
|
|
299
|
+
}
|
|
300
|
+
await tx.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
const result = await tx.run('UPDATE lessons SET state = ?, visibility = ?, updated_at = ? WHERE id = ?', state, visibility, now, id);
|
|
304
|
+
if (result.changes > 0)
|
|
305
|
+
await tx.run("DELETE FROM context_cache WHERE cache_type = 'semantic'");
|
|
306
|
+
if (result.changes > 0 && options.queueSync && visibility === 'team') {
|
|
307
|
+
const payload = { ...await rowToLesson(existing), state, visibility, updatedAt: now };
|
|
308
|
+
await tx.run(`INSERT INTO sync_outbox (
|
|
309
|
+
id, operation, entity_type, entity_id, payload_json, created_at
|
|
310
|
+
) VALUES (?, 'upsert', 'lesson', ?, ?, ?)`, `outbox-${randomUUID()}`, id, await encryptLocalPayload(payload), now);
|
|
311
|
+
}
|
|
312
|
+
return result.changes > 0;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
await db.close();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function txSafeLesson(db, id) {
|
|
320
|
+
const row = await db.get('SELECT * FROM lessons WHERE id = ?', id);
|
|
321
|
+
return row ? rowToLesson(row) : null;
|
|
322
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getRepositoryId, shouldValidateInteractionLesson } from './lessons.js';
|
|
6
|
+
const roots = [];
|
|
7
|
+
afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.remove(root))); });
|
|
8
|
+
describe('getRepositoryId', () => {
|
|
9
|
+
it('uses normalized origin identity instead of a basename', async () => {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-repo-id-'));
|
|
11
|
+
roots.push(root);
|
|
12
|
+
await fs.ensureDir(path.join(root, '.git'));
|
|
13
|
+
await fs.writeFile(path.join(root, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
14
|
+
const first = await getRepositoryId(root);
|
|
15
|
+
const second = await getRepositoryId(path.join(root, '..', path.basename(root)));
|
|
16
|
+
expect(first).toBe(second);
|
|
17
|
+
expect(first).toHaveLength(64);
|
|
18
|
+
});
|
|
19
|
+
it('uses the shared origin identity from a linked worktree', async () => {
|
|
20
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-worktree-id-'));
|
|
21
|
+
roots.push(root);
|
|
22
|
+
const common = path.join(root, 'common');
|
|
23
|
+
const linkedGit = path.join(common, 'worktrees', 'feature');
|
|
24
|
+
const worktree = path.join(root, 'checkout');
|
|
25
|
+
await fs.ensureDir(linkedGit);
|
|
26
|
+
await fs.ensureDir(worktree);
|
|
27
|
+
await fs.writeFile(path.join(worktree, '.git'), `gitdir: ${linkedGit}\n`);
|
|
28
|
+
await fs.writeFile(path.join(linkedGit, 'commondir'), '../..\n');
|
|
29
|
+
await fs.writeFile(path.join(common, 'config'), '[remote "origin"]\nurl = https://github.com/rigour-labs/rigour.git\n');
|
|
30
|
+
const regular = path.join(root, 'regular');
|
|
31
|
+
await fs.ensureDir(path.join(regular, '.git'));
|
|
32
|
+
await fs.writeFile(path.join(regular, '.git', 'config'), '[remote "origin"]\nurl = git@github.com:Rigour-Labs/Rigour.git\n');
|
|
33
|
+
expect(await getRepositoryId(worktree)).toBe(await getRepositoryId(regular));
|
|
34
|
+
expect(await getRepositoryId(worktree)).toHaveLength(64);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('interaction lesson validation', () => {
|
|
38
|
+
const evidence = { tool: 'rigour_context_scope', outcome: 'success', requestId: 'req-1' };
|
|
39
|
+
it('keeps repeated unverified interaction as evidence only', () => {
|
|
40
|
+
expect(shouldValidateInteractionLesson(evidence, 0)).toBe(false);
|
|
41
|
+
expect(shouldValidateInteractionLesson(evidence, 2)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('validates deterministic or repeatedly verified outcomes', () => {
|
|
44
|
+
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true, reusableClaim: 'Use scoped retrieval before edits.' }, 0)).toBe(true);
|
|
45
|
+
expect(shouldValidateInteractionLesson({ ...evidence, verifiedOutcome: true, reusableClaim: 'Run the focused test suite.' }, 3)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('does not turn a successful tool call into reusable knowledge', () => {
|
|
48
|
+
expect(shouldValidateInteractionLesson({ ...evidence, deterministic: true }, 3)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
const KEY_PATH = path.join(os.homedir(), '.rigour', 'team-cache.key');
|
|
6
|
+
async function getKey() {
|
|
7
|
+
const configured = process.env.RIGOUR_LOCAL_CACHE_KEY;
|
|
8
|
+
if (configured) {
|
|
9
|
+
const key = Buffer.from(configured, 'base64');
|
|
10
|
+
if (key.length !== 32)
|
|
11
|
+
throw new Error('RIGOUR_LOCAL_CACHE_KEY must be a base64-encoded 32-byte key.');
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const key = Buffer.from((await fs.readFile(KEY_PATH, 'utf8')).trim(), 'base64');
|
|
16
|
+
if (key.length === 32)
|
|
17
|
+
return key;
|
|
18
|
+
}
|
|
19
|
+
catch { /* create below */ }
|
|
20
|
+
const key = randomBytes(32);
|
|
21
|
+
await fs.ensureDir(path.dirname(KEY_PATH));
|
|
22
|
+
await fs.writeFile(KEY_PATH, key.toString('base64'), { mode: 0o600 });
|
|
23
|
+
await fs.chmod(KEY_PATH, 0o600);
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
26
|
+
export async function encryptLocalPayload(value) {
|
|
27
|
+
const key = await getKey();
|
|
28
|
+
const iv = randomBytes(12);
|
|
29
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
30
|
+
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
31
|
+
return `enc:v1:${iv.toString('base64')}:${cipher.getAuthTag().toString('base64')}:${ciphertext.toString('base64')}`;
|
|
32
|
+
}
|
|
33
|
+
export async function decryptLocalPayload(value) {
|
|
34
|
+
if (!value.startsWith('enc:v1:'))
|
|
35
|
+
return JSON.parse(value);
|
|
36
|
+
const [, , ivValue, tagValue, ciphertextValue] = value.split(':');
|
|
37
|
+
const decipher = createDecipheriv('aes-256-gcm', await getKey(), Buffer.from(ivValue, 'base64'));
|
|
38
|
+
decipher.setAuthTag(Buffer.from(tagValue, 'base64'));
|
|
39
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(ciphertextValue, 'base64')), decipher.final()]);
|
|
40
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
41
|
+
}
|