sneakoscope 3.1.9 → 3.1.10
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 +6 -5
- 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 +27 -2
- 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/fsx.js +1 -1
- package/dist/core/init.js +31 -6
- package/dist/core/version.js +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -35,14 +35,15 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
|
|
|
35
35
|
|
|
36
36
|
## 🚀 Current Release
|
|
37
37
|
|
|
38
|
-
SKS **3.1.
|
|
38
|
+
SKS **3.1.10** is a release-ready hardening pass for release wiring parity, immutable core skills, duplicate skill prevention, native capability postchecks, and protected secret rollback.
|
|
39
39
|
|
|
40
|
-
What changed in 3.1.
|
|
40
|
+
What changed in 3.1.10:
|
|
41
41
|
|
|
42
42
|
- **Core SKS skills are content-addressed and immutable.** The eight built-in route skills now have a manifest and no-drift gates; setup/update/doctor may install missing managed copies or restore corrupted managed copies, but they do not overwrite user-authored collisions.
|
|
43
|
-
- **
|
|
44
|
-
-
|
|
45
|
-
-
|
|
43
|
+
- **Release wiring is self-checking.** `release:gate-script-parity`, `release:wiring-3110-blackbox`, and `sks:3110-all-feature-regression` prove required ids, package scripts, release gates, source scripts, and built dist targets stay aligned.
|
|
44
|
+
- **Duplicate skills are detected and repaired safely.** Canonical skill names collapse variants such as `Loop`, `loop`, and `loop/SKILL.md`; SKS-managed duplicates can be quarantined automatically, while user-authored duplicates require explicit confirmation and produce active-name proof.
|
|
45
|
+
- **`sks doctor --fix` reports native capability truthfully.** Image generation, image follow-up edit paths, Computer Use, Chrome/web review, app screenshots, app handoff, and image path exposure now run capability-specific postchecks; manual-only and fallback surfaces do not become `verified`.
|
|
46
|
+
- **Supabase keys survive setup/update/doctor.** Protected secret surfaces are fingerprinted before and after guarded operations; missing or changed values are restored from backup or hard-fail without writing raw values to reports.
|
|
46
47
|
|
|
47
48
|
SKS 3.0.0 was the parallel-runtime stabilization release. The whole live-swarm experience — what you actually *see* while 5, 20, or 100 workers run — was rebuilt and proven end-to-end.
|
|
48
49
|
|
|
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
|
|
|
4
4
|
fn main() {
|
|
5
5
|
let mut args = std::env::args().skip(1);
|
|
6
6
|
match args.next().as_deref() {
|
|
7
|
-
Some("--version") => println!("sks-rs 3.1.
|
|
7
|
+
Some("--version") => println!("sks-rs 3.1.10"),
|
|
8
8
|
Some("compact-info") => {
|
|
9
9
|
let mut input = String::new();
|
|
10
10
|
let _ = io::stdin().read_to_string(&mut input);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "sks.dist-build-stamp.v1",
|
|
3
3
|
"package_name": "sneakoscope",
|
|
4
|
-
"package_version": "3.1.
|
|
5
|
-
"source_digest": "
|
|
6
|
-
"source_file_count":
|
|
7
|
-
"built_at_source_time":
|
|
4
|
+
"package_version": "3.1.10",
|
|
5
|
+
"source_digest": "57a7f5ee1f3ac797f46e3dfd59365b1e147e4637a8ddd18fde2cbee802fec0ac",
|
|
6
|
+
"source_file_count": 2609,
|
|
7
|
+
"built_at_source_time": 1781550268084
|
|
8
8
|
}
|
package/dist/bin/sks.js
CHANGED
package/dist/commands/doctor.js
CHANGED
|
@@ -29,8 +29,15 @@ import { buildCodexNativeFeatureMatrix } from '../core/codex-native/codex-native
|
|
|
29
29
|
import { repairCodexNativeManagedAssets } from '../core/codex-native/codex-native-repair-transaction.js';
|
|
30
30
|
import { runDoctorNativeCapabilityRepair } from '../core/doctor/doctor-native-capability-repair.js';
|
|
31
31
|
import { runDoctorCommandAliasCleanup } from '../core/doctor/command-alias-cleanup.js';
|
|
32
|
+
import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
|
|
32
33
|
export async function run(_command, args = []) {
|
|
34
|
+
const root = await projectRoot();
|
|
33
35
|
const doctorFix = flag(args, '--fix');
|
|
36
|
+
if (doctorFix)
|
|
37
|
+
return withSecretPreservationGuard(root, 'doctor-fix', () => runDoctor(args, root, doctorFix));
|
|
38
|
+
return runDoctor(args, root, doctorFix);
|
|
39
|
+
}
|
|
40
|
+
async function runDoctor(args = [], root, doctorFix) {
|
|
34
41
|
let setupRepair = null;
|
|
35
42
|
const sksUpdate = doctorFix
|
|
36
43
|
? {
|
|
@@ -70,7 +77,6 @@ export async function run(_command, args = []) {
|
|
|
70
77
|
: await ensureGlobalCodexFastModeDuringInstall().catch((err) => ({ status: 'failed', error: err?.message || String(err) }))
|
|
71
78
|
};
|
|
72
79
|
}
|
|
73
|
-
const root = await projectRoot();
|
|
74
80
|
const commandAliasCleanup = await runDoctorCommandAliasCleanup({
|
|
75
81
|
root,
|
|
76
82
|
fix: doctorFix
|
|
@@ -397,6 +403,12 @@ export async function run(_command, args = []) {
|
|
|
397
403
|
console.log(` app screenshot: ${nativeCapabilityStatus(nativeCapabilityRows, 'codex_app_screenshot', 'degraded')}`);
|
|
398
404
|
console.log(` app handoff: ${nativeCapabilityStatus(nativeCapabilityRows, 'app_handoff', 'unavailable')}`);
|
|
399
405
|
console.log(` image path exposure: ${nativeCapabilityStatus(nativeCapabilityRows, 'image_path_exposure', 'fallback')}`);
|
|
406
|
+
const nativeManualActions = uniqueNativeManualActions(nativeCapabilityRows);
|
|
407
|
+
if (nativeManualActions.length) {
|
|
408
|
+
console.log(' manual next actions:');
|
|
409
|
+
for (const action of nativeManualActions)
|
|
410
|
+
console.log(` - ${action}`);
|
|
411
|
+
}
|
|
400
412
|
console.log('SKS Skills:');
|
|
401
413
|
console.log(` core skills: ${doctorSkillStatus(doctorNativeCapabilityRepair?.core_skills)}`);
|
|
402
414
|
console.log(` duplicate project skills: ${doctorDedupeStatus(doctorNativeCapabilityRepair?.skill_dedupe)}`);
|
|
@@ -408,7 +420,7 @@ export async function run(_command, args = []) {
|
|
|
408
420
|
console.log(` report: ${commandAliasCleanup.report_path}`);
|
|
409
421
|
console.log('Secret preservation:');
|
|
410
422
|
console.log(` Supabase keys: ${doctorNativeCapabilityRepair?.ok === false && String((doctorNativeCapabilityRepair?.blockers || []).join(' ')).includes('secret_preservation_failed') ? 'blocked' : 'preserved'}`);
|
|
411
|
-
console.log(' secret values:
|
|
423
|
+
console.log(' raw secret values: never recorded');
|
|
412
424
|
console.log(` migration journal: ${doctorNativeCapabilityRepair?.secret_preservation_guard || '.sneakoscope/reports/secret-preservation-guard.json'}`);
|
|
413
425
|
console.log('Codex App Harness:');
|
|
414
426
|
console.log(` plugins: ${codexAppHarnessMatrix.app_features?.plugin_json ? 'ok' : 'degraded'}`);
|
|
@@ -540,6 +552,13 @@ function nativeCapabilityStatus(rows, id, fallback) {
|
|
|
540
552
|
return fallback;
|
|
541
553
|
if (row.after === 'verified' || row.before === 'verified')
|
|
542
554
|
return 'verified';
|
|
555
|
+
if (id === 'image_path_exposure') {
|
|
556
|
+
if (row.before === 'degraded' || row.after === 'degraded' || row.repairability === 'doctor-fix')
|
|
557
|
+
return 'fallback';
|
|
558
|
+
return fallback;
|
|
559
|
+
}
|
|
560
|
+
if (id === 'app_handoff')
|
|
561
|
+
return 'unavailable';
|
|
543
562
|
if (row.repairability === 'manual-required')
|
|
544
563
|
return 'manual_required';
|
|
545
564
|
if (row.before === 'degraded' || row.after === 'degraded')
|
|
@@ -550,6 +569,12 @@ function nativeCapabilityStatus(rows, id, fallback) {
|
|
|
550
569
|
return 'unavailable';
|
|
551
570
|
return fallback;
|
|
552
571
|
}
|
|
572
|
+
function uniqueNativeManualActions(rows) {
|
|
573
|
+
return [...new Set(rows
|
|
574
|
+
.filter((row) => row?.repairability === 'manual-required' && row?.after !== 'verified')
|
|
575
|
+
.flatMap((row) => Array.isArray(row.repair_actions) ? row.repair_actions : [])
|
|
576
|
+
.filter((action) => typeof action === 'string' && action.trim()))];
|
|
577
|
+
}
|
|
553
578
|
function doctorSkillStatus(coreSkills) {
|
|
554
579
|
if (!coreSkills)
|
|
555
580
|
return 'drift_detected';
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
5
5
|
import { buildSksCoreSkillManifest } from '../codex-native/core-skill-manifest.js';
|
|
6
6
|
import { syncCoreSkillsIntegrity } from '../codex-native/core-skill-integrity.js';
|
|
7
7
|
import { dedupeProjectSkills } from '../codex-native/project-skill-dedupe.js';
|
|
8
8
|
const EXTERNAL_ROUTE_RESERVED = new Set(['ulw-loop', 'ulw-plan', 'start-work']);
|
|
9
|
+
const SKILL_SYNC_LOCK_STALE_AFTER_MS = 30000;
|
|
9
10
|
export async function syncCodexSksSkills(input) {
|
|
10
11
|
const root = path.resolve(input.root);
|
|
11
12
|
const skillsRoot = input.skillsRoot || path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'skills');
|
|
@@ -71,6 +72,8 @@ export async function withSkillSyncLock(root, fn) {
|
|
|
71
72
|
const code = err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
|
|
72
73
|
if (code !== 'EEXIST')
|
|
73
74
|
throw err;
|
|
75
|
+
if (await recoverStaleSkillSyncLock(lockPath))
|
|
76
|
+
continue;
|
|
74
77
|
if (Date.now() - started > 30000)
|
|
75
78
|
throw new Error(`Timed out waiting for skill sync lock: ${lockPath}`);
|
|
76
79
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
@@ -80,7 +83,8 @@ export async function withSkillSyncLock(root, fn) {
|
|
|
80
83
|
await writeJsonAtomic(path.join(lockPath, 'owner.json'), {
|
|
81
84
|
schema: 'sks.skill-sync-lock.v1',
|
|
82
85
|
pid: process.pid,
|
|
83
|
-
acquired_at: nowIso()
|
|
86
|
+
acquired_at: nowIso(),
|
|
87
|
+
stale_after_ms: SKILL_SYNC_LOCK_STALE_AFTER_MS
|
|
84
88
|
}).catch(() => undefined);
|
|
85
89
|
return await fn();
|
|
86
90
|
}
|
|
@@ -88,6 +92,37 @@ export async function withSkillSyncLock(root, fn) {
|
|
|
88
92
|
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
89
93
|
}
|
|
90
94
|
}
|
|
95
|
+
async function recoverStaleSkillSyncLock(lockPath) {
|
|
96
|
+
const ownerPath = path.join(lockPath, 'owner.json');
|
|
97
|
+
const stat = await fs.stat(lockPath).catch(() => null);
|
|
98
|
+
const owner = await readJson(ownerPath, null).catch(() => null);
|
|
99
|
+
const staleAfterMs = Number(owner?.stale_after_ms || SKILL_SYNC_LOCK_STALE_AFTER_MS);
|
|
100
|
+
const acquiredAt = owner?.acquired_at ? Date.parse(owner.acquired_at) : NaN;
|
|
101
|
+
const ageMs = Number.isFinite(acquiredAt) ? Date.now() - acquiredAt : stat ? Date.now() - stat.mtimeMs : 0;
|
|
102
|
+
if (owner?.schema === 'sks.skill-sync-lock.v1' && Number.isFinite(owner.pid)) {
|
|
103
|
+
if (ageMs <= staleAfterMs || pidAlive(Number(owner.pid)))
|
|
104
|
+
return false;
|
|
105
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (stat && Date.now() - stat.mtimeMs > staleAfterMs) {
|
|
109
|
+
await fs.rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function pidAlive(pid) {
|
|
115
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
116
|
+
return false;
|
|
117
|
+
try {
|
|
118
|
+
process.kill(pid, 0);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
const code = err && typeof err === 'object' && 'code' in err ? String(err.code) : '';
|
|
123
|
+
return code === 'EPERM';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
91
126
|
async function listSkillNames(root) {
|
|
92
127
|
const rows = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
|
|
93
128
|
return rows.filter((row) => row.isDirectory()).map((row) => row.name).sort();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
|
|
4
|
-
import { buildSksCoreSkillManifest, isSksManagedCoreSkillContent, renderCoreSkillTemplate } from './core-skill-manifest.js';
|
|
4
|
+
import { CORE_SKILL_TEMPLATE_VERSION, buildSksCoreSkillManifest, isSksManagedCoreSkillContent, renderCoreSkillTemplate } from './core-skill-manifest.js';
|
|
5
5
|
import { canonicalSkillName } from './skill-name-canonicalizer.js';
|
|
6
6
|
export async function syncCoreSkillsIntegrity(input) {
|
|
7
7
|
const root = path.resolve(input.root);
|
|
@@ -69,7 +69,12 @@ export async function syncCoreSkillsIntegrity(input) {
|
|
|
69
69
|
root,
|
|
70
70
|
apply,
|
|
71
71
|
skills_root: skillsRoot,
|
|
72
|
+
template_version: CORE_SKILL_TEMPLATE_VERSION,
|
|
72
73
|
manifest_sha256: sha256(JSON.stringify(manifest.skills.map((skill) => [skill.canonical_name, skill.content_sha256]))),
|
|
74
|
+
drift_detected_count: rows.filter((row) => row.action === 'restore-corrupted-managed-copy' || row.action === 'skip-user-authored').length,
|
|
75
|
+
restored_count: restored.length,
|
|
76
|
+
installed_count: installed.length,
|
|
77
|
+
user_collision_count: skippedUserAuthored.length,
|
|
73
78
|
rows,
|
|
74
79
|
installed,
|
|
75
80
|
restored,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PACKAGE_VERSION, nowIso, sha256 } from '../fsx.js';
|
|
2
2
|
import { canonicalSkillName } from './skill-name-canonicalizer.js';
|
|
3
|
-
export const CORE_SKILL_TEMPLATE_VERSION = '
|
|
3
|
+
export const CORE_SKILL_TEMPLATE_VERSION = 'sks-core-skill-template.v1';
|
|
4
4
|
export const CORE_SKILL_MANAGED_BEGIN = '<!-- BEGIN SKS IMMUTABLE CORE SKILL -->';
|
|
5
5
|
export const CORE_SKILL_MANAGED_END = '<!-- END SKS IMMUTABLE CORE SKILL -->';
|
|
6
6
|
const CORE_SKILL_DEFINITIONS = [
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
1
2
|
import path from 'node:path';
|
|
2
|
-
import { writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { ensureDir, readJson, writeJsonAtomic } from '../fsx.js';
|
|
3
4
|
import { buildNativeCapabilityRepairMatrix } from './native-capability-repair-matrix.js';
|
|
4
5
|
export async function postcheckNativeCapabilities(input) {
|
|
5
6
|
const root = path.resolve(input.root);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return { ...state, after: 'unknown', blockers: ['computer_use_os_permission_or_capability_unknown'] };
|
|
11
|
-
}
|
|
12
|
-
if (state.id === 'chrome_web_review' && process.env.SKS_CHROME_EXTENSION_READY !== '1' && input.fixture !== 'all-repairable') {
|
|
13
|
-
return { ...state, after: 'unknown', blockers: ['codex_chrome_extension_readiness_not_verified'] };
|
|
14
|
-
}
|
|
15
|
-
if (state.blockers.length === 0 || verifiedAfterRepair)
|
|
16
|
-
return { ...state, after: state.repairability === 'manual-required' ? 'unknown' : 'verified', blockers: state.repairability === 'manual-required' ? state.blockers : [] };
|
|
17
|
-
return { ...state, after: 'blocked' };
|
|
18
|
-
});
|
|
19
|
-
const blockers = capabilities.flatMap((state) => state.after === 'verified' ? [] : state.blockers);
|
|
7
|
+
const fixture = input.fixture || false;
|
|
8
|
+
const matrix = input.matrix || await buildNativeCapabilityRepairMatrix({ root, fixture, reportPath: null });
|
|
9
|
+
const capabilities = await Promise.all(matrix.capabilities.map((state) => postcheckCapability(root, state, fixture)));
|
|
10
|
+
const blockers = capabilities.flatMap((state) => state.after === 'verified' || state.after === 'degraded' ? [] : state.blockers);
|
|
20
11
|
const checked = {
|
|
21
12
|
...matrix,
|
|
22
13
|
generated_at: new Date().toISOString(),
|
|
@@ -32,4 +23,141 @@ export async function postcheckNativeCapabilities(input) {
|
|
|
32
23
|
await writeJsonAtomic(reportPath, checked).catch(() => undefined);
|
|
33
24
|
return checked;
|
|
34
25
|
}
|
|
26
|
+
async function postcheckCapability(root, state, fixture) {
|
|
27
|
+
if (state.id === 'image_generation')
|
|
28
|
+
return postcheckImageGeneration(state, fixture);
|
|
29
|
+
if (state.id === 'image_followup_edit')
|
|
30
|
+
return postcheckImageFollowupEdit(root, state);
|
|
31
|
+
if (state.id === 'computer_use')
|
|
32
|
+
return postcheckComputerUse(state, fixture);
|
|
33
|
+
if (state.id === 'chrome_web_review')
|
|
34
|
+
return postcheckChromeWebReview(state, fixture);
|
|
35
|
+
if (state.id === 'codex_app_screenshot')
|
|
36
|
+
return postcheckAppScreenshot(root, state);
|
|
37
|
+
if (state.id === 'app_handoff')
|
|
38
|
+
return postcheckAppHandoff(state, fixture);
|
|
39
|
+
if (state.id === 'image_path_exposure')
|
|
40
|
+
return postcheckImagePathExposure(root, state, fixture);
|
|
41
|
+
if (state.id === 'saved_artifact_path_contract')
|
|
42
|
+
return postcheckSavedArtifactPathContract(root, state);
|
|
43
|
+
return { ...state, after: 'blocked', blockers: [...state.blockers, `unknown_capability:${state.id}`] };
|
|
44
|
+
}
|
|
45
|
+
function postcheckImageGeneration(state, fixture) {
|
|
46
|
+
if (fixture === 'all-repairable' || state.before === 'verified')
|
|
47
|
+
return verified(state);
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
after: 'unknown',
|
|
51
|
+
blockers: ['imagegen_auth_or_codex_app_builtin_missing'],
|
|
52
|
+
warnings: [...new Set([...state.warnings, 'image_generation_not_verified_without_real_capability'])]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function postcheckImageFollowupEdit(root, state) {
|
|
56
|
+
const contract = await validateSavedArtifactPathContract(root);
|
|
57
|
+
if (!contract.ok)
|
|
58
|
+
return { ...state, after: 'blocked', blockers: contract.blockers };
|
|
59
|
+
const sample = path.join(contract.imageArtifacts, 'postcheck-followup-sample.txt');
|
|
60
|
+
if (!(await writeReadSample(sample)))
|
|
61
|
+
return { ...state, after: 'blocked', blockers: ['image_followup_sample_artifact_unwritable'] };
|
|
62
|
+
return verified(state);
|
|
63
|
+
}
|
|
64
|
+
function postcheckComputerUse(state, _fixture) {
|
|
65
|
+
if (process.env.SKS_COMPUTER_USE_CAPABILITY === 'verified')
|
|
66
|
+
return verified(state);
|
|
67
|
+
return {
|
|
68
|
+
...state,
|
|
69
|
+
after: 'unknown',
|
|
70
|
+
blockers: ['computer_use_os_permission_or_capability_unknown'],
|
|
71
|
+
warnings: [...new Set([...state.warnings, 'manual_os_permission_required'])]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function postcheckChromeWebReview(state, fixture) {
|
|
75
|
+
if (fixture === 'all-repairable' || process.env.SKS_CHROME_EXTENSION_READY === '1')
|
|
76
|
+
return verified(state);
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
after: 'unknown',
|
|
80
|
+
blockers: ['codex_chrome_extension_readiness_not_verified'],
|
|
81
|
+
warnings: [...new Set([...state.warnings, 'manual_chrome_extension_setup_required'])]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function postcheckAppScreenshot(root, state) {
|
|
85
|
+
const dir = path.join(root, '.sneakoscope', 'app-screenshots');
|
|
86
|
+
const registry = path.join(dir, 'screenshot-registry.json');
|
|
87
|
+
if (!(await writeReadSample(path.join(dir, 'postcheck-screenshot-sample.txt')))) {
|
|
88
|
+
return { ...state, after: 'blocked', blockers: ['app_screenshot_directory_unwritable'] };
|
|
89
|
+
}
|
|
90
|
+
await writeJsonAtomic(registry, { schema: 'sks.app-screenshot-registry.v1', generated_at: new Date().toISOString(), screenshots: [] }).catch(() => undefined);
|
|
91
|
+
const json = await readJson(registry, {}).catch(() => ({}));
|
|
92
|
+
if (json.schema !== 'sks.app-screenshot-registry.v1')
|
|
93
|
+
return { ...state, after: 'blocked', blockers: ['app_screenshot_registry_invalid'] };
|
|
94
|
+
return verified(state);
|
|
95
|
+
}
|
|
96
|
+
function postcheckAppHandoff(state, fixture) {
|
|
97
|
+
if (fixture === 'all-repairable' || state.before === 'verified')
|
|
98
|
+
return verified(state);
|
|
99
|
+
return {
|
|
100
|
+
...state,
|
|
101
|
+
after: 'unknown',
|
|
102
|
+
blockers: ['codex_app_handoff_not_verified'],
|
|
103
|
+
warnings: [...new Set([...state.warnings, 'manual_app_handoff_approval_required'])]
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async function postcheckImagePathExposure(root, state, fixture) {
|
|
107
|
+
if (fixture === 'all-repairable' || state.before === 'verified')
|
|
108
|
+
return verified(state);
|
|
109
|
+
const contract = await validateSavedArtifactPathContract(root);
|
|
110
|
+
if (contract.ok) {
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
after: 'degraded',
|
|
114
|
+
blockers: [],
|
|
115
|
+
warnings: [...new Set([...state.warnings, 'using_saved_artifact_path_contract_fallback'])]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { ...state, after: 'blocked', blockers: ['image_path_exposure_missing_without_fallback_contract', ...contract.blockers] };
|
|
119
|
+
}
|
|
120
|
+
async function postcheckSavedArtifactPathContract(root, state) {
|
|
121
|
+
const contract = await validateSavedArtifactPathContract(root);
|
|
122
|
+
if (!contract.ok)
|
|
123
|
+
return { ...state, after: 'blocked', blockers: contract.blockers };
|
|
124
|
+
if (!(await writeReadSample(path.join(contract.imageArtifacts, 'postcheck-contract-image.txt'))))
|
|
125
|
+
return { ...state, after: 'blocked', blockers: ['image_artifacts_directory_unwritable'] };
|
|
126
|
+
if (!(await writeReadSample(path.join(contract.appScreenshots, 'postcheck-contract-screenshot.txt'))))
|
|
127
|
+
return { ...state, after: 'blocked', blockers: ['app_screenshots_directory_unwritable'] };
|
|
128
|
+
return verified(state);
|
|
129
|
+
}
|
|
130
|
+
function verified(state) {
|
|
131
|
+
return { ...state, after: 'verified', blockers: [] };
|
|
132
|
+
}
|
|
133
|
+
async function validateSavedArtifactPathContract(root) {
|
|
134
|
+
const contractPath = path.join(root, '.sneakoscope', 'reports', 'saved-artifact-path-contract.json');
|
|
135
|
+
const contract = await readJson(contractPath, null).catch(() => null);
|
|
136
|
+
const imageArtifacts = String(contract?.image_artifacts || path.join(root, '.sneakoscope', 'image-artifacts'));
|
|
137
|
+
const appScreenshots = String(contract?.app_screenshots || path.join(root, '.sneakoscope', 'app-screenshots'));
|
|
138
|
+
const blockers = [];
|
|
139
|
+
if (contract?.schema !== 'sks.saved-artifact-path-contract.v1')
|
|
140
|
+
blockers.push('saved_artifact_path_contract_schema_invalid');
|
|
141
|
+
for (const dir of [imageArtifacts, appScreenshots]) {
|
|
142
|
+
try {
|
|
143
|
+
await ensureDir(dir);
|
|
144
|
+
await fs.access(dir);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
blockers.push(`directory_unwritable:${path.basename(dir)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { ok: blockers.length === 0, imageArtifacts, appScreenshots, blockers };
|
|
151
|
+
}
|
|
152
|
+
async function writeReadSample(file) {
|
|
153
|
+
try {
|
|
154
|
+
await ensureDir(path.dirname(file));
|
|
155
|
+
await fs.writeFile(file, 'sks-native-capability-postcheck\n', 'utf8');
|
|
156
|
+
const text = await fs.readFile(file, 'utf8');
|
|
157
|
+
return text === 'sks-native-capability-postcheck\n';
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
35
163
|
//# sourceMappingURL=native-capability-postcheck.js.map
|
|
@@ -109,7 +109,7 @@ async function stateForCapability(root, id, imageCapability, nativeFeatureMatrix
|
|
|
109
109
|
warnings: envVerified ? [] : ['manual_os_permission_required']
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
|
-
const chromeReady = process.env.SKS_CHROME_EXTENSION_READY === '1'
|
|
112
|
+
const chromeReady = process.env.SKS_CHROME_EXTENSION_READY === '1';
|
|
113
113
|
return {
|
|
114
114
|
id,
|
|
115
115
|
before: chromeReady ? 'verified' : 'unknown',
|
|
@@ -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
|
package/dist/core/fsx.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
export const PACKAGE_VERSION = '3.1.
|
|
8
|
+
export const PACKAGE_VERSION = '3.1.10';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
export function nowIso() {
|
package/dist/core/init.js
CHANGED
|
@@ -11,7 +11,8 @@ import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_
|
|
|
11
11
|
import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.js';
|
|
12
12
|
import { CODEX_HOOK_EVENT_STATE_KEYS } from './codex-compat/codex-hook-events.js';
|
|
13
13
|
import { codexCommandHookCurrentHash } from './codex-hooks/codex-hook-hash.js';
|
|
14
|
-
import { buildSksCoreSkillManifest, isCoreSkillName
|
|
14
|
+
import { buildSksCoreSkillManifest, isCoreSkillName } from './codex-native/core-skill-manifest.js';
|
|
15
|
+
import { syncCoreSkillsIntegrity } from './codex-native/core-skill-integrity.js';
|
|
15
16
|
const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
|
|
16
17
|
const SKS_GENERATED_GIT_PATTERNS = [
|
|
17
18
|
'.sneakoscope/missions/',
|
|
@@ -1084,23 +1085,45 @@ export async function installSkills(root) {
|
|
|
1084
1085
|
'design-ui-editor': `---\nname: design-ui-editor\ndescription: Legacy fallback UI/UX editor for existing design.md systems when Product Design plugin is unavailable.\n---\n\nUse Product Design plugin first. When falling back, read \`design.md\`, inspect relevant UI/assets/tests, consult getdesign-reference when improving the design system, apply the smallest design-system-conformant change, use imagegen for image/logo/raster assets, and verify render quality. ${productDesignPluginPolicyText()} ${CODEX_IMAGEGEN_REQUIRED_POLICY} If design.md is missing and Product Design is unavailable, use design-system-builder as fallback.\n`,
|
|
1085
1086
|
'design-artifact-expert': `---\nname: design-artifact-expert\ndescription: Legacy fallback for high-fidelity HTML/UI/prototype artifacts when Product Design plugin cannot be used.\n---\n\nUse Product Design plugin first for design/UI/prototype work. When falling back, read design.md when present, consult getdesign-reference for design-system grounding, build the usable artifact first, preserve state, verify overlap/readability/responsiveness, and use imagegen for required assets. ${productDesignPluginPolicyText()} ${CODEX_IMAGEGEN_REQUIRED_POLICY}\n`
|
|
1086
1087
|
};
|
|
1087
|
-
|
|
1088
|
-
skills[skill.canonical_name] = renderCoreSkillTemplate(skill.canonical_name);
|
|
1089
|
-
}
|
|
1088
|
+
const nonCoreSkillNames = Object.keys(skills).filter((name) => !isCoreSkillName(name));
|
|
1090
1089
|
for (const [name, content] of Object.entries(skills)) {
|
|
1090
|
+
if (isCoreSkillName(name))
|
|
1091
|
+
continue;
|
|
1091
1092
|
const dir = path.join(root, '.agents', 'skills', name);
|
|
1092
|
-
const skillContent =
|
|
1093
|
+
const skillContent = enrichSkillContent(name, content);
|
|
1093
1094
|
await ensureDir(dir);
|
|
1094
1095
|
await writeTextAtomic(path.join(dir, 'SKILL.md'), `${skillContent.trim()}\n`);
|
|
1095
1096
|
await writeSkillMetadata(dir, name);
|
|
1096
1097
|
}
|
|
1097
|
-
const
|
|
1098
|
+
const coreManifest = buildSksCoreSkillManifest();
|
|
1099
|
+
const coreByName = new Map(coreManifest.skills.map((skill) => [skill.canonical_name, skill.content_sha256]));
|
|
1100
|
+
const coreIntegrity = await syncCoreSkillsIntegrity({
|
|
1101
|
+
root,
|
|
1102
|
+
apply: true,
|
|
1103
|
+
skillsRoot: path.join(root, '.agents', 'skills'),
|
|
1104
|
+
reportPath: path.join(root, '.sneakoscope', 'reports', 'core-skill-integrity.json')
|
|
1105
|
+
});
|
|
1106
|
+
const managedCoreSkillNames = coreIntegrity.rows
|
|
1107
|
+
.filter((row) => row.after_sha256 === coreByName.get(row.canonical_name) && row.action !== 'skip-user-authored')
|
|
1108
|
+
.map((row) => row.canonical_name);
|
|
1109
|
+
for (const name of managedCoreSkillNames) {
|
|
1110
|
+
await writeSkillMetadata(path.join(root, '.agents', 'skills', name), name);
|
|
1111
|
+
}
|
|
1112
|
+
const skillNames = [...nonCoreSkillNames, ...managedCoreSkillNames];
|
|
1098
1113
|
const removedStaleGeneratedSkills = await removeStaleGeneratedSkillsFromManifest(root, skillNames);
|
|
1099
1114
|
const removedPluginSkillCollisions = await removeGeneratedPluginSkillCollisions(root);
|
|
1100
1115
|
await writeGeneratedSkillManifest(root, skillNames);
|
|
1101
1116
|
return {
|
|
1102
1117
|
installed_skills: skillNames,
|
|
1103
1118
|
generated_files: generatedSkillFiles(skillNames),
|
|
1119
|
+
core_skill_integrity: {
|
|
1120
|
+
ok: coreIntegrity.ok,
|
|
1121
|
+
template_version: coreIntegrity.template_version,
|
|
1122
|
+
installed_count: coreIntegrity.installed_count,
|
|
1123
|
+
restored_count: coreIntegrity.restored_count,
|
|
1124
|
+
user_collision_count: coreIntegrity.user_collision_count,
|
|
1125
|
+
report: '.sneakoscope/reports/core-skill-integrity.json'
|
|
1126
|
+
},
|
|
1104
1127
|
removed_stale_generated_skills: [...removedStaleGeneratedSkills, ...removedPluginSkillCollisions].sort(),
|
|
1105
1128
|
removed_agent_skill_aliases: await removeGeneratedAgentSkillAliases(root, skillNames),
|
|
1106
1129
|
removed_codex_skill_mirrors: await removeGeneratedCodexSkillMirrors(root, skillNames)
|
|
@@ -1137,6 +1160,8 @@ async function removeStaleGeneratedSkillsFromManifest(root, skillNames) {
|
|
|
1137
1160
|
const skillName = String(name || '').trim();
|
|
1138
1161
|
if (!skillName || current.has(skillName) || !/^[a-z0-9-]+$/.test(skillName))
|
|
1139
1162
|
continue;
|
|
1163
|
+
if (isCoreSkillName(skillName))
|
|
1164
|
+
continue;
|
|
1140
1165
|
const dir = path.join(root, '.agents', 'skills', skillName);
|
|
1141
1166
|
if (!(await exists(dir)))
|
|
1142
1167
|
continue;
|
package/dist/core/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '3.1.
|
|
1
|
+
export const PACKAGE_VERSION = '3.1.10';
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.10",
|
|
5
5
|
"description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -643,6 +643,10 @@
|
|
|
643
643
|
"update:preserves-supabase-keys": "node ./dist/scripts/update-preserves-supabase-keys-blackbox.js",
|
|
644
644
|
"update:secret-preservation-guard": "node ./dist/scripts/update-secret-preservation-guard-check.js",
|
|
645
645
|
"update:secret-migration-journal": "node ./dist/scripts/update-secret-migration-journal-check.js",
|
|
646
|
+
"config:managed-merge-callsite-coverage": "node ./dist/scripts/config-managed-merge-callsite-coverage-check.js",
|
|
647
|
+
"release:gate-script-parity": "node ./dist/scripts/release-gate-script-parity-check.js",
|
|
648
|
+
"release:wiring-3110-blackbox": "node ./dist/scripts/release-wiring-3110-blackbox.js",
|
|
649
|
+
"sks:3110-all-feature-regression": "node ./dist/scripts/sks-3110-all-feature-regression-blackbox.js",
|
|
646
650
|
"release:dag-full-coverage": "node ./dist/scripts/release-dag-full-coverage-check.js",
|
|
647
651
|
"release:cache-glob-hashing": "node ./dist/scripts/release-cache-glob-hashing-check.js",
|
|
648
652
|
"release:check:affected": "npm run build --silent && node ./dist/scripts/release-gate-dag-runner.js --preset affected --changed-since auto",
|