sneakoscope 3.1.9 → 3.1.11
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/README.md +7 -6
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/.sks-build-stamp.json +4 -4
- package/dist/bin/sks.js +1 -1
- package/dist/commands/doctor.js +103 -4
- package/dist/core/codex-app/codex-agent-role-sync.js +6 -6
- package/dist/core/codex-app/codex-skill-sync.js +37 -2
- package/dist/core/codex-native/core-skill-integrity.js +6 -1
- package/dist/core/codex-native/core-skill-manifest.js +1 -1
- package/dist/core/codex-native/native-capability-postcheck.js +143 -15
- package/dist/core/codex-native/native-capability-repair-matrix.js +1 -1
- package/dist/core/codex-native/project-skill-dedupe.js +18 -3
- package/dist/core/codex-native/skill-registry-ledger.js +9 -2
- package/dist/core/commands/basic-cli.js +3 -1
- package/dist/core/commands/research-command.js +1 -1
- package/dist/core/config/managed-config-merge.js +59 -10
- package/dist/core/config/secret-preservation.js +145 -37
- package/dist/core/doctor/doctor-codex-startup-repair.js +269 -0
- package/dist/core/doctor/doctor-context7-repair.js +116 -0
- package/dist/core/fsx.js +1 -1
- package/dist/core/init.js +31 -6
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-capability.js +1 -1
- package/package.json +7 -1
|
@@ -17,8 +17,16 @@ export async function dedupeProjectSkills(input) {
|
|
|
17
17
|
const userEntries = group.filter((entry) => !entry.managed_by_sks);
|
|
18
18
|
const managedEntries = group.filter((entry) => entry.managed_by_sks);
|
|
19
19
|
if (userEntries.length > 0 && managedEntries.length > 0) {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const keepUser = userEntries[0];
|
|
21
|
+
if (keepUser)
|
|
22
|
+
actions.push(actionRow(canonical, 'kept', keepUser, null, 'user-authored skill preserved'));
|
|
23
|
+
const shouldMoveUserDuplicates = fix && yes && input.quarantineUserDuplicates === true;
|
|
24
|
+
for (const duplicateUser of userEntries.slice(1)) {
|
|
25
|
+
const quarantine = shouldMoveUserDuplicates ? await quarantineSkill(root, canonical, duplicateUser, 'user-authored duplicate skill') : null;
|
|
26
|
+
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', duplicateUser, quarantine, 'user-authored duplicate skill requires --quarantine-user-duplicates --yes'));
|
|
27
|
+
}
|
|
28
|
+
if (userEntries.length > 1 && !shouldMoveUserDuplicates)
|
|
29
|
+
unresolvedUserDuplicates.push(canonical);
|
|
22
30
|
for (const managed of managedEntries) {
|
|
23
31
|
const quarantine = await maybeQuarantine(root, canonical, managed, fix, 'managed collision with user-authored skill');
|
|
24
32
|
actions.push(actionRow(canonical, quarantine ? 'quarantined' : 'reported', managed, quarantine, 'managed collision with user-authored skill'));
|
|
@@ -49,7 +57,11 @@ export async function dedupeProjectSkills(input) {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
const duplicateNames = [...new Set(actions.filter((action) => action.action !== 'kept').map((action) => action.canonical_name))].sort();
|
|
52
|
-
const
|
|
60
|
+
const afterLedger = await buildSkillRegistryLedger({ root, reportPath: null });
|
|
61
|
+
const blockers = [
|
|
62
|
+
...unresolvedUserDuplicates.map((name) => `user_duplicate_requires_confirmation:${name}`),
|
|
63
|
+
...afterLedger.duplicate_active_canonical_names.map((name) => `duplicate_active_skill_name:${name}`)
|
|
64
|
+
];
|
|
53
65
|
const report = {
|
|
54
66
|
schema: 'sks.project-skill-dedupe.v1',
|
|
55
67
|
generated_at: nowIso(),
|
|
@@ -58,6 +70,9 @@ export async function dedupeProjectSkills(input) {
|
|
|
58
70
|
fix,
|
|
59
71
|
yes,
|
|
60
72
|
actions,
|
|
73
|
+
active_unique_by_canonical_name: afterLedger.active_unique_by_canonical_name,
|
|
74
|
+
active_entries: afterLedger.active_entries,
|
|
75
|
+
duplicate_active_canonical_names: afterLedger.duplicate_active_canonical_names,
|
|
61
76
|
duplicate_canonical_names: duplicateNames,
|
|
62
77
|
unresolved_user_duplicates: unresolvedUserDuplicates,
|
|
63
78
|
blockers
|
|
@@ -56,13 +56,20 @@ export async function buildSkillRegistryLedger(input) {
|
|
|
56
56
|
entry.status = entry.status === 'user-owned' ? 'duplicate' : 'duplicate';
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
const
|
|
59
|
+
const activeEntries = entries.filter((entry) => entry.status !== 'quarantined');
|
|
60
|
+
const activeGrouped = groupByCanonical(activeEntries);
|
|
61
|
+
const duplicateActiveNames = [...activeGrouped.entries()].filter(([, group]) => group.length > 1).map(([name]) => name).sort();
|
|
62
|
+
const activeUnique = duplicateActiveNames.length === 0;
|
|
63
|
+
const blockers = duplicateActiveNames.map((name) => `duplicate_active_skill_name:${name}`);
|
|
60
64
|
const ledger = {
|
|
61
65
|
schema: 'sks.skill-registry-ledger.v1',
|
|
62
66
|
generated_at: nowIso(),
|
|
63
|
-
ok:
|
|
67
|
+
ok: activeUnique,
|
|
64
68
|
root,
|
|
65
69
|
entries,
|
|
70
|
+
active_unique_by_canonical_name: activeUnique,
|
|
71
|
+
active_entries: activeEntries,
|
|
72
|
+
duplicate_active_canonical_names: duplicateActiveNames,
|
|
66
73
|
duplicate_canonical_names: duplicates,
|
|
67
74
|
blockers
|
|
68
75
|
};
|
|
@@ -185,7 +185,9 @@ export async function initCommand(args = []) {
|
|
|
185
185
|
export async function fixPathCommand(args = []) {
|
|
186
186
|
const root = await projectRoot();
|
|
187
187
|
const installScope = installScopeFromArgs(args);
|
|
188
|
-
await
|
|
188
|
+
await withSecretPreservationGuard(root, 'fix-path-command', async () => {
|
|
189
|
+
await initProject(root, { installScope, localOnly: flag(args, '--local-only'), globalCommand: 'sks', force: true });
|
|
190
|
+
});
|
|
189
191
|
const result = {
|
|
190
192
|
schema: 'sks.fix-path.v1',
|
|
191
193
|
ok: true,
|
|
@@ -180,7 +180,7 @@ async function researchRun(args) {
|
|
|
180
180
|
await runResearchCycle(dir, researchWorkGraph, { cycle: 0, status: mock ? 'mock_native_orchestrator_planned' : 'native_orchestrator_planned' });
|
|
181
181
|
await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
|
|
182
182
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
|
|
183
|
-
const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph:
|
|
183
|
+
const nativeAgentRun = await runNativeAgentOrchestrator({ root, missionId: id, route: flag(args, '--autoresearch') ? '$AutoResearch' : '$Research', prompt: mission.prompt || plan.prompt || 'Research run', backend: mock ? 'fake' : 'codex-sdk', mock, agents: requestedAgents, targetActiveSlots, desiredWorkItemCount: effectiveDesiredWorkItemCount, minimumWorkItems: effectiveMinimumWorkItems, maxQueueExpansion, concurrency: Math.min(requestedAgents, 5), readonly: true, profile, writeMode: writeMode, applyPatches: false, dryRunPatches, maxWriteAgents, roster: plan.native_agent_plan, routeCommand: 'sks research run', routeBlackboxKind: 'actual_research_command', narutoWorkGraph: researchWorkGraph });
|
|
184
184
|
await writeJsonAtomic(path.join(dir, 'research-native-agent-run.json'), nativeAgentRun);
|
|
185
185
|
await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.native_agents.completed', backend: nativeAgentRun.backend, ok: nativeAgentRun.ok, proof: nativeAgentRun.proof?.status });
|
|
186
186
|
if (!nativeAgentRun.ok) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
3
|
+
import { ensureDir, nowIso, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
4
4
|
import { isProtectedSecretKey, PROTECTED_SECRET_KEYS } from './supabase-secret-preservation.js';
|
|
5
5
|
export async function writeManagedJsonConfig(file, current, managed) {
|
|
6
6
|
const next = safeMergeObject(current, managed);
|
|
@@ -23,13 +23,14 @@ export async function writeManagedEnvConfig(file, currentText, managedLines) {
|
|
|
23
23
|
const next = additions.length ? `${String(currentText || '').replace(/\s*$/, '\n')}${additions.join('\n')}\n` : String(currentText || '');
|
|
24
24
|
return writeMergedText(file, currentText, next, 'env', protectedKeysInText(currentText));
|
|
25
25
|
}
|
|
26
|
-
export function safeMergeObject(current, managed) {
|
|
26
|
+
export function safeMergeObject(current, managed, prefix = '') {
|
|
27
27
|
const out = { ...current };
|
|
28
28
|
for (const [key, value] of Object.entries(managed)) {
|
|
29
|
-
|
|
29
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
30
|
+
if (isProtectedSecretKey(dotted) && current[key] != null)
|
|
30
31
|
continue;
|
|
31
32
|
if (isPlainObject(value) && isPlainObject(current[key]))
|
|
32
|
-
out[key] = safeMergeObject(current[key], value);
|
|
33
|
+
out[key] = safeMergeObject(current[key], value, dotted);
|
|
33
34
|
else
|
|
34
35
|
out[key] = value;
|
|
35
36
|
}
|
|
@@ -51,11 +52,23 @@ function upsertTomlBlockPreservingSecrets(text, block) {
|
|
|
51
52
|
break;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const existingBody = lines.slice(start + 1, end);
|
|
56
|
+
const nextBody = [...existingBody];
|
|
57
|
+
for (const managedLine of blockLines.slice(1)) {
|
|
58
|
+
const managedKey = managedLine.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
59
|
+
if (!managedKey) {
|
|
60
|
+
if (!nextBody.includes(managedLine))
|
|
61
|
+
nextBody.push(managedLine);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const existingIndex = nextBody.findIndex((line) => (line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '') === managedKey);
|
|
65
|
+
const protectedLine = isProtectedSecretKey(`${header}.${managedKey}`) || isProtectedSecretKey(managedKey);
|
|
66
|
+
if (existingIndex === -1)
|
|
67
|
+
nextBody.push(managedLine);
|
|
68
|
+
else if (!protectedLine)
|
|
69
|
+
nextBody[existingIndex] = managedLine;
|
|
70
|
+
}
|
|
71
|
+
lines.splice(start, end - start, blockLines[0] || `[${header}]`, ...nextBody);
|
|
59
72
|
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
60
73
|
}
|
|
61
74
|
async function writeMergedText(file, before, after, format, preserved) {
|
|
@@ -68,6 +81,7 @@ async function writeMergedText(file, before, after, format, preserved) {
|
|
|
68
81
|
}
|
|
69
82
|
await writeTextAtomic(file, after);
|
|
70
83
|
}
|
|
84
|
+
const preservedSecretLineHashes = protectedSecretLineHashes(before);
|
|
71
85
|
return {
|
|
72
86
|
schema: 'sks.managed-config-merge.v1',
|
|
73
87
|
generated_at: nowIso(),
|
|
@@ -77,6 +91,8 @@ async function writeMergedText(file, before, after, format, preserved) {
|
|
|
77
91
|
changed: before !== after,
|
|
78
92
|
backup_path: backupPath,
|
|
79
93
|
protected_keys_preserved: preserved,
|
|
94
|
+
preserved_secret_lines_sha256: preservedSecretLineHashes,
|
|
95
|
+
idempotent: before === after || protectedSecretLineHashes(after).every((hash) => preservedSecretLineHashes.includes(hash)),
|
|
80
96
|
blockers: []
|
|
81
97
|
};
|
|
82
98
|
}
|
|
@@ -88,7 +104,40 @@ function protectedKeysPresent(value) {
|
|
|
88
104
|
return found;
|
|
89
105
|
}
|
|
90
106
|
function protectedKeysInText(text) {
|
|
91
|
-
|
|
107
|
+
const found = new Set();
|
|
108
|
+
for (const key of PROTECTED_SECRET_KEYS) {
|
|
109
|
+
if (new RegExp(`(^|\\n)\\s*(?:export\\s+)?${String(key).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`).test(text))
|
|
110
|
+
found.add(String(key));
|
|
111
|
+
}
|
|
112
|
+
let section = '';
|
|
113
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
114
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
115
|
+
if (sectionMatch) {
|
|
116
|
+
section = String(sectionMatch[1] || '');
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const key = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
120
|
+
if (key && section && isProtectedSecretKey(`${section}.${key}`))
|
|
121
|
+
found.add(`${section}.${key}`);
|
|
122
|
+
}
|
|
123
|
+
return [...found].sort();
|
|
124
|
+
}
|
|
125
|
+
function protectedSecretLineHashes(text) {
|
|
126
|
+
const hashes = [];
|
|
127
|
+
let section = '';
|
|
128
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
129
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
130
|
+
if (sectionMatch) {
|
|
131
|
+
section = String(sectionMatch[1] || '');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const key = line.match(/^\s*(?:export\s+)?([A-Za-z0-9_.-]+)\s*=/)?.[1] || '';
|
|
135
|
+
if (!key)
|
|
136
|
+
continue;
|
|
137
|
+
if (isProtectedSecretKey(key) || (section && isProtectedSecretKey(`${section}.${key}`)))
|
|
138
|
+
hashes.push(sha256(line));
|
|
139
|
+
}
|
|
140
|
+
return hashes.sort();
|
|
92
141
|
}
|
|
93
142
|
function lookupPath(value, dotted) {
|
|
94
143
|
let current = value;
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { ensureDir, nowIso, readJson, readText, sha256, writeJsonAtomic } from '../fsx.js';
|
|
5
5
|
import { PROTECTED_SECRET_KEYS, PROTECTED_SUPABASE_ENV_KEYS } from './supabase-secret-preservation.js';
|
|
6
|
+
const activeGuardRoots = new Set();
|
|
6
7
|
export async function captureSecretPreservationSnapshot(input) {
|
|
7
8
|
const root = path.resolve(input.root);
|
|
8
9
|
const sources = secretSources(root);
|
|
@@ -31,56 +32,83 @@ export async function captureSecretPreservationSnapshot(input) {
|
|
|
31
32
|
}
|
|
32
33
|
export async function withSecretPreservationGuard(root, operationName, fn) {
|
|
33
34
|
const resolvedRoot = path.resolve(root);
|
|
35
|
+
if (activeGuardRoots.has(resolvedRoot))
|
|
36
|
+
return fn();
|
|
37
|
+
activeGuardRoots.add(resolvedRoot);
|
|
34
38
|
const reportDir = path.join(resolvedRoot, '.sneakoscope', 'reports');
|
|
35
39
|
await ensureDir(reportDir);
|
|
36
40
|
const beforePath = path.join(reportDir, 'secret-preservation-before.json');
|
|
37
41
|
const afterPath = path.join(reportDir, 'secret-preservation-after.json');
|
|
38
42
|
const guardPath = path.join(reportDir, 'secret-preservation-guard.json');
|
|
39
43
|
const before = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: beforePath });
|
|
44
|
+
const backup = await backupSecretBearingSources(resolvedRoot, operationName, before);
|
|
40
45
|
let result;
|
|
46
|
+
let operationError = null;
|
|
41
47
|
try {
|
|
42
48
|
result = await fn();
|
|
43
49
|
}
|
|
44
50
|
catch (err) {
|
|
45
|
-
|
|
46
|
-
schema: 'sks.secret-preservation-guard.v1',
|
|
47
|
-
generated_at: nowIso(),
|
|
48
|
-
ok: false,
|
|
49
|
-
operation: operationName,
|
|
50
|
-
before_path: beforePath,
|
|
51
|
-
after_path: null,
|
|
52
|
-
restored_keys_count: 0,
|
|
53
|
-
missing_after: [],
|
|
54
|
-
raw_values_recorded: false,
|
|
55
|
-
operation_error: err instanceof Error ? err.message : String(err)
|
|
56
|
-
}).catch(() => undefined);
|
|
57
|
-
throw err;
|
|
51
|
+
operationError = err;
|
|
58
52
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
53
|
+
try {
|
|
54
|
+
const after = await captureSecretPreservationSnapshot({ root: resolvedRoot, artifactPath: afterPath });
|
|
55
|
+
const changedOrMissing = changedOrMissingProtectedSecrets(before, after);
|
|
56
|
+
let rollbackAttempted = false;
|
|
57
|
+
let rollbackOk = false;
|
|
58
|
+
let restoredKeysCount = 0;
|
|
59
|
+
if (changedOrMissing.length) {
|
|
60
|
+
rollbackAttempted = true;
|
|
61
|
+
await restoreChangedSecretSources(changedOrMissing, backup.bySource);
|
|
62
|
+
const restored = await captureSecretPreservationSnapshot({
|
|
63
|
+
root: resolvedRoot,
|
|
64
|
+
artifactPath: path.join(reportDir, 'secret-preservation-after-restore.json')
|
|
65
|
+
});
|
|
66
|
+
const remaining = changedOrMissingProtectedSecrets(before, restored);
|
|
67
|
+
rollbackOk = remaining.length === 0;
|
|
68
|
+
restoredKeysCount = rollbackOk ? changedOrMissing.length : 0;
|
|
69
|
+
if (!rollbackOk) {
|
|
70
|
+
const failedReport = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, false, backup.paths);
|
|
71
|
+
await writeJsonAtomic(guardPath, operationError ? { ...failedReport, ok: false, operation_error: sanitizeErrorMessage(operationError) } : failedReport).catch(() => undefined);
|
|
72
|
+
throw new Error(`secret_preservation_rollback_failed:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const report = guardReport(operationName, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackAttempted ? rollbackOk : true, backup.paths);
|
|
76
|
+
if (operationError) {
|
|
77
|
+
report.ok = false;
|
|
78
|
+
report.operation_error = sanitizeErrorMessage(operationError);
|
|
79
|
+
}
|
|
80
|
+
await writeJsonAtomic(guardPath, report).catch(() => undefined);
|
|
81
|
+
if (operationError)
|
|
82
|
+
throw operationError;
|
|
83
|
+
if (operationName === 'doctor-fix' && rollbackAttempted) {
|
|
84
|
+
throw new Error(`secret_preservation_restored:${changedOrMissing.map((item) => `${safeSourceForError(resolvedRoot, item.source)}:${item.key}:${item.reason}`).join(',')}`);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
activeGuardRoots.delete(resolvedRoot);
|
|
75
90
|
}
|
|
76
|
-
return result;
|
|
77
91
|
}
|
|
78
92
|
export function missingProtectedSecrets(before, after) {
|
|
93
|
+
return changedOrMissingProtectedSecrets(before, after)
|
|
94
|
+
.filter((item) => item.reason === 'missing')
|
|
95
|
+
.map((item) => ({ key: item.key, source: item.source }));
|
|
96
|
+
}
|
|
97
|
+
export function changedOrMissingProtectedSecrets(before, after) {
|
|
79
98
|
const afterMap = new Map(after.fingerprints.filter((fp) => fp.present).map((fp) => [`${fp.source}\0${fp.key}`, fp]));
|
|
80
99
|
return before.fingerprints
|
|
81
100
|
.filter((fp) => fp.present && fp.value_sha256)
|
|
82
|
-
.
|
|
83
|
-
|
|
101
|
+
.map((fp) => {
|
|
102
|
+
const afterFp = afterMap.get(`${fp.source}\0${fp.key}`);
|
|
103
|
+
if (!afterFp) {
|
|
104
|
+
return { key: fp.key, source: fp.source, before_sha256: fp.value_sha256, after_sha256: null, reason: 'missing' };
|
|
105
|
+
}
|
|
106
|
+
if (afterFp.value_sha256 !== fp.value_sha256) {
|
|
107
|
+
return { key: fp.key, source: fp.source, before_sha256: fp.value_sha256, after_sha256: afterFp.value_sha256, reason: 'changed' };
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
})
|
|
111
|
+
.filter((item) => Boolean(item));
|
|
84
112
|
}
|
|
85
113
|
function secretSources(root) {
|
|
86
114
|
const home = process.env.HOME || os.homedir();
|
|
@@ -90,8 +118,10 @@ function secretSources(root) {
|
|
|
90
118
|
'.env.development',
|
|
91
119
|
'.env.production',
|
|
92
120
|
'.sneakoscope/config.json',
|
|
93
|
-
'.codex/config.toml'
|
|
94
|
-
|
|
121
|
+
'.codex/config.toml',
|
|
122
|
+
'.cursor/mcp.json',
|
|
123
|
+
'mcp.json'
|
|
124
|
+
].map((rel) => path.join(root, rel)).concat(path.join(home, '.codex', 'config.toml'), path.join(home, '.config', 'sks', 'config.json'));
|
|
95
125
|
}
|
|
96
126
|
function fingerprintsFromText(text, source) {
|
|
97
127
|
const rows = [];
|
|
@@ -101,6 +131,7 @@ function fingerprintsFromText(text, source) {
|
|
|
101
131
|
continue;
|
|
102
132
|
rows.push(fingerprint(String(key), source, value));
|
|
103
133
|
}
|
|
134
|
+
rows.push(...fingerprintsFromTomlSections(text, source));
|
|
104
135
|
for (const envKey of PROTECTED_SUPABASE_ENV_KEYS) {
|
|
105
136
|
const value = readAssignment(text, envKey);
|
|
106
137
|
if (value)
|
|
@@ -118,6 +149,25 @@ function fingerprintsFromObject(value, source) {
|
|
|
118
149
|
}
|
|
119
150
|
return rows;
|
|
120
151
|
}
|
|
152
|
+
function fingerprintsFromTomlSections(text, source) {
|
|
153
|
+
const rows = [];
|
|
154
|
+
let section = '';
|
|
155
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
156
|
+
const sectionMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
157
|
+
if (sectionMatch) {
|
|
158
|
+
section = String(sectionMatch[1] || '').trim();
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const kv = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$/);
|
|
162
|
+
if (!kv || !section)
|
|
163
|
+
continue;
|
|
164
|
+
const key = `${section}.${kv[1]}`;
|
|
165
|
+
if (!PROTECTED_SECRET_KEYS.includes(key))
|
|
166
|
+
continue;
|
|
167
|
+
rows.push(fingerprint(key, source, unquote(String(kv[2] || ''))));
|
|
168
|
+
}
|
|
169
|
+
return rows;
|
|
170
|
+
}
|
|
121
171
|
function fingerprint(key, source, value) {
|
|
122
172
|
return {
|
|
123
173
|
key,
|
|
@@ -143,9 +193,7 @@ function redactPreview(value) {
|
|
|
143
193
|
const text = String(value || '');
|
|
144
194
|
if (!text)
|
|
145
195
|
return '';
|
|
146
|
-
|
|
147
|
-
const tail = text.length > 6 ? text.slice(-3) : '';
|
|
148
|
-
return `${head}...${tail || 'redacted'}(${text.length})`;
|
|
196
|
+
return `sha256:${sha256(text).slice(0, 12)}(${text.length})`;
|
|
149
197
|
}
|
|
150
198
|
function flattenObject(value, prefix = '') {
|
|
151
199
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -166,4 +214,64 @@ function dedupeFingerprints(fingerprints) {
|
|
|
166
214
|
byKey.set(`${fp.source}\0${fp.key}`, fp);
|
|
167
215
|
return [...byKey.values()].sort((a, b) => a.source.localeCompare(b.source) || a.key.localeCompare(b.key));
|
|
168
216
|
}
|
|
217
|
+
function guardReport(operation, beforePath, afterPath, changedOrMissing, restoredKeysCount, rollbackAttempted, rollbackOk, backupPaths) {
|
|
218
|
+
return {
|
|
219
|
+
schema: 'sks.secret-preservation-guard.v1',
|
|
220
|
+
generated_at: nowIso(),
|
|
221
|
+
ok: changedOrMissing.length === 0 || rollbackOk,
|
|
222
|
+
operation,
|
|
223
|
+
before_path: beforePath,
|
|
224
|
+
after_path: afterPath,
|
|
225
|
+
restored_keys_count: restoredKeysCount,
|
|
226
|
+
changed_or_missing: changedOrMissing,
|
|
227
|
+
missing_after: changedOrMissing.filter((item) => item.reason === 'missing').map((item) => ({ key: item.key, source: item.source })),
|
|
228
|
+
rollback_attempted: rollbackAttempted,
|
|
229
|
+
rollback_ok: rollbackOk,
|
|
230
|
+
backup_paths: backupPaths,
|
|
231
|
+
raw_values_recorded: false
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function backupSecretBearingSources(root, operationName, snapshot) {
|
|
235
|
+
const bySource = new Map();
|
|
236
|
+
const sources = [...new Set(snapshot.fingerprints.filter((fp) => fp.present).map((fp) => fp.source))];
|
|
237
|
+
if (!sources.length)
|
|
238
|
+
return { bySource, paths: [] };
|
|
239
|
+
const backupRoot = path.join(root, '.sneakoscope', 'backups', 'secrets', sanitizeSegment(operationName), new Date().toISOString().replace(/[:.]/g, '-'));
|
|
240
|
+
for (const source of sources) {
|
|
241
|
+
const backupPath = path.join(backupRoot, sanitizeSourcePath(root, source));
|
|
242
|
+
await ensureDir(path.dirname(backupPath));
|
|
243
|
+
await fs.copyFile(source, backupPath);
|
|
244
|
+
bySource.set(source, backupPath);
|
|
245
|
+
}
|
|
246
|
+
return { bySource, paths: [...bySource.values()] };
|
|
247
|
+
}
|
|
248
|
+
async function restoreChangedSecretSources(changedOrMissing, backups) {
|
|
249
|
+
for (const source of [...new Set(changedOrMissing.map((item) => item.source))]) {
|
|
250
|
+
const backup = backups.get(source);
|
|
251
|
+
if (!backup)
|
|
252
|
+
continue;
|
|
253
|
+
await ensureDir(path.dirname(source));
|
|
254
|
+
await fs.copyFile(backup, source);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function sanitizeSegment(value) {
|
|
258
|
+
return String(value || 'operation').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'operation';
|
|
259
|
+
}
|
|
260
|
+
function sanitizeSourcePath(root, source) {
|
|
261
|
+
return safeSourceForError(root, source).replace(/[^A-Za-z0-9._/-]+/g, '_').replace(/^\/+/, '');
|
|
262
|
+
}
|
|
263
|
+
function safeSourceForError(root, source) {
|
|
264
|
+
const rel = path.relative(root, source);
|
|
265
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
|
|
266
|
+
return rel;
|
|
267
|
+
const home = process.env.HOME || os.homedir();
|
|
268
|
+
const homeRel = path.relative(home, source);
|
|
269
|
+
if (homeRel && !homeRel.startsWith('..') && !path.isAbsolute(homeRel))
|
|
270
|
+
return `~/${homeRel}`;
|
|
271
|
+
return path.basename(source);
|
|
272
|
+
}
|
|
273
|
+
function sanitizeErrorMessage(err) {
|
|
274
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
275
|
+
return message.replace(/([A-Za-z0-9_]*(?:SECRET|TOKEN|KEY|PASSWORD)[A-Za-z0-9_]*=)[^\s,;]+/gi, '$1<redacted>');
|
|
276
|
+
}
|
|
169
277
|
//# sourceMappingURL=secret-preservation.js.map
|