sneakoscope 0.7.53 → 0.7.55
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 +1 -1
- package/package.json +1 -1
- package/src/cli/codex-app-command.mjs +2 -1
- package/src/cli/main.mjs +36 -2
- package/src/core/codex-adapter.mjs +12 -6
- package/src/core/codex-model-guard.mjs +50 -0
- package/src/core/db-safety.mjs +39 -3
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +43 -1
- package/src/core/tmux-ui.mjs +4 -3
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ sks --mad
|
|
|
211
211
|
sks --mad --yes
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and
|
|
214
|
+
This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, Supabase MCP `apply_migration`, and required Supabase CLI migration application such as `supabase migration up` or `supabase db push` are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session unless auth repair requires a fresh codex-lb session.
|
|
215
215
|
|
|
216
216
|
MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
|
|
217
217
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.55",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { codexRemoteControlStatus, formatCodexRemoteControlStatus } from '../core/codex-app.mjs';
|
|
3
|
+
import { forceGpt55CodexConfigArgs } from '../core/codex-model-guard.mjs';
|
|
3
4
|
|
|
4
5
|
export async function codexAppRemoteControlCommand(args = [], opts = {}) {
|
|
5
6
|
const controlArgs = argsBeforeSeparator(args);
|
|
@@ -27,7 +28,7 @@ export async function codexAppRemoteControlCommand(args = [], opts = {}) {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const passthrough = stripSeparator(args);
|
|
31
|
+
const passthrough = forceGpt55CodexConfigArgs(stripSeparator(args));
|
|
31
32
|
const spawnFn = opts.spawn || spawn;
|
|
32
33
|
const code = await spawnInherited(spawnFn, status.codex_cli.bin, ['remote-control', ...passthrough], {
|
|
33
34
|
cwd: process.cwd(),
|
package/src/cli/main.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import readline from 'node:readline/promises';
|
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
|
|
7
7
|
import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
8
|
-
import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
8
|
+
import { buildCodexExecArgs, getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
9
9
|
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
10
10
|
import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
|
|
11
11
|
import { sealContract, validateAnswers } from '../core/decision-contract.mjs';
|
|
@@ -2277,6 +2277,10 @@ async function selftest() {
|
|
|
2277
2277
|
if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
|
|
2278
2278
|
const forcedModelPlan = await buildTmuxLaunchPlan({ root: tmp, env: { SKS_CODEX_MODEL: 'gpt-5.4-mini', SKS_CODEX_FAST_HIGH: '0', SKS_CODEX_REASONING: 'medium' }, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2279
2279
|
if (forcedModelPlan.codexArgs.includes('gpt-5.4-mini') || forcedModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="medium"') throw new Error('selftest failed: sks tmux launch allowed a non-GPT-5.5 model override');
|
|
2280
|
+
const explicitBadModelPlan = await buildTmuxLaunchPlan({ root: tmp, codexArgs: ['--profile', 'legacy-5.4', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '-c', 'model_reasoning_effort="low"'], tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2281
|
+
if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.4') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 --profile legacy-5.4 -c model_reasoning_effort="low"') throw new Error('selftest failed: explicit tmux model override was not forced back to GPT-5.5');
|
|
2282
|
+
const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-5.4', extraArgs: ['--model=gpt-5.4-mini', '--config', 'model = "gpt-5.4"', '-c', 'model_reasoning_effort="medium"'] });
|
|
2283
|
+
if (codexExecArgs.join(' ').includes('gpt-5.4') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.4-mini')) throw new Error('selftest failed: codex exec args allowed a non-GPT-5.5 model override');
|
|
2280
2284
|
const codexLbHome = path.join(tmp, 'codex-lb-home');
|
|
2281
2285
|
await ensureDir(path.join(codexLbHome, '.codex'));
|
|
2282
2286
|
const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
|
|
@@ -2309,6 +2313,7 @@ async function selftest() {
|
|
|
2309
2313
|
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || codexLbConfig.includes('codex_hooks = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults, force GPT-5.5, or migrate the hooks feature flag');
|
|
2310
2314
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
2311
2315
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
2316
|
+
if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
|
|
2312
2317
|
if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
|
|
2313
2318
|
const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
|
|
2314
2319
|
if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
|
|
@@ -2353,7 +2358,7 @@ async function selftest() {
|
|
|
2353
2358
|
if (!String(openClawAutoUpdate.stdout || '').includes('Codex CLI ready: 0.1.0 -> codex-cli 99.0.0')) throw new Error('selftest failed: OpenClaw mode did not auto-approve Codex CLI update before tmux launch');
|
|
2354
2359
|
const remoteControlBin = path.join(tmp, 'remote-control-bin');
|
|
2355
2360
|
await ensureDir(remoteControlBin);
|
|
2356
|
-
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2361
|
+
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then shift; for arg in "$@"; do if [ "$arg" = "--model" ]; then echo "remote-control rejects --model" >&2; exit 64; fi; done; echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2357
2362
|
await fsp.chmod(path.join(remoteControlBin, 'codex'), 0o755);
|
|
2358
2363
|
const remoteControlStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run', '--json'], {
|
|
2359
2364
|
cwd: globalCwd,
|
|
@@ -2364,6 +2369,14 @@ async function selftest() {
|
|
|
2364
2369
|
if (remoteControlStatus.code !== 0) throw new Error(`selftest failed: Codex remote-control status exited ${remoteControlStatus.code}: ${remoteControlStatus.stderr}`);
|
|
2365
2370
|
const remoteControlJson = JSON.parse(remoteControlStatus.stdout);
|
|
2366
2371
|
if (!remoteControlJson.ok || remoteControlJson.min_version !== '0.130.0' || !String(remoteControlJson.command || '').includes('remote-control')) throw new Error('selftest failed: Codex remote-control status did not report 0.130.0 readiness');
|
|
2372
|
+
const remoteControlLaunch = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '--example'], {
|
|
2373
|
+
cwd: globalCwd,
|
|
2374
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
|
|
2375
|
+
timeoutMs: 15000,
|
|
2376
|
+
maxOutputBytes: 64 * 1024
|
|
2377
|
+
});
|
|
2378
|
+
const remoteControlLaunchText = `${remoteControlLaunch.stdout}\n${remoteControlLaunch.stderr}`;
|
|
2379
|
+
if (remoteControlLaunch.code !== 0 || remoteControlLaunchText.includes('gpt-5.4') || remoteControlLaunchText.includes('--model') || !remoteControlLaunchText.includes('-c model="gpt-5.5"')) throw new Error('selftest failed: Codex remote-control passthrough did not force GPT-5.5 with config syntax');
|
|
2367
2380
|
const remoteControlOldBin = path.join(tmp, 'remote-control-old-bin');
|
|
2368
2381
|
await ensureDir(remoteControlOldBin);
|
|
2369
2382
|
await writeTextAtomic(path.join(remoteControlOldBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.129.0"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
@@ -2804,6 +2817,10 @@ async function selftest() {
|
|
|
2804
2817
|
if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest failed: $Team hook did not prepare direct Team mission');
|
|
2805
2818
|
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pipeline plan');
|
|
2806
2819
|
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Team plan was not created directly');
|
|
2820
|
+
const hookForbiddenModelResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team should be blocked before route work', model: 'gpt-5.5', metadata: { client: { modelId: 'gpt-5.4-mini' } } }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2821
|
+
if (hookForbiddenModelResult.code !== 0) throw new Error(`selftest failed: forbidden model hook exited ${hookForbiddenModelResult.code}: ${hookForbiddenModelResult.stderr}`);
|
|
2822
|
+
const hookForbiddenModelJson = JSON.parse(hookForbiddenModelResult.stdout);
|
|
2823
|
+
if (hookForbiddenModelJson.decision !== 'block' || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.5') || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.4-mini')) throw new Error('selftest failed: hook did not block GPT-5.4 client model metadata');
|
|
2807
2824
|
const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2808
2825
|
if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
|
|
2809
2826
|
const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
|
|
@@ -3497,6 +3514,12 @@ async function selftest() {
|
|
|
3497
3514
|
const supabaseWritePayloadClass = classifyToolPayload({ tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'x' where id = '1';" });
|
|
3498
3515
|
if (supabaseWritePayloadClass.level !== 'write' || !supabaseWritePayloadClass.toolReasons.includes('database_tool')) throw new Error('selftest failed: Supabase execute_sql write classification was weakened');
|
|
3499
3516
|
if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest failed: supabase db reset not detected');
|
|
3517
|
+
const supabaseMigrationApplyClass = classifyCommand('supabase migration up --linked');
|
|
3518
|
+
if (supabaseMigrationApplyClass.level !== 'write' || !supabaseMigrationApplyClass.reasons.includes('supabase_migration_apply')) throw new Error('selftest failed: supabase migration apply was not classified as DB write');
|
|
3519
|
+
const supabaseDbPushClass = classifyCommand('supabase db push');
|
|
3520
|
+
if (supabaseDbPushClass.level !== 'write' || !supabaseDbPushClass.reasons.includes('supabase_db_push')) throw new Error('selftest failed: supabase db push was not classified as migration apply work');
|
|
3521
|
+
const supabaseApplyMigrationToolClass = classifyToolPayload({ tool_name: 'mcp__supabase__apply_migration', name: 'add_selftest_table' });
|
|
3522
|
+
if (supabaseApplyMigrationToolClass.level !== 'write' || !supabaseApplyMigrationToolClass.toolReasons.includes('migration_apply_tool')) throw new Error('selftest failed: Supabase apply_migration tool was not classified as DB write');
|
|
3500
3523
|
const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringNoQuestion: true });
|
|
3501
3524
|
if (dbDecision.action !== 'block') throw new Error('selftest failed: destructive MCP SQL allowed');
|
|
3502
3525
|
const computerUseDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__computer_use__open_app', bundle_id: 'com.microsoft.edgemac', action: 'open_app' }, { duringNoQuestion: true });
|
|
@@ -3509,6 +3532,17 @@ async function selftest() {
|
|
|
3509
3532
|
if (madColumnCleanupDecision.action !== 'allow' || !madColumnCleanupDecision.mad_sks?.permission_profile?.allowed?.includes('direct_execute_sql_writes')) throw new Error('selftest failed: MAD-SKS column cleanup was not allowed through the modular permission gate');
|
|
3510
3533
|
const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
|
|
3511
3534
|
if (madLiveDmlDecision.action !== 'allow' || !madLiveDmlDecision.mad_sks?.live_server_writes_allowed) throw new Error('selftest failed: MAD-SKS targeted live DML was not allowed');
|
|
3535
|
+
const madMigrationUpDecision = await checkDbOperation(tmp, madState, { command: 'supabase migration up --linked' }, { duringNoQuestion: true });
|
|
3536
|
+
if (madMigrationUpDecision.action !== 'allow' || !madMigrationUpDecision.mad_sks?.permission_profile?.allowed?.includes('migration_apply_when_required')) throw new Error('selftest failed: MAD-SKS did not allow Supabase migration up during no-question execution');
|
|
3537
|
+
const madDbPushDecision = await checkDbOperation(tmp, madState, { command: 'supabase db push' }, { duringNoQuestion: true });
|
|
3538
|
+
if (madDbPushDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS did not allow Supabase db push migration application');
|
|
3539
|
+
const madApplyMigrationDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__apply_migration', name: 'add_selftest_table' }, { duringNoQuestion: true });
|
|
3540
|
+
if (madApplyMigrationDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS did not allow Supabase MCP apply_migration');
|
|
3541
|
+
const madTmuxMission = await createMission(tmp, { mode: 'mad-sks', prompt: 'sks --mad migration selftest' });
|
|
3542
|
+
await writeJsonAtomic(path.join(madTmuxMission.dir, 'mad-sks-gate.json'), { schema_version: 1, passed: false, mad_sks_permission_active: true, migration_apply_allowed: true });
|
|
3543
|
+
const madTmuxState = { mission_id: madTmuxMission.id, mode: 'MADSKS', route_command: '$MAD-SKS', stop_gate: 'mad-sks-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'mad-sks-gate.json', migration_apply_allowed: true };
|
|
3544
|
+
const madTmuxMigrationDecision = await checkDbOperation(tmp, madTmuxState, { command: 'supabase migration up --linked' }, { duringNoQuestion: true });
|
|
3545
|
+
if (madTmuxMigrationDecision.action !== 'allow') throw new Error('selftest failed: sks --mad state did not allow Supabase migration application');
|
|
3512
3546
|
const tableRemovalSql = 'dr' + 'op table users;';
|
|
3513
3547
|
const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
|
|
3514
3548
|
if (madTableRemovalDecision.action !== 'block') throw new Error('selftest failed: MAD-SKS catastrophic table removal was not blocked');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { exists, packageRoot, runProcess, which } from './fsx.mjs';
|
|
3
|
+
import { forceGpt55CodexArgs } from './codex-model-guard.mjs';
|
|
3
4
|
|
|
4
5
|
export async function findCodexBinary() {
|
|
5
6
|
const env = process.env.SKS_CODEX_BIN || process.env.DCODEX_CODEX_BIN || process.env.CODEX_BIN;
|
|
@@ -25,17 +26,22 @@ export async function getCodexInfo() {
|
|
|
25
26
|
return { bin, version, available: Boolean(bin) };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export
|
|
29
|
-
const bin = await findCodexBinary();
|
|
30
|
-
if (!bin) {
|
|
31
|
-
return { code: 127, stdout: '', stderr: 'Codex CLI not found. Install @openai/codex or set SKS_CODEX_BIN.' };
|
|
32
|
-
}
|
|
29
|
+
export function buildCodexExecArgs({ root, prompt, outputFile, json = true, profile = null, extraArgs = [] }) {
|
|
33
30
|
const args = ['exec', '--cd', root];
|
|
34
31
|
if (profile) args.push('--profile', profile);
|
|
35
32
|
if (json) args.push('--json');
|
|
36
33
|
if (outputFile) args.push('--output-last-message', outputFile);
|
|
37
|
-
args.push(...extraArgs);
|
|
34
|
+
args.push(...forceGpt55CodexArgs(extraArgs));
|
|
38
35
|
args.push(prompt);
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runCodexExec({ root, prompt, outputFile, json = true, profile = null, extraArgs = [], onStdout, onStderr, logDir = null, stdoutFile = null, stderrFile = null, maxBufferBytes = 256 * 1024, timeoutMs = null }) {
|
|
40
|
+
const bin = await findCodexBinary();
|
|
41
|
+
if (!bin) {
|
|
42
|
+
return { code: 127, stdout: '', stderr: 'Codex CLI not found. Install @openai/codex or set SKS_CODEX_BIN.' };
|
|
43
|
+
}
|
|
44
|
+
const args = buildCodexExecArgs({ root, prompt, outputFile, json, profile, extraArgs });
|
|
39
45
|
const effectiveTimeoutMs = Number(timeoutMs || process.env.SKS_CODEX_TIMEOUT_MS || process.env.DCODEX_CODEX_TIMEOUT_MS || 30 * 60 * 1000);
|
|
40
46
|
return runProcess(bin, args, {
|
|
41
47
|
cwd: root,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const REQUIRED_CODEX_MODEL = 'gpt-5.5';
|
|
2
|
+
|
|
3
|
+
const MODEL_VALUE_FLAGS = new Set(['--model', '-m']);
|
|
4
|
+
const CONFIG_VALUE_FLAGS = new Set(['-c', '--config']);
|
|
5
|
+
|
|
6
|
+
function isModelConfigOverride(value = '') {
|
|
7
|
+
return /^model\s*=/.test(String(value || '').trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripCodexModelOverrides(args = []) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const input = Array.isArray(args) ? args : [];
|
|
13
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
14
|
+
const arg = String(input[i]);
|
|
15
|
+
if (MODEL_VALUE_FLAGS.has(arg)) {
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg.startsWith('--model=') || arg.startsWith('-m=')) continue;
|
|
20
|
+
if (CONFIG_VALUE_FLAGS.has(arg)) {
|
|
21
|
+
const value = i + 1 < input.length ? String(input[i + 1]) : '';
|
|
22
|
+
if (isModelConfigOverride(value)) {
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
out.push(arg);
|
|
27
|
+
if (i + 1 < input.length) out.push(String(input[++i]));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg.startsWith('-c=') || arg.startsWith('--config=')) {
|
|
31
|
+
const value = arg.slice(arg.indexOf('=') + 1);
|
|
32
|
+
if (isModelConfigOverride(value)) continue;
|
|
33
|
+
}
|
|
34
|
+
out.push(arg);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function forceGpt55CodexArgs(args = []) {
|
|
40
|
+
return ['--model', REQUIRED_CODEX_MODEL, ...stripCodexModelOverrides(args)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function forceGpt55CodexConfigArgs(args = []) {
|
|
44
|
+
return ['-c', `model="${REQUIRED_CODEX_MODEL}"`, ...stripCodexModelOverrides(args)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isForbiddenCodexModel(value = '') {
|
|
48
|
+
const model = String(value || '').trim().toLowerCase();
|
|
49
|
+
return /^gpt-5\./.test(model) && model !== REQUIRED_CODEX_MODEL;
|
|
50
|
+
}
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -137,9 +137,14 @@ export function classifyCommand(command = '') {
|
|
|
137
137
|
const low = c.toLowerCase();
|
|
138
138
|
const reasons = [];
|
|
139
139
|
if (!low.trim()) return { level: 'none', kind: 'none', reasons: [], command: c };
|
|
140
|
+
const supabaseMigrationApply = [];
|
|
141
|
+
if (/\bsupabase\s+migration\s+up\b/.test(low)) supabaseMigrationApply.push('supabase_migration_up', 'supabase_migration_apply');
|
|
142
|
+
if (/\bsupabase\s+db\s+push\b/.test(low)) supabaseMigrationApply.push('supabase_db_push', 'supabase_migration_apply');
|
|
143
|
+
const supabaseMigrationRead = [];
|
|
144
|
+
if (/\bsupabase\s+db\s+(diff|pull)\b/.test(low)) supabaseMigrationRead.push('supabase_migration_schema_read');
|
|
145
|
+
if (/\bsupabase\s+migration\s+(list|new|squash)\b/.test(low)) supabaseMigrationRead.push('supabase_migration_file_work');
|
|
140
146
|
const hard = [
|
|
141
147
|
[/\bsupabase\s+db\s+reset\b/, 'supabase_db_reset'],
|
|
142
|
-
[/\bsupabase\s+db\s+push\b/, 'supabase_db_push'],
|
|
143
148
|
[/\bsupabase\s+migration\s+repair\b/, 'supabase_migration_repair'],
|
|
144
149
|
[/\bprisma\s+migrate\s+reset\b/, 'prisma_migrate_reset'],
|
|
145
150
|
[/\bprisma\s+db\s+push\b/, 'prisma_db_push'],
|
|
@@ -152,6 +157,25 @@ export function classifyCommand(command = '') {
|
|
|
152
157
|
const maybeSql = extractSqlLiterals(c).join('\n');
|
|
153
158
|
const sqlClass = maybeSql ? classifySql(maybeSql) : { level: 'none', reasons: [] };
|
|
154
159
|
if (reasons.length) return { level: 'destructive', kind: 'db_command', reasons, sql: sqlClass, command: c };
|
|
160
|
+
if (supabaseMigrationApply.length) {
|
|
161
|
+
const level = sqlClass.level === 'destructive' ? 'destructive' : 'write';
|
|
162
|
+
return {
|
|
163
|
+
level,
|
|
164
|
+
kind: 'db_migration',
|
|
165
|
+
reasons: [...new Set([...supabaseMigrationApply, ...(sqlClass.reasons || [])])],
|
|
166
|
+
sql: sqlClass,
|
|
167
|
+
command: c
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (supabaseMigrationRead.length && !['write', 'destructive'].includes(sqlClass.level)) {
|
|
171
|
+
return {
|
|
172
|
+
level: 'safe',
|
|
173
|
+
kind: 'db_migration',
|
|
174
|
+
reasons: [...new Set([...supabaseMigrationRead, ...(sqlClass.reasons || [])])],
|
|
175
|
+
sql: sqlClass,
|
|
176
|
+
command: c
|
|
177
|
+
};
|
|
178
|
+
}
|
|
155
179
|
if (/\b(psql|supabase|prisma|drizzle-kit|knex|sequelize)\b/.test(low)) {
|
|
156
180
|
if (sqlClass.level === 'destructive' || sqlClass.level === 'write') return { level: sqlClass.level, kind: 'db_command', reasons: sqlClass.reasons, sql: sqlClass, command: c };
|
|
157
181
|
return { level: sqlClass.level === 'safe' ? 'safe' : 'possible_db', kind: 'db_command', reasons: sqlClass.reasons, sql: sqlClass, command: c };
|
|
@@ -199,10 +223,15 @@ export function classifyToolPayload(payload = {}) {
|
|
|
199
223
|
const sqlClass = classifySql(combined);
|
|
200
224
|
const commandClass = classifyCommand(strings.find((s) => /\b(supabase|psql|prisma|drizzle|knex|sequelize)\b/i.test(s)) || '');
|
|
201
225
|
const toolReasons = [];
|
|
226
|
+
const reasons = [];
|
|
202
227
|
if (/\b(apply_patch|edit|write|create|remove|rename|str_replace|file_write|fs_write)\b/i.test(toolName) && !/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) {
|
|
203
|
-
return { level: 'none', toolName, toolReasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
|
|
228
|
+
return { level: 'none', toolName, toolReasons, reasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
|
|
204
229
|
}
|
|
205
230
|
if (/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) toolReasons.push('database_tool');
|
|
231
|
+
if (/apply_migration|migration_apply/i.test(toolName)) {
|
|
232
|
+
toolReasons.push('migration_apply_tool');
|
|
233
|
+
reasons.push('supabase_migration_apply');
|
|
234
|
+
}
|
|
206
235
|
if (/delete_project|pause_project|restore_project|delete_branch|reset_branch|merge_branch/.test(toolName)) toolReasons.push('dangerous_supabase_management_tool');
|
|
207
236
|
let level = 'none';
|
|
208
237
|
for (const candidate of [sqlClass.level, commandClass.level]) {
|
|
@@ -211,8 +240,9 @@ export function classifyToolPayload(payload = {}) {
|
|
|
211
240
|
else if ((candidate === 'safe' || candidate === 'possible_db') && level === 'none') level = candidate;
|
|
212
241
|
}
|
|
213
242
|
if (toolReasons.includes('dangerous_supabase_management_tool')) level = 'destructive';
|
|
243
|
+
if (toolReasons.includes('migration_apply_tool') && level !== 'destructive') level = 'write';
|
|
214
244
|
if (toolReasons.includes('database_tool') && level === 'none') level = 'possible_db';
|
|
215
|
-
return { level, toolName, toolReasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
|
|
245
|
+
return { level, toolName, toolReasons, reasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
function contractAllowsDbWrite(contract = {}) {
|
|
@@ -290,6 +320,12 @@ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_PO
|
|
|
290
320
|
}
|
|
291
321
|
if (cls.level === 'destructive') reasons.push('destructive_database_operation_blocked_always');
|
|
292
322
|
if (cls.level === 'write') {
|
|
323
|
+
const reasonSet = new Set([
|
|
324
|
+
...(cls.reasons || []),
|
|
325
|
+
...(cls.sql?.reasons || []),
|
|
326
|
+
...(cls.command?.reasons || [])
|
|
327
|
+
]);
|
|
328
|
+
if (reasonSet.has('supabase_db_push')) reasons.push('supabase_db_push_requires_active_mad_sks');
|
|
293
329
|
if (effective.mode === 'read_only_only') reasons.push('database_write_mode_is_read_only_only');
|
|
294
330
|
if (effective.env === 'production' || effective.env === 'production_read_only') reasons.push('production_database_writes_forbidden');
|
|
295
331
|
if (!['local_dev', 'preview_branch', 'supabase_branch'].includes(effective.env)) reasons.push('database_write_target_not_local_or_branch');
|
package/src/core/fsx.mjs
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
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.55';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -6,6 +6,7 @@ import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from '.
|
|
|
6
6
|
import { checkHarnessModification, harnessGuardBlockReason } from './harness-guard.mjs';
|
|
7
7
|
import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext as routePipelineContext, recordContext7Evidence, recordSubagentEvidence, routePrompt } from './pipeline.mjs';
|
|
8
8
|
import { classifyToolError } from './evaluation.mjs';
|
|
9
|
+
import { REQUIRED_CODEX_MODEL, isForbiddenCodexModel } from './codex-model-guard.mjs';
|
|
9
10
|
|
|
10
11
|
const TEAM_DIGEST_MAX_EVENTS = 4;
|
|
11
12
|
const TEAM_DIGEST_MESSAGE_CHARS = 180;
|
|
@@ -91,7 +92,11 @@ export async function hookMain(name) {
|
|
|
91
92
|
const root = await projectRoot(payload.cwd || process.cwd());
|
|
92
93
|
const state = await loadState(root);
|
|
93
94
|
const noQuestion = isNoQuestionRunning(state);
|
|
94
|
-
if (name === 'user-prompt-submit')
|
|
95
|
+
if (name === 'user-prompt-submit') {
|
|
96
|
+
const modelBlock = blockForbiddenClientModel(payload);
|
|
97
|
+
if (modelBlock) return modelBlock;
|
|
98
|
+
return hookUserPrompt(root, state, payload, noQuestion);
|
|
99
|
+
}
|
|
95
100
|
if (name === 'pre-tool') return hookPreTool(root, state, payload, noQuestion);
|
|
96
101
|
if (name === 'post-tool') return hookPostTool(root, state, payload, noQuestion);
|
|
97
102
|
if (name === 'permission-request') return hookPermission(root, state, payload, noQuestion);
|
|
@@ -99,6 +104,43 @@ export async function hookMain(name) {
|
|
|
99
104
|
return { continue: true };
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
function blockForbiddenClientModel(payload = {}) {
|
|
108
|
+
const model = forbiddenClientModelFromPayload(payload);
|
|
109
|
+
if (!model || !isForbiddenCodexModel(model)) return null;
|
|
110
|
+
return {
|
|
111
|
+
decision: 'block',
|
|
112
|
+
reason: `SKS requires ${REQUIRED_CODEX_MODEL}; client payload requested ${model}. Switch the Codex client/session model to ${REQUIRED_CODEX_MODEL} and retry.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function forbiddenClientModelFromPayload(payload = {}) {
|
|
117
|
+
const candidates = [
|
|
118
|
+
payload.model,
|
|
119
|
+
payload.model_id,
|
|
120
|
+
payload.modelId,
|
|
121
|
+
payload.client_model,
|
|
122
|
+
payload.clientModel,
|
|
123
|
+
...clientModelCandidates(payload.client),
|
|
124
|
+
...clientModelCandidates(payload.metadata),
|
|
125
|
+
...clientModelCandidates(payload.context),
|
|
126
|
+
...clientModelCandidates(payload.thread),
|
|
127
|
+
...clientModelCandidates(payload.session)
|
|
128
|
+
];
|
|
129
|
+
return candidates.find((value) => typeof value === 'string' && isForbiddenCodexModel(value)) || '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function clientModelCandidates(value, depth = 0) {
|
|
133
|
+
if (!value || typeof value !== 'object' || depth > 4) return [];
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const key of ['model', 'model_id', 'modelId', 'client_model', 'clientModel']) {
|
|
136
|
+
if (typeof value[key] === 'string') out.push(value[key]);
|
|
137
|
+
}
|
|
138
|
+
for (const key of ['client', 'metadata', 'context', 'thread', 'session']) {
|
|
139
|
+
out.push(...clientModelCandidates(value[key], depth + 1));
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
async function hookUserPrompt(root, state, payload, noQuestion) {
|
|
103
145
|
if (!noQuestion) {
|
|
104
146
|
const prompt = extractUserPrompt(payload);
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
|
|
5
5
|
import { getCodexInfo } from './codex-adapter.mjs';
|
|
6
6
|
import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
|
|
7
|
+
import { REQUIRED_CODEX_MODEL, forceGpt55CodexArgs } from './codex-model-guard.mjs';
|
|
7
8
|
import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
|
|
8
9
|
|
|
9
10
|
export const SKS_TMUX_LOGO = [
|
|
@@ -114,7 +115,7 @@ const SKS_TMUX_LOGO_ANIMATION_STEPS = Object.freeze([
|
|
|
114
115
|
{ frame: 9, color: '51', bold: true, delay: '0.16' }
|
|
115
116
|
]);
|
|
116
117
|
|
|
117
|
-
export const DEFAULT_SKS_CODEX_MODEL =
|
|
118
|
+
export const DEFAULT_SKS_CODEX_MODEL = REQUIRED_CODEX_MODEL;
|
|
118
119
|
export const DEFAULT_SKS_CODEX_REASONING = 'high';
|
|
119
120
|
|
|
120
121
|
export function defaultCodexLaunchArgs(env = process.env) {
|
|
@@ -200,7 +201,7 @@ export function tmuxStatusKind(tmux = {}) {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
export function codexLaunchCommand(root, codexBin, codexArgs = []) {
|
|
203
|
-
const extraArgs =
|
|
204
|
+
const extraArgs = forceGpt55CodexArgs(codexArgs);
|
|
204
205
|
return [
|
|
205
206
|
sksLogoIntroCommand(codexBin),
|
|
206
207
|
`printf '\\nProject: %s\\n' ${shellEscape(root)}`,
|
|
@@ -322,7 +323,7 @@ export async function buildTmuxLaunchPlan(opts = {}) {
|
|
|
322
323
|
const codex = opts.codex || await getCodexInfo().catch(() => ({}));
|
|
323
324
|
const tmux = opts.tmux || await tmuxReadiness(opts);
|
|
324
325
|
const app = opts.app || await codexAppIntegrationStatus({ codex });
|
|
325
|
-
const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env);
|
|
326
|
+
const codexArgs = forceGpt55CodexArgs(Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env));
|
|
326
327
|
return {
|
|
327
328
|
root,
|
|
328
329
|
session,
|