sneakoscope 10.3.2 → 10.3.4
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 +22 -12
- package/config/skills-hash-ledger.v1.json +7 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/config/skills-manifest.json +13 -7
- package/dist/core/agents/agent-effort-policy.js +15 -13
- package/dist/core/agents/native-worker-backend-router.js +13 -8
- package/dist/core/codex-hooks/codex-hook-managed-install.js +56 -2
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +1 -0
- package/dist/core/codex-native/core-skill-manifest.js +4 -4
- package/dist/core/decisions/cli.js +9 -2
- package/dist/core/decisions/config.js +7 -0
- package/dist/core/decisions/integration.js +54 -12
- package/dist/core/decisions/policy.js +27 -4
- package/dist/core/decisions/questions.js +39 -9
- package/dist/core/decisions/routing.js +15 -10
- package/dist/core/decisions/types.js +11 -25
- package/dist/core/doctor/current-project-guidance.js +38 -1
- package/dist/core/doctor/skill-legacy-surface.js +2 -1
- package/dist/core/hooks-runtime/hook-context.js +1 -1
- package/dist/core/hooks-runtime/jev-spawn-routing.js +21 -2
- package/dist/core/hooks-runtime/managed-guidance-preflight.js +30 -0
- package/dist/core/hooks-runtime/official-subagent-lifecycle.js +3 -3
- package/dist/core/hooks-runtime/parent-orchestration-gate.js +372 -0
- package/dist/core/hooks-runtime/subagent-context.js +13 -3
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +5 -10
- package/dist/core/hooks-runtime.js +46 -8
- package/dist/core/init/skills/inventory.js +5 -1
- package/dist/core/init/skills.js +4 -4
- package/dist/core/init.js +39 -7
- package/dist/core/managed-assets/managed-assets-manifest.js +17 -17
- package/dist/core/pipeline-internals/runtime-core.js +2 -2
- package/dist/core/provider/model-router.js +8 -5
- package/dist/core/recallpulse/policy.js +2 -2
- package/dist/core/recallpulse.js +3 -3
- package/dist/core/release/gate-affected-globs.js +0 -1
- package/dist/core/research/mock-result.js +2 -2
- package/dist/core/research/research-adversarial-review.js +7 -7
- package/dist/core/research/research-claim-synthesizer.js +3 -3
- package/dist/core/research/research-falsification-runner.js +3 -3
- package/dist/core/research/research-super-search.js +5 -2
- package/dist/core/research/research-synthesis-writer.js +2 -2
- package/dist/core/research.js +5 -5
- package/dist/core/routes/dollar-manifest-lite.js +1 -1
- package/dist/core/routes.js +10 -10
- package/dist/core/runtime/task-profile.js +9 -5
- package/dist/core/subagents/model-policy.js +27 -45
- package/dist/core/subagents/model-tiers.js +153 -0
- package/dist/core/subagents/naruto-command-args.js +3 -3
- package/dist/core/subagents/naruto-help-contract.js +18 -13
- package/dist/core/subagents/naruto-host-credentials.js +8 -13
- package/dist/core/subagents/official-subagent-config.js +11 -7
- package/dist/core/subagents/official-subagent-preparation.js +12 -8
- package/dist/core/subagents/official-subagent-prompt.js +33 -33
- package/dist/core/subagents/official-subagent-runner.js +3 -3
- package/dist/core/subagents/role-model-preferences.js +26 -21
- package/dist/core/update/managed-permission-repair.js +462 -0
- package/dist/core/update/update-migration-state/retired-local-decision.js +32 -2
- package/dist/core/update/update-migration-state/simple-stages.js +42 -5
- package/dist/core/update/update-migration-state.js +63 -1
- package/dist/core/update-check.js +35 -8
- package/dist/core/version.js +1 -1
- package/dist/scripts/codex-native-agent-role-content-check.js +10 -5
- package/dist/scripts/codex-native-gate-lib.js +3 -1
- package/dist/scripts/current-surface-update-e2e-check.js +1 -0
- package/dist/scripts/mutation-callsite-coverage-check.js +2 -2
- package/dist/scripts/release-affected-selector-check.js +1 -2
- package/dist/scripts/typed-routing-gate-lib.js +3 -1
- package/package.json +1 -1
- package/release-gates.v2.json +2 -6
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { nowIso, readJson, runProcess, writeJsonAtomic } from '../fsx.js';
|
|
7
|
+
import { scanHarnessConflicts } from '../harness-conflicts.js';
|
|
8
|
+
import { canonicalSkillNameFromValue, REMOVED_SKS_SKILL_NAME_SET } from '../init/skills/inventory.js';
|
|
9
|
+
export const MANAGED_PERMISSION_REPAIR_SCHEMA = 'sks.managed-permission-repair.v1';
|
|
10
|
+
const ELEVATION_PROMPT_TIMEOUT_MS = 90_000;
|
|
11
|
+
const DECLINE_COOLDOWN_MS = 12 * 60 * 60 * 1000;
|
|
12
|
+
const MAX_REPAIR_ROUNDS = 6;
|
|
13
|
+
const FIND_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const MAX_REPORTED_PATHS = 20;
|
|
15
|
+
const USER_FLAGS = 'nouchg,nouappnd';
|
|
16
|
+
const ALL_FLAGS = 'nouchg,noschg,nouappnd,nosappnd';
|
|
17
|
+
function isSksSkillName(name) {
|
|
18
|
+
const canonical = canonicalSkillNameFromValue(name);
|
|
19
|
+
return canonical === 'sks' || canonical.startsWith('sks-') || REMOVED_SKS_SKILL_NAME_SET.has(canonical);
|
|
20
|
+
}
|
|
21
|
+
async function lstatOrNull(p) {
|
|
22
|
+
try {
|
|
23
|
+
return await fsp.lstat(p);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function readdirNames(dir) {
|
|
30
|
+
try {
|
|
31
|
+
return await fsp.readdir(dir);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function resolveManagedPermissionTargets(input) {
|
|
38
|
+
const root = path.resolve(input.root);
|
|
39
|
+
const home = path.resolve(input.home);
|
|
40
|
+
const globalRoot = path.resolve(input.globalRoot);
|
|
41
|
+
const wanted = new Map();
|
|
42
|
+
const add = (p, recursive) => {
|
|
43
|
+
const resolved = path.resolve(p);
|
|
44
|
+
wanted.set(resolved, wanted.get(resolved) === true || recursive);
|
|
45
|
+
};
|
|
46
|
+
const projectIsHome = root === home;
|
|
47
|
+
add(path.join(root, '.sneakoscope'), true);
|
|
48
|
+
if (!projectIsHome) {
|
|
49
|
+
add(path.join(root, '.codex'), true);
|
|
50
|
+
add(path.join(root, '.agents'), false);
|
|
51
|
+
add(path.join(root, '.agents', 'skills'), false);
|
|
52
|
+
}
|
|
53
|
+
add(path.join(home, '.sneakoscope'), true);
|
|
54
|
+
add(path.join(home, '.sneakoscope-global'), true);
|
|
55
|
+
add(globalRoot, true);
|
|
56
|
+
add(path.join(home, '.codex', '.sneakoscope'), true);
|
|
57
|
+
add(path.join(home, '.codex', 'agents'), true);
|
|
58
|
+
add(path.join(home, '.codex', 'sks'), true);
|
|
59
|
+
add(path.join(home, '.codex', 'sks-menubar'), true);
|
|
60
|
+
for (const container of [
|
|
61
|
+
path.join(home, '.codex'),
|
|
62
|
+
path.join(home, '.codex', 'skills'),
|
|
63
|
+
path.join(home, '.agents'),
|
|
64
|
+
path.join(home, '.agents', 'skills'),
|
|
65
|
+
path.join(home, '.cursor', 'skills'),
|
|
66
|
+
path.join(home, '.claude', 'skills')
|
|
67
|
+
])
|
|
68
|
+
add(container, false);
|
|
69
|
+
add(path.join(home, '.codex', 'config.toml'), false);
|
|
70
|
+
add(path.join(home, '.codex', 'hooks.json'), false);
|
|
71
|
+
const skillRoots = [
|
|
72
|
+
path.join(home, '.agents', 'skills'),
|
|
73
|
+
path.join(home, '.codex', 'skills'),
|
|
74
|
+
path.join(home, '.cursor', 'skills'),
|
|
75
|
+
path.join(home, '.claude', 'skills'),
|
|
76
|
+
...(projectIsHome ? [] : [path.join(root, '.agents', 'skills'), path.join(root, '.codex', 'skills')])
|
|
77
|
+
];
|
|
78
|
+
for (const skillsDir of skillRoots) {
|
|
79
|
+
for (const name of await readdirNames(skillsDir)) {
|
|
80
|
+
if (isSksSkillName(name))
|
|
81
|
+
add(path.join(skillsDir, name), true);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const conflicts = await scanHarnessConflicts(root, { home }).catch(() => null);
|
|
85
|
+
for (const conflict of (conflicts?.hard || [])) {
|
|
86
|
+
if (!conflict?.path)
|
|
87
|
+
continue;
|
|
88
|
+
const stat = await lstatOrNull(conflict.path);
|
|
89
|
+
if (stat && !stat.isSymbolicLink())
|
|
90
|
+
add(conflict.path, stat.isDirectory());
|
|
91
|
+
}
|
|
92
|
+
const forbidden = new Set([path.parse(root).root, home, root]);
|
|
93
|
+
const targets = [];
|
|
94
|
+
for (const [p, recursive] of [...wanted].sort(([a], [b]) => a.localeCompare(b))) {
|
|
95
|
+
if (forbidden.has(p) || !path.isAbsolute(p))
|
|
96
|
+
continue;
|
|
97
|
+
const stat = await lstatOrNull(p);
|
|
98
|
+
if (!stat || stat.isSymbolicLink())
|
|
99
|
+
continue;
|
|
100
|
+
targets.push({ path: p, recursive: recursive && stat.isDirectory() });
|
|
101
|
+
}
|
|
102
|
+
return targets;
|
|
103
|
+
}
|
|
104
|
+
export async function npmGlobalInstallTargets(globalNodeModules, packageName = 'sneakoscope') {
|
|
105
|
+
const nodeModules = path.resolve(globalNodeModules);
|
|
106
|
+
if (path.basename(nodeModules) !== 'node_modules')
|
|
107
|
+
return [];
|
|
108
|
+
const candidates = [
|
|
109
|
+
{ path: nodeModules, recursive: false },
|
|
110
|
+
{ path: path.join(nodeModules, packageName), recursive: true },
|
|
111
|
+
{ path: path.resolve(nodeModules, '..', '..', 'bin'), recursive: false }
|
|
112
|
+
];
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const candidate of candidates) {
|
|
115
|
+
const stat = await lstatOrNull(candidate.path);
|
|
116
|
+
if (stat && !stat.isSymbolicLink())
|
|
117
|
+
out.push({ path: candidate.path, recursive: candidate.recursive && stat.isDirectory() });
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function findConditions(owner, prune) {
|
|
122
|
+
const foreign = ['(', '!', '-type', 'l', '!', '-user', String(owner.uid), '-print0', ...(prune ? ['-prune'] : []), ')'];
|
|
123
|
+
const dirMode = ['(', '-type', 'd', '!', '-perm', '-0700', '-print0', ')'];
|
|
124
|
+
const fileMode = ['(', '-type', 'f', '!', '-perm', '-0600', '!', '-path', '*.app/*', '-print0', ')'];
|
|
125
|
+
const flags = process.platform === 'darwin'
|
|
126
|
+
? ['-o', '(', '-flags', '+uchg,uappnd,schg,sappnd', '-print0', ')']
|
|
127
|
+
: [];
|
|
128
|
+
return [...foreign, '-o', ...dirMode, '-o', ...fileMode, ...flags];
|
|
129
|
+
}
|
|
130
|
+
function parseFindErrors(stderr) {
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const line of stderr.split('\n')) {
|
|
133
|
+
const match = /^find: (.+): (?:Permission denied|Operation not permitted)$/.exec(line.trim());
|
|
134
|
+
if (match?.[1])
|
|
135
|
+
out.push(match[1]);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
async function runFind(paths, owner, recursive) {
|
|
140
|
+
if (!paths.length)
|
|
141
|
+
return { hits: [], unreadable: [], truncated: false };
|
|
142
|
+
const args = ['-P', ...paths, ...(recursive ? [] : ['-maxdepth', '0']), ...findConditions(owner, recursive)];
|
|
143
|
+
const result = await runProcess(firstExisting(['/usr/bin/find', '/bin/find']), args, { timeoutMs: 120_000, maxOutputBytes: FIND_OUTPUT_BYTES });
|
|
144
|
+
return {
|
|
145
|
+
hits: result.stdout.split('\0').filter(Boolean),
|
|
146
|
+
unreadable: parseFindErrors(result.stderr),
|
|
147
|
+
truncated: result.truncated === true
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function classify(p, owner, unreadable) {
|
|
151
|
+
const stat = await lstatOrNull(p);
|
|
152
|
+
if (!stat || stat.isSymbolicLink())
|
|
153
|
+
return null;
|
|
154
|
+
if (stat.uid !== owner.uid)
|
|
155
|
+
return { path: p, kind: 'foreign_owner' };
|
|
156
|
+
const needed = stat.isDirectory() ? 0o700 : 0o600;
|
|
157
|
+
if ((stat.mode & needed) !== needed)
|
|
158
|
+
return { path: p, kind: 'mode' };
|
|
159
|
+
if (unreadable)
|
|
160
|
+
return { path: p, kind: stat.isDirectory() ? 'tcc' : 'unreadable' };
|
|
161
|
+
return { path: p, kind: 'flags' };
|
|
162
|
+
}
|
|
163
|
+
export async function scanManagedPermissionIssues(targets, owner) {
|
|
164
|
+
const recursive = await runFind(targets.filter((t) => t.recursive).map((t) => t.path), owner, true);
|
|
165
|
+
const single = await runFind(targets.filter((t) => !t.recursive).map((t) => t.path), owner, false);
|
|
166
|
+
const unreadable = new Set([...recursive.unreadable, ...single.unreadable]);
|
|
167
|
+
const seen = new Map();
|
|
168
|
+
for (const p of [...recursive.hits, ...single.hits, ...unreadable]) {
|
|
169
|
+
if (seen.has(p))
|
|
170
|
+
continue;
|
|
171
|
+
const issue = await classify(p, owner, unreadable.has(p));
|
|
172
|
+
if (issue)
|
|
173
|
+
seen.set(p, issue);
|
|
174
|
+
}
|
|
175
|
+
return { issues: [...seen.values()], truncated: recursive.truncated || single.truncated };
|
|
176
|
+
}
|
|
177
|
+
async function fixOwnedIssues(issues) {
|
|
178
|
+
let fixed = 0;
|
|
179
|
+
for (const issue of issues) {
|
|
180
|
+
if (issue.kind !== 'mode')
|
|
181
|
+
continue;
|
|
182
|
+
const stat = await lstatOrNull(issue.path);
|
|
183
|
+
if (!stat)
|
|
184
|
+
continue;
|
|
185
|
+
const needed = stat.isDirectory() ? 0o700 : 0o600;
|
|
186
|
+
try {
|
|
187
|
+
await fsp.chmod(issue.path, (stat.mode & 0o7777) | needed);
|
|
188
|
+
fixed++;
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
if (process.platform !== 'darwin')
|
|
193
|
+
return fixed;
|
|
194
|
+
const flagged = issues.filter((issue) => issue.kind === 'flags').map((issue) => issue.path);
|
|
195
|
+
for (let i = 0; i < flagged.length; i += 200) {
|
|
196
|
+
const batch = flagged.slice(i, i + 200);
|
|
197
|
+
const result = await runProcess('/usr/bin/chflags', [USER_FLAGS, ...batch], { timeoutMs: 30_000, maxOutputBytes: 64 * 1024 });
|
|
198
|
+
if (result.code === 0)
|
|
199
|
+
fixed += batch.length;
|
|
200
|
+
}
|
|
201
|
+
return fixed;
|
|
202
|
+
}
|
|
203
|
+
function withinTargets(p, targets) {
|
|
204
|
+
for (const target of targets) {
|
|
205
|
+
if (p === target.path)
|
|
206
|
+
return target;
|
|
207
|
+
if (target.recursive && p.startsWith(`${target.path}${path.sep}`))
|
|
208
|
+
return target;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function elevationPaths(issues, targets) {
|
|
213
|
+
const candidates = issues
|
|
214
|
+
.filter((issue) => issue.kind === 'foreign_owner' || issue.kind === 'flags' || issue.kind === 'unreadable')
|
|
215
|
+
.map((issue) => issue.path)
|
|
216
|
+
.filter((p) => path.isAbsolute(p) && !/[\0\n\r]/.test(p) && withinTargets(p, targets))
|
|
217
|
+
.sort();
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const p of candidates) {
|
|
220
|
+
if (out.some((kept) => kept.recursive && p.startsWith(`${kept.path}${path.sep}`)))
|
|
221
|
+
continue;
|
|
222
|
+
const target = withinTargets(p, targets);
|
|
223
|
+
out.push({ path: p, recursive: Boolean(target?.recursive) });
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
function shellQuote(value) {
|
|
228
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
229
|
+
}
|
|
230
|
+
function firstExisting(candidates) {
|
|
231
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[candidates.length - 1];
|
|
232
|
+
}
|
|
233
|
+
export function elevationScript(paths, owner) {
|
|
234
|
+
const chown = firstExisting(['/usr/sbin/chown', '/bin/chown', '/usr/bin/chown']);
|
|
235
|
+
const chmod = firstExisting(['/bin/chmod', '/usr/bin/chmod']);
|
|
236
|
+
const chflags = process.platform === 'darwin' ? firstExisting(['/usr/bin/chflags']) : null;
|
|
237
|
+
const lines = [];
|
|
238
|
+
for (const recursive of [true, false]) {
|
|
239
|
+
const group = paths.filter((p) => p.recursive === recursive).map((p) => shellQuote(p.path));
|
|
240
|
+
if (!group.length)
|
|
241
|
+
continue;
|
|
242
|
+
const r = recursive ? ' -R' : '';
|
|
243
|
+
if (chflags)
|
|
244
|
+
lines.push(`${chflags}${r} ${ALL_FLAGS} ${group.join(' ')}`);
|
|
245
|
+
lines.push(`${chown}${r} ${owner.uid}:${owner.gid} ${group.join(' ')}`);
|
|
246
|
+
lines.push(`${chmod}${r} u+rwX ${group.join(' ')}`);
|
|
247
|
+
}
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
function testOrCi(env) {
|
|
251
|
+
return [env, process.env].some((e) => Boolean(e.NODE_TEST_CONTEXT)
|
|
252
|
+
|| e.SKS_TEST_ISOLATION === '1'
|
|
253
|
+
|| Boolean(e.SKS_TEST_FORBID_REAL_HOME)
|
|
254
|
+
|| e.CI === 'true'
|
|
255
|
+
|| e.CI === '1');
|
|
256
|
+
}
|
|
257
|
+
function sudoCached() {
|
|
258
|
+
if (process.platform === 'win32')
|
|
259
|
+
return false;
|
|
260
|
+
const result = spawnSync('/usr/bin/sudo', ['-n', 'true'], { stdio: 'ignore', timeout: 5_000 });
|
|
261
|
+
return result.status === 0;
|
|
262
|
+
}
|
|
263
|
+
function macosGuiSession() {
|
|
264
|
+
if (process.platform !== 'darwin')
|
|
265
|
+
return false;
|
|
266
|
+
const result = spawnSync('/bin/launchctl', ['managername'], { encoding: 'utf8', timeout: 3_000 });
|
|
267
|
+
return result.status === 0 && String(result.stdout || '').trim() === 'Aqua';
|
|
268
|
+
}
|
|
269
|
+
function declineStatePath(home) {
|
|
270
|
+
return path.join(home, '.sneakoscope', 'state', 'managed-permission-elevation.json');
|
|
271
|
+
}
|
|
272
|
+
async function recentlyDeclined(home) {
|
|
273
|
+
const state = await readJson(declineStatePath(home), null).catch(() => null);
|
|
274
|
+
if (!state?.declined_at)
|
|
275
|
+
return false;
|
|
276
|
+
return Date.now() - Date.parse(state.declined_at) < DECLINE_COOLDOWN_MS;
|
|
277
|
+
}
|
|
278
|
+
async function recordDecline(home) {
|
|
279
|
+
await writeJsonAtomic(declineStatePath(home), {
|
|
280
|
+
schema: 'sks.managed-permission-elevation.v1',
|
|
281
|
+
declined_at: nowIso()
|
|
282
|
+
}).catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
function chooseChannel(opts, env) {
|
|
285
|
+
if (opts.channel)
|
|
286
|
+
return opts.channel;
|
|
287
|
+
if (env.SKS_UPDATE_ELEVATION === 'never' || testOrCi(env))
|
|
288
|
+
return 'disabled';
|
|
289
|
+
if (process.getuid?.() === 0)
|
|
290
|
+
return 'running_as_root';
|
|
291
|
+
if (sudoCached())
|
|
292
|
+
return 'sudo_cached';
|
|
293
|
+
if (macosGuiSession())
|
|
294
|
+
return 'macos_admin_dialog';
|
|
295
|
+
if (opts.interactive && process.stdin.isTTY && process.stderr.isTTY)
|
|
296
|
+
return 'sudo_terminal';
|
|
297
|
+
return 'unavailable';
|
|
298
|
+
}
|
|
299
|
+
function appleScriptString(value) {
|
|
300
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
301
|
+
}
|
|
302
|
+
const ELEVATION_REASON = 'SKS update needs administrator permission to give you back ownership of SKS-managed files it must clean up.';
|
|
303
|
+
async function defaultRunElevated(script, channel, reason = ELEVATION_REASON) {
|
|
304
|
+
if (channel === 'running_as_root') {
|
|
305
|
+
const result = await runProcess('/bin/sh', ['-c', script], { timeoutMs: 120_000, maxOutputBytes: 64 * 1024 });
|
|
306
|
+
return { ok: result.code === 0, error: result.code === 0 ? undefined : result.stderr.trim() || `exit_${result.code}` };
|
|
307
|
+
}
|
|
308
|
+
if (channel === 'sudo_cached') {
|
|
309
|
+
const result = await runProcess('/usr/bin/sudo', ['-n', '/bin/sh', '-c', script], { timeoutMs: 120_000, maxOutputBytes: 64 * 1024 });
|
|
310
|
+
return { ok: result.code === 0, error: result.code === 0 ? undefined : result.stderr.trim() || `exit_${result.code}` };
|
|
311
|
+
}
|
|
312
|
+
if (channel === 'macos_admin_dialog') {
|
|
313
|
+
const apple = `do shell script ${appleScriptString(script)} with prompt ${appleScriptString(reason)} with administrator privileges`;
|
|
314
|
+
const result = await runProcess('/usr/bin/osascript', ['-e', apple], { timeoutMs: ELEVATION_PROMPT_TIMEOUT_MS, maxOutputBytes: 64 * 1024 });
|
|
315
|
+
if (result.code === 0)
|
|
316
|
+
return { ok: true };
|
|
317
|
+
const declined = /-128|User canceled/i.test(result.stderr) || result.timedOut === true;
|
|
318
|
+
return { ok: false, declined, error: result.timedOut ? 'admin_dialog_timed_out' : result.stderr.trim() || `exit_${result.code}` };
|
|
319
|
+
}
|
|
320
|
+
if (channel === 'sudo_terminal') {
|
|
321
|
+
process.stderr.write(`\n${reason}\n`);
|
|
322
|
+
const result = spawnSync('/usr/bin/sudo', ['-p', 'Password for SKS permission repair: ', '/bin/sh', '-c', script], {
|
|
323
|
+
stdio: ['inherit', 'ignore', 'inherit'],
|
|
324
|
+
timeout: ELEVATION_PROMPT_TIMEOUT_MS
|
|
325
|
+
});
|
|
326
|
+
return { ok: result.status === 0, declined: result.status !== 0, error: result.status === 0 ? undefined : `sudo_exit_${result.status ?? 'timeout'}` };
|
|
327
|
+
}
|
|
328
|
+
return { ok: false, error: `no_elevation_channel:${channel}` };
|
|
329
|
+
}
|
|
330
|
+
function ownerFor(opts, env) {
|
|
331
|
+
if (opts.uid !== undefined)
|
|
332
|
+
return { uid: opts.uid, gid: opts.gid ?? opts.uid };
|
|
333
|
+
const uid = process.getuid?.();
|
|
334
|
+
if (uid === undefined)
|
|
335
|
+
return null;
|
|
336
|
+
if (uid !== 0)
|
|
337
|
+
return { uid, gid: process.getgid?.() ?? uid };
|
|
338
|
+
const sudoUid = Number.parseInt(env.SUDO_UID || '', 10);
|
|
339
|
+
const sudoGid = Number.parseInt(env.SUDO_GID || '', 10);
|
|
340
|
+
if (!Number.isSafeInteger(sudoUid) || sudoUid === 0)
|
|
341
|
+
return null;
|
|
342
|
+
return { uid: sudoUid, gid: Number.isSafeInteger(sudoGid) ? sudoGid : sudoUid };
|
|
343
|
+
}
|
|
344
|
+
function emptyReport(root, warning) {
|
|
345
|
+
return {
|
|
346
|
+
schema: MANAGED_PERMISSION_REPAIR_SCHEMA,
|
|
347
|
+
generated_at: nowIso(),
|
|
348
|
+
ok: true,
|
|
349
|
+
root,
|
|
350
|
+
target_count: 0,
|
|
351
|
+
issues_found: 0,
|
|
352
|
+
user_fixed: 0,
|
|
353
|
+
elevation: { needed: false, channel: 'not_needed', paths: [], ok: null, declined: false, error: null },
|
|
354
|
+
remaining: [],
|
|
355
|
+
warnings: [`managed_permission_repair_skipped:${warning}`],
|
|
356
|
+
operator_actions: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function listPaths(paths) {
|
|
360
|
+
const shown = paths.slice(0, MAX_REPORTED_PATHS);
|
|
361
|
+
return shown.join(', ') + (paths.length > shown.length ? ` (+${paths.length - shown.length} more)` : '');
|
|
362
|
+
}
|
|
363
|
+
export async function repairManagedPermissions(opts) {
|
|
364
|
+
const env = opts.env || process.env;
|
|
365
|
+
const root = path.resolve(opts.root);
|
|
366
|
+
const home = path.resolve(opts.home || env.HOME || os.homedir());
|
|
367
|
+
const globalRoot = path.resolve(opts.globalRoot || env.SKS_GLOBAL_ROOT || path.join(home, '.sneakoscope-global'));
|
|
368
|
+
const owner = ownerFor(opts, env);
|
|
369
|
+
if (!owner || process.platform === 'win32')
|
|
370
|
+
return emptyReport(root, owner ? 'unsupported_platform' : 'no_target_user');
|
|
371
|
+
const targets = opts.targets || await resolveManagedPermissionTargets({ root, home, globalRoot });
|
|
372
|
+
const warnings = [];
|
|
373
|
+
let issuesFound = 0;
|
|
374
|
+
let userFixed = 0;
|
|
375
|
+
let scan = await scanManagedPermissionIssues(targets, owner);
|
|
376
|
+
issuesFound = scan.issues.length;
|
|
377
|
+
for (let round = 0; round < MAX_REPAIR_ROUNDS && scan.issues.some((issue) => issue.kind === 'mode' || issue.kind === 'flags'); round++) {
|
|
378
|
+
const fixed = await fixOwnedIssues(scan.issues);
|
|
379
|
+
userFixed += fixed;
|
|
380
|
+
if (!fixed)
|
|
381
|
+
break;
|
|
382
|
+
scan = await scanManagedPermissionIssues(targets, owner);
|
|
383
|
+
issuesFound = Math.max(issuesFound, userFixed + scan.issues.length);
|
|
384
|
+
}
|
|
385
|
+
if (scan.truncated)
|
|
386
|
+
warnings.push('managed_permission_scan_truncated');
|
|
387
|
+
const elevate = elevationPaths(scan.issues, targets);
|
|
388
|
+
let channel = 'not_needed';
|
|
389
|
+
let elevationOk = null;
|
|
390
|
+
let declined = false;
|
|
391
|
+
let elevationError = null;
|
|
392
|
+
if (elevate.length) {
|
|
393
|
+
channel = chooseChannel(opts, env);
|
|
394
|
+
if (channel !== 'disabled' && channel !== 'unavailable' && opts.explicit !== true && await recentlyDeclined(home)) {
|
|
395
|
+
channel = 'cooldown';
|
|
396
|
+
}
|
|
397
|
+
if (channel !== 'disabled' && channel !== 'unavailable' && channel !== 'cooldown') {
|
|
398
|
+
const script = elevationScript(elevate, owner);
|
|
399
|
+
const result = await (opts.runElevated
|
|
400
|
+
? opts.runElevated(script, channel)
|
|
401
|
+
: defaultRunElevated(script, channel, opts.reason));
|
|
402
|
+
elevationOk = result.ok;
|
|
403
|
+
declined = result.declined === true;
|
|
404
|
+
elevationError = result.error || null;
|
|
405
|
+
if (declined)
|
|
406
|
+
await recordDecline(home);
|
|
407
|
+
scan = await scanManagedPermissionIssues(targets, owner);
|
|
408
|
+
if (scan.issues.some((issue) => issue.kind === 'mode' || issue.kind === 'flags')) {
|
|
409
|
+
userFixed += await fixOwnedIssues(scan.issues);
|
|
410
|
+
scan = await scanManagedPermissionIssues(targets, owner);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const remaining = scan.issues;
|
|
415
|
+
const operatorActions = [];
|
|
416
|
+
const blocking = remaining.filter((issue) => issue.kind !== 'tcc');
|
|
417
|
+
const tcc = remaining.filter((issue) => issue.kind === 'tcc');
|
|
418
|
+
if (blocking.length) {
|
|
419
|
+
warnings.push(`managed_permission_unresolved:${blocking.length}`);
|
|
420
|
+
const manual = elevationPaths(blocking, targets);
|
|
421
|
+
if (manual.length) {
|
|
422
|
+
operatorActions.push(`SKS could not repair permissions on ${listPaths(manual.map((p) => p.path))}. Run this once, then rerun \`sks update\`:\nsudo /bin/sh -c ${shellQuote(elevationScript(manual, owner).replace(/\n/g, '; '))}`);
|
|
423
|
+
}
|
|
424
|
+
if (channel === 'unavailable')
|
|
425
|
+
warnings.push('managed_permission_elevation_unavailable');
|
|
426
|
+
if (channel === 'cooldown')
|
|
427
|
+
warnings.push('managed_permission_elevation_declined_recently');
|
|
428
|
+
if (declined)
|
|
429
|
+
warnings.push('managed_permission_elevation_declined');
|
|
430
|
+
}
|
|
431
|
+
if (tcc.length) {
|
|
432
|
+
warnings.push(`managed_permission_privacy_denied:${tcc.length}`);
|
|
433
|
+
operatorActions.push(`macOS privacy settings block ${listPaths(tcc.map((issue) => issue.path))}. Grant Full Disk Access to the app that runs \`sks\` (Terminal, Codex, or SKS Menu Bar), then rerun \`sks update\`.`);
|
|
434
|
+
}
|
|
435
|
+
const report = {
|
|
436
|
+
schema: MANAGED_PERMISSION_REPAIR_SCHEMA,
|
|
437
|
+
generated_at: nowIso(),
|
|
438
|
+
ok: remaining.length === 0,
|
|
439
|
+
root,
|
|
440
|
+
target_count: targets.length,
|
|
441
|
+
issues_found: issuesFound,
|
|
442
|
+
user_fixed: userFixed,
|
|
443
|
+
elevation: {
|
|
444
|
+
needed: elevate.length > 0,
|
|
445
|
+
channel,
|
|
446
|
+
paths: elevate.map((p) => p.path).slice(0, MAX_REPORTED_PATHS),
|
|
447
|
+
ok: elevationOk,
|
|
448
|
+
declined,
|
|
449
|
+
error: elevationError
|
|
450
|
+
},
|
|
451
|
+
remaining: remaining.slice(0, MAX_REPORTED_PATHS),
|
|
452
|
+
warnings,
|
|
453
|
+
operator_actions: operatorActions
|
|
454
|
+
};
|
|
455
|
+
const reportPath = opts.reportPath === undefined
|
|
456
|
+
? path.join(root, '.sneakoscope', 'reports', 'managed-permission-repair.json')
|
|
457
|
+
: opts.reportPath;
|
|
458
|
+
if (reportPath && (issuesFound > 0 || fs.existsSync(path.dirname(reportPath)))) {
|
|
459
|
+
await writeJsonAtomic(reportPath, report).catch(() => undefined);
|
|
460
|
+
}
|
|
461
|
+
return report;
|
|
462
|
+
}
|
|
@@ -5,6 +5,8 @@ import { runProcess } from '../../fsx.js';
|
|
|
5
5
|
import { inspectConfinedPath, ManagedPathSafetyError } from '../../managed-path-safety.js';
|
|
6
6
|
const RUNTIME_DIRNAME = 'local-decision';
|
|
7
7
|
const MAX_MARKER_BYTES = 256 * 1024;
|
|
8
|
+
const RETIRED_LOCAL_MODEL_CONFIG = 'local-model.json';
|
|
9
|
+
const RETIRED_LOCAL_MODEL_SCHEMAS = new Set(['sks.local-model-config.v1', 'sks.local-model-config.v2']);
|
|
8
10
|
export async function runRetiredLocalDecisionStage(env = process.env) {
|
|
9
11
|
const cleanup = await removeRetiredLocalDecisionRuntime(env);
|
|
10
12
|
return {
|
|
@@ -26,12 +28,14 @@ export async function removeRetiredLocalDecisionRuntime(env = process.env, opts
|
|
|
26
28
|
runtime_root: runtimeRoot,
|
|
27
29
|
removed_runtime: false,
|
|
28
30
|
removed_socket_dir: false,
|
|
29
|
-
stopped_worker: false
|
|
31
|
+
stopped_worker: false,
|
|
32
|
+
removed_local_model_config: false
|
|
30
33
|
};
|
|
31
34
|
if (sksHome === path.parse(sksHome).root) {
|
|
32
35
|
blockers.push('retired_local_decision_home_root_refused');
|
|
33
36
|
return { ok: false, actions, blockers, warnings, detail };
|
|
34
37
|
}
|
|
38
|
+
const sksHomeExists = await fsp.lstat(sksHome).then(() => true, () => false);
|
|
35
39
|
const stopped = await stopRecordedWorker(runtimeRoot).catch(() => 'unreadable');
|
|
36
40
|
if (stopped === 'stopped') {
|
|
37
41
|
detail.stopped_worker = true;
|
|
@@ -40,7 +44,7 @@ export async function removeRetiredLocalDecisionRuntime(env = process.env, opts
|
|
|
40
44
|
else if (stopped === 'not_ours') {
|
|
41
45
|
warnings.push('retired_local_decision_worker_pid_not_owned');
|
|
42
46
|
}
|
|
43
|
-
const removedDefault = await removeConfinedTree(sksHome, runtimeRoot);
|
|
47
|
+
const removedDefault = sksHomeExists ? await removeConfinedTree(sksHome, runtimeRoot) : 'absent';
|
|
44
48
|
if (removedDefault === 'removed') {
|
|
45
49
|
detail.removed_runtime = true;
|
|
46
50
|
actions.push('removed_retired_local_decision_runtime');
|
|
@@ -51,6 +55,14 @@ export async function removeRetiredLocalDecisionRuntime(env = process.env, opts
|
|
|
51
55
|
else {
|
|
52
56
|
blockers.push(`retired_local_decision_runtime_${removedDefault}`);
|
|
53
57
|
}
|
|
58
|
+
const localModel = sksHomeExists ? await removeRetiredLocalModelConfig(sksHome) : 'absent';
|
|
59
|
+
if (localModel === 'removed') {
|
|
60
|
+
detail.removed_local_model_config = true;
|
|
61
|
+
actions.push('removed_retired_local_model_config');
|
|
62
|
+
}
|
|
63
|
+
else if (localModel === 'preserved') {
|
|
64
|
+
warnings.push('retired_local_model_config_preserved_unrecognized');
|
|
65
|
+
}
|
|
54
66
|
const explicit = explicitRuntimeRoot(env, runtimeRoot);
|
|
55
67
|
if (explicit) {
|
|
56
68
|
const removedExplicit = await removeMarkedExplicitRoot(explicit);
|
|
@@ -92,6 +104,24 @@ async function removeMarkedExplicitRoot(target) {
|
|
|
92
104
|
return 'preserved';
|
|
93
105
|
return removeConfinedTree(parent, target);
|
|
94
106
|
}
|
|
107
|
+
async function removeRetiredLocalModelConfig(sksHome) {
|
|
108
|
+
const file = path.join(sksHome, RETIRED_LOCAL_MODEL_CONFIG);
|
|
109
|
+
const stat = await fsp.lstat(file).catch(() => null);
|
|
110
|
+
if (!stat)
|
|
111
|
+
return 'absent';
|
|
112
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_MARKER_BYTES)
|
|
113
|
+
return 'preserved';
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
116
|
+
if (!RETIRED_LOCAL_MODEL_SCHEMAS.has(String(parsed?.schema || '')))
|
|
117
|
+
return 'preserved';
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return 'preserved';
|
|
121
|
+
}
|
|
122
|
+
await fsp.rm(file, { force: false });
|
|
123
|
+
return 'removed';
|
|
124
|
+
}
|
|
95
125
|
async function isRetiredRuntimeMarker(root) {
|
|
96
126
|
const configPath = path.join(root, 'config.json');
|
|
97
127
|
const receiptPath = path.join(root, 'install-receipt.json');
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { retargetLiveManagedHookScript } from '../../codex-hooks/codex-hook-managed-install.js';
|
|
1
3
|
import { codexHookTrustDoctor } from '../../codex-hooks/codex-hook-trust-doctor.js';
|
|
4
|
+
import { codexHomePath } from '../../codex-app/codex-model-catalog.js';
|
|
5
|
+
import { readText, writeTextAtomic } from '../../fsx.js';
|
|
6
|
+
import { managedHookEventNames, pruneRetiredSksHookEvents } from '../../init.js';
|
|
2
7
|
export async function runOtherHarnessCleanupStage(root) {
|
|
3
8
|
const { cleanupOtherHarnessConflicts, scanHarnessConflicts } = await import('../../harness-conflicts.js');
|
|
4
9
|
const scan = await scanHarnessConflicts(root);
|
|
@@ -36,17 +41,49 @@ export async function runOtherHarnessCleanupStage(root) {
|
|
|
36
41
|
}
|
|
37
42
|
};
|
|
38
43
|
}
|
|
44
|
+
async function pruneRetiredSksHookFiles(root) {
|
|
45
|
+
const installed = managedHookEventNames(root);
|
|
46
|
+
const files = [...new Set([
|
|
47
|
+
path.join(root, '.codex', 'hooks.json'),
|
|
48
|
+
path.join(codexHomePath(), 'hooks.json')
|
|
49
|
+
])];
|
|
50
|
+
const actions = [];
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const before = await readText(file, '');
|
|
53
|
+
if (!before.trim())
|
|
54
|
+
continue;
|
|
55
|
+
const pruned = pruneRetiredSksHookEvents(before, installed);
|
|
56
|
+
if (!pruned.removed.length)
|
|
57
|
+
continue;
|
|
58
|
+
await writeTextAtomic(file, pruned.text);
|
|
59
|
+
actions.push(`pruned_retired_sks_hook_events:${pruned.removed.join('+')}`);
|
|
60
|
+
}
|
|
61
|
+
return actions;
|
|
62
|
+
}
|
|
39
63
|
export async function runHookTrustRefreshStage(root) {
|
|
64
|
+
const pruneActions = await pruneRetiredSksHookFiles(root).catch(() => []);
|
|
40
65
|
const result = await codexHookTrustDoctor(root, { fix: true, managed: true, actual: true });
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
:
|
|
66
|
+
const liveHook = await retargetLiveManagedHookScript(process.env).catch((err) => ({
|
|
67
|
+
ok: false,
|
|
68
|
+
status: 'absent',
|
|
69
|
+
script: null,
|
|
70
|
+
command: null,
|
|
71
|
+
error: err instanceof Error ? err.message : String(err)
|
|
72
|
+
}));
|
|
73
|
+
const blockers = [
|
|
74
|
+
...(result.ok === false ? (result.blockers || ['hook_trust_refresh_failed']) : []),
|
|
75
|
+
...(liveHook.ok === false ? [`live_managed_hook_retarget_failed:${liveHook.error || liveHook.status}`] : [])
|
|
76
|
+
];
|
|
44
77
|
return {
|
|
45
78
|
ok: blockers.length === 0,
|
|
46
79
|
status: blockers.length ? 'failed' : 'ok',
|
|
47
|
-
actions: ['refreshed_hook_trust'],
|
|
80
|
+
actions: [...pruneActions, 'refreshed_hook_trust', ...(liveHook.status === 'rewritten' ? ['retargeted_live_managed_hook'] : [])],
|
|
48
81
|
blockers,
|
|
49
82
|
warnings: result.warnings || [],
|
|
50
|
-
detail: {
|
|
83
|
+
detail: {
|
|
84
|
+
entries: result.current_hash_count ?? null,
|
|
85
|
+
live_hook_status: liveHook.status,
|
|
86
|
+
live_hook_script: liveHook.script
|
|
87
|
+
}
|
|
51
88
|
};
|
|
52
89
|
}
|