Skip to content

Commit c79b190

Browse files
committed
chore: merge main into xAI provider branch
2 parents 72d596a + 5e46b7b commit c79b190

13 files changed

Lines changed: 207 additions & 21 deletions

File tree

backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ model SupplementalPostScriptRun {
256256
modelProvider String? @map("model_provider")
257257
harness String?
258258
thinkingEffort String? @map("thinking_effort")
259+
retryOfRunId BigInt? @unique(map: "supplemental_post_script_runs_retry_of_run_id_key") @map("retry_of_run_id")
259260
extra Json @default("{}")
260261
status String @default("queued")
261262
targetCount Int @default(0) @map("target_count")

backend/src/lib/serialize.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ export function serializeSupplementalPostScriptRun(run, targets = []) {
471471
modelProvider: run.modelProvider ?? null,
472472
harness: run.harness ?? null,
473473
thinkingEffort: run.thinkingEffort ?? null,
474+
retryOfRunId: run.retryOfRunId?.toString() ?? null,
474475
status: run.status,
475476
targetCount: run.targetCount,
476477
completedCount: run.completedCount,

backend/src/routes/scans.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,15 @@ function supplementalModelSelection(scan, body = {}, fallbackRun = null) {
218218
});
219219
}
220220

221-
async function createSupplementalRunRecords(tx, scanId, postScript, vulnerabilityIds, extra, selection) {
221+
async function createSupplementalRunRecords(
222+
tx,
223+
scanId,
224+
postScript,
225+
vulnerabilityIds,
226+
extra,
227+
selection,
228+
{ retryOfRunId = null } = {}
229+
) {
222230
const run = await tx.supplementalPostScriptRun.create({
223231
data: {
224232
scanId,
@@ -230,6 +238,7 @@ async function createSupplementalRunRecords(tx, scanId, postScript, vulnerabilit
230238
modelProvider: selection.modelProvider,
231239
harness: selection.harness,
232240
thinkingEffort: selection.thinkingEffort,
241+
...(retryOfRunId === null ? {} : { retryOfRunId }),
233242
extra,
234243
targetCount: vulnerabilityIds.length,
235244
},
@@ -306,6 +315,12 @@ export async function retrySupplementalPostScriptRun(tx, scanId, runId, body = {
306315
return { kind: 'run-active', status: original.status };
307316
}
308317

318+
const existingRetry = await tx.supplementalPostScriptRun.findFirst({
319+
where: { scanId, retryOfRunId: runId },
320+
select: { id: true },
321+
});
322+
if (existingRetry) return { kind: 'already-retried', retryRunId: existingRetry.id };
323+
309324
const failedTargets = await tx.supplementalPostScriptTarget.findMany({
310325
where: { runId, scanId, status: 'failed' },
311326
select: { vulnerabilityId: true },
@@ -339,7 +354,8 @@ export async function retrySupplementalPostScriptRun(tx, scanId, runId, body = {
339354
},
340355
vulnerabilityIds,
341356
original.extra,
342-
selection
357+
selection,
358+
{ retryOfRunId: original.id }
343359
);
344360
}
345361

@@ -802,6 +818,11 @@ router.post('/:id/supplemental-post-script-runs/:runId/retry', async (req, res,
802818
if (result.kind === 'no-failures') {
803819
return res.status(409).json({ error: 'This supplemental run has no failed findings to retry.' });
804820
}
821+
if (result.kind === 'already-retried') {
822+
return res.status(409).json({
823+
error: `These failed findings were already re-run as supplemental run ${result.retryRunId}.`,
824+
});
825+
}
805826
res.status(201).json(serializeSupplementalPostScriptRun(result.run, result.targets));
806827
} catch (e) {
807828
next(e);

backend/test/dataIntegrity.test.js

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -908,20 +908,23 @@ test('supplemental retry clones the snapshot and extras but queues only failed f
908908
}),
909909
},
910910
supplementalPostScriptRun: {
911-
findFirst: async () => ({
912-
id: 71n,
913-
scanId: 9n,
914-
status: 'completed_with_errors',
915-
postScriptId: 4n,
916-
postScriptName: 'Network report',
917-
postScriptContent: 'Inspect {{extra.network}}',
918-
postScriptOutputFormat: '{"note":"string"}',
919-
model: 'old-model',
920-
modelProvider: 'codex',
921-
harness: 'codex',
922-
thinkingEffort: 'medium',
923-
extra: { network: 'mainnet' },
924-
}),
911+
findFirst: async ({ where }) =>
912+
where.retryOfRunId
913+
? null
914+
: {
915+
id: 71n,
916+
scanId: 9n,
917+
status: 'completed_with_errors',
918+
postScriptId: 4n,
919+
postScriptName: 'Network report',
920+
postScriptContent: 'Inspect {{extra.network}}',
921+
postScriptOutputFormat: '{"note":"string"}',
922+
model: 'old-model',
923+
modelProvider: 'codex',
924+
harness: 'codex',
925+
thinkingEffort: 'medium',
926+
extra: { network: 'mainnet' },
927+
},
925928
create: async ({ data }) => {
926929
created.push(data);
927930
return {
@@ -965,6 +968,7 @@ test('supplemental retry clones the snapshot and extras but queues only failed f
965968
modelProvider: 'openrouter',
966969
harness: 'codex',
967970
thinkingEffort: 'high',
971+
retryOfRunId: 71n,
968972
extra: { network: 'mainnet' },
969973
targetCount: 2,
970974
});
@@ -974,6 +978,22 @@ test('supplemental retry clones the snapshot and extras but queues only failed f
974978
);
975979
});
976980

981+
test('supplemental retry cannot be queued twice from the same failed run', async () => {
982+
const tx = {
983+
$queryRaw: async () => [],
984+
scan: { findUnique: async () => ({ id: 9n, status: 'completed' }) },
985+
supplementalPostScriptRun: {
986+
findFirst: async ({ where }) =>
987+
where.retryOfRunId ? { id: 72n } : { id: 71n, scanId: 9n, status: 'completed_with_errors' },
988+
},
989+
};
990+
991+
assert.deepEqual(await retrySupplementalPostScriptRun(tx, 9n, 71n), {
992+
kind: 'already-retried',
993+
retryRunId: 72n,
994+
});
995+
});
996+
977997
test('supplemental creation rejects scans that can still execute automatically', async () => {
978998
const tx = {
979999
$queryRaw: async () => [],

backend/test/scanPresentation.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ test('supplemental run serialization exposes model settings and safe target erro
8080
modelProvider: 'codex',
8181
harness: 'codex',
8282
thinkingEffort: 'high',
83+
retryOfRunId: 40n,
8384
status: 'completed_with_errors',
8485
targetCount: 1,
8586
completedCount: 0,
@@ -101,6 +102,7 @@ test('supplemental run serialization exposes model settings and safe target erro
101102
assert.equal(serialized.model, 'gpt-5-codex');
102103
assert.equal(serialized.modelProvider, 'codex');
103104
assert.equal(serialized.thinkingEffort, 'high');
105+
assert.equal(serialized.retryOfRunId, '40');
104106
assert.equal(serialized.targets[0].error, 'The model timed out.');
105107
});
106108

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Record supplemental retry lineage so each failed run exposes at most one
2+
-- durable retry action. Additive and idempotent for existing installations.
3+
4+
ALTER TABLE workflows.supplemental_post_script_runs
5+
ADD COLUMN IF NOT EXISTS retry_of_run_id bigint;
6+
7+
CREATE UNIQUE INDEX IF NOT EXISTS supplemental_post_script_runs_retry_of_run_id_key
8+
ON workflows.supplemental_post_script_runs USING btree (retry_of_run_id);
9+
10+
COMMENT ON COLUMN workflows.supplemental_post_script_runs.retry_of_run_id IS
11+
'The failed supplemental run retried by this run; each run can be retried at most once.';

docs-site/scans/post-scripts.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ Supplemental runs are additive. Their output is attached as a new enrichment ins
3939
replacing an earlier result, and the findings list shows how many additional runs targeted
4040
each finding. Run progress and finding-level error messages remain visible in **Supplemental
4141
run history**. A run that completed with errors can requeue only its failed findings; its
42-
snapshotted script and extra values are reused while the model settings remain editable. A scan
43-
cannot be resumed or deleted while supplemental targets are queued or running.
42+
snapshotted script and extra values are reused while the model settings remain editable. Once
43+
that retry is queued, the original run no longer offers the retry action; if the retry itself
44+
fails, it can be retried once in turn. A scan cannot be resumed or deleted while supplemental
45+
targets are queued or running.
4446

4547
<Note>
4648
Post-scripts are chosen per scan, independent of the workflow - so you can reuse the same

engine/open_kritt_engine/harnesses.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ class CodexJsonlResult:
178178
"glm-5.2": "z-ai/glm-5.2",
179179
"grok-4.5": "x-ai/grok-4.5",
180180
}
181+
CLAUDE_OPENROUTER_MODEL_ENV_KEYS = (
182+
"ANTHROPIC_MODEL",
183+
"ANTHROPIC_DEFAULT_MODEL",
184+
"ANTHROPIC_DEFAULT_FABLE_MODEL",
185+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
186+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
187+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
188+
"CLAUDE_CODE_SUBAGENT_MODEL",
189+
)
181190
CLAUDE_MODEL_ALIASES = {
182191
"opus-4.7": "claude-opus-4-7",
183192
"opus-4.8": "claude-opus-4-8",
@@ -890,6 +899,7 @@ def _scan_docker_command(
890899
"ANTHROPIC_BASE_URL",
891900
"ANTHROPIC_AUTH_TOKEN",
892901
"ANTHROPIC_API_KEY",
902+
*CLAUDE_OPENROUTER_MODEL_ENV_KEYS,
893903
"CODEX_MODEL_PROVIDER",
894904
"CLAUDE_CODE_MODEL_PROVIDER",
895905
"CURSOR_API_KEY",
@@ -1041,9 +1051,12 @@ def _claude_env(env: dict[str, str], model: str, model_provider: str | None = No
10411051
if claude_model_provider(model, actual_env, model_provider) == "openrouter":
10421052
if not actual_env.get("OPENROUTER_API_KEY"):
10431053
raise HarnessError("OPENROUTER_API_KEY is required when model provider is openrouter")
1054+
routed_model = OPENROUTER_MODEL_ALIASES.get(model, model)
10441055
actual_env["ANTHROPIC_BASE_URL"] = actual_env.get("ANTHROPIC_BASE_URL") or OPENROUTER_CLAUDE_BASE_URL
10451056
actual_env["ANTHROPIC_AUTH_TOKEN"] = actual_env.get("ANTHROPIC_AUTH_TOKEN") or actual_env["OPENROUTER_API_KEY"]
10461057
actual_env["ANTHROPIC_API_KEY"] = ""
1058+
for key in CLAUDE_OPENROUTER_MODEL_ENV_KEYS:
1059+
actual_env[key] = routed_model
10471060
return actual_env
10481061

10491062

@@ -1757,6 +1770,20 @@ def run(
17571770
"--append-system-prompt",
17581771
CLAUDE_WORKSPACE_SYSTEM_PROMPT if allow_tools else CLAUDE_GENERATION_SYSTEM_PROMPT,
17591772
]
1773+
if provider == "openrouter":
1774+
cmd.extend(
1775+
[
1776+
"--settings",
1777+
json.dumps(
1778+
{
1779+
"availableModels": [model],
1780+
"enforceAvailableModels": True,
1781+
"model": model,
1782+
},
1783+
separators=(",", ":"),
1784+
),
1785+
]
1786+
)
17601787
if allow_tools:
17611788
cmd.extend(["--dangerously-skip-permissions", "--tools", "default"])
17621789
else:

engine/open_kritt_engine/model_catalog.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
XAI_DEFAULT_MODEL_ID = "grok-4.6"
2929
XAI_THINKING_EFFORTS = ("low", "medium", "high")
3030
XAI_GROK_46_THINKING_EFFORTS = (*XAI_THINKING_EFFORTS, "xhigh")
31+
OPENROUTER_MODEL_THINKING_EFFORTS = {
32+
"stealth/ox-alpha": ("low", "high", "max"),
33+
}
3134
CATALOG_REFRESH_ERROR = "Unable to refresh the provider model catalog."
3235
MAX_CATALOG_MODELS = 500
3336
MAX_CATALOG_PAGES = 10
@@ -443,11 +446,14 @@ def fetch_openrouter_models(api_key: str, timeout_seconds: float) -> tuple[list[
443446
if not model_id or model_id in seen_ids:
444447
continue
445448
seen_ids.add(model_id)
449+
thinking_efforts = OPENROUTER_MODEL_THINKING_EFFORTS.get(model_id)
446450
entries.append(
447451
{
448452
"model": model_id,
449453
"displayName": raw.get("name"),
450-
"supportedReasoningEfforts": _openrouter_thinking_efforts(raw),
454+
"supportedReasoningEfforts": (
455+
list(thinking_efforts) if thinking_efforts else _openrouter_thinking_efforts(raw)
456+
),
451457
"isDefault": model_id == OPENROUTER_DEFAULT_MODEL_ID,
452458
}
453459
)

engine/tests/test_engine.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,6 +1427,14 @@ def fake_run_process(cmd, prompt, cwd, timeout, env=None):
14271427
assert captured["env"]["ANTHROPIC_BASE_URL"] == "https://openrouter.ai/api"
14281428
assert captured["env"]["ANTHROPIC_AUTH_TOKEN"] == "or-key"
14291429
assert captured["env"]["ANTHROPIC_API_KEY"] == ""
1430+
for key in harnesses.CLAUDE_OPENROUTER_MODEL_ENV_KEYS:
1431+
assert captured["env"][key] == "z-ai/glm-5.2"
1432+
settings = json.loads(captured["cmd"][captured["cmd"].index("--settings") + 1])
1433+
assert settings == {
1434+
"availableModels": ["z-ai/glm-5.2"],
1435+
"enforceAvailableModels": True,
1436+
"model": "z-ai/glm-5.2",
1437+
}
14301438
# The subprocess environment is authoritative in both containers and local
14311439
# development; credentials must never be serialized into process arguments.
14321440
if captured["cmd"][:3] == ["runuser", "-u", "nobody"]:

0 commit comments

Comments
 (0)