Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dashboard/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@
"cost": "Cost",
"failure": "Failure code",
"dataset": "Frozen dataset",
"llmSnapshot": "Frozen LLM & pricing",
"seedReport": "Seed report",
"baseline": "Market baseline",
"notAvailable": "Not available yet",
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@
"cost": "费用",
"failure": "失败码",
"dataset": "冻结数据集",
"llmSnapshot": "冻结 LLM 与定价",
"seedReport": "种子报告",
"baseline": "市场基准",
"notAvailable": "尚不可用",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async function token(
provider: "deepseek",
operation_id: "operation-1",
llm_config_digest: "a".repeat(64),
request_digest: "b".repeat(64),
...overrides,
})
.setProtectedHeader({ alg: "EdDSA" })
Expand Down Expand Up @@ -76,6 +77,7 @@ describe("internal owner LLM credential route", () => {
const now = Math.floor(Date.now() / 1_000);
const requests = [
callRoute(`Bearer ${await token({ token_use: "session" })}`),
callRoute(`Bearer ${await token({ request_digest: "missing-scope" })}`),
callRoute(`Bearer ${await token({}, { issuedAt: null })}`),
callRoute(`Bearer ${await token({}, { issuedAt: now - 108_100, expiresAt: now + 1 })}`),
callRoute(`Bearer ${await token({}, { issuedAt: now + 60, expiresAt: now + 120 })}`),
Expand All @@ -84,6 +86,7 @@ describe("internal owner LLM credential route", () => {
];

expect((await Promise.all(requests)).map((response) => response.status)).toEqual([
403,
403,
401,
403,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export async function GET(
payload.operation_id.length < 8 ||
typeof payload.llm_config_digest !== "string" ||
!/^[0-9a-f]{64}$/.test(payload.llm_config_digest) ||
typeof payload.request_digest !== "string" ||
!/^[0-9a-f]{64}$/.test(payload.request_digest) ||
typeof payload.jti !== "string" ||
!payload.jti ||
typeof payload.sub !== "string" ||
Expand All @@ -68,15 +70,16 @@ export async function GET(
try {
recorded = await getPool().query(
`INSERT INTO evolution_credential_grant_uses
(jti,owner_sub,config_id,operation_id,config_digest,consumed_at)
VALUES ($1,$2,$3,$4,$5,NOW())
(jti,owner_sub,config_id,operation_id,config_digest,request_digest,consumed_at)
VALUES ($1,$2,$3,$4,$5,$6,NOW())
ON CONFLICT (jti) DO UPDATE SET
redemption_count=evolution_credential_grant_uses.redemption_count+1,
last_redeemed_at=NOW()
WHERE evolution_credential_grant_uses.owner_sub=EXCLUDED.owner_sub
AND evolution_credential_grant_uses.config_id=EXCLUDED.config_id
AND evolution_credential_grant_uses.operation_id=EXCLUDED.operation_id
AND evolution_credential_grant_uses.config_digest=EXCLUDED.config_digest
AND evolution_credential_grant_uses.request_digest=EXCLUDED.request_digest
AND evolution_credential_grant_uses.redemption_count<2
AND evolution_credential_grant_uses.consumed_at>=NOW()-INTERVAL '2 minutes'
RETURNING jti`,
Expand All @@ -86,6 +89,7 @@ export async function GET(
configId,
payload.operation_id,
payload.llm_config_digest,
payload.request_digest,
],
);
} catch {
Expand Down
22 changes: 21 additions & 1 deletion apps/dashboard/src/components/evolution/EvolutionRunData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useTranslations } from "next-intl";
import type { EvolutionRun } from "@/lib/types";
import { Panel } from "@/components/ui/Panel";

/** 展示 seed、基准、冻结数据 manifest 与失败信息。 */
/** 展示 seed、冻结 LLM/数据快照、基准与失败信息。 */
export function EvolutionRunData({ run }: { run: EvolutionRun }) {
const t = useTranslations("evolution.detail");
return (
Expand All @@ -25,6 +25,13 @@ export function EvolutionRunData({ run }: { run: EvolutionRun }) {
</p>
)}
</Panel>
<Panel title={t("llmSnapshot")}>
<ObjectRows
value={llmSnapshotRows(run)}
empty={t("notAvailable")}
preferred={["provider", "model", "config_id", "pricing_version", "estimated_max_usd_per_candidate", "config_digest"]}
/>
</Panel>
<Panel title={t("dataset")}>
<ObjectRows
value={run.dataset_manifest}
Expand All @@ -42,6 +49,19 @@ export function EvolutionRunData({ run }: { run: EvolutionRun }) {
);
}

function llmSnapshotRows(run: EvolutionRun): Record<string, unknown> | null {
const snapshot = run.llm_snapshot;
if (!snapshot) return null;
return {
provider: snapshot.provider,
model: snapshot.model,
config_id: snapshot.config_id,
pricing_version: snapshot.pricing.version,
estimated_max_usd_per_candidate: snapshot.pricing.estimated_max_usd_per_candidate,
config_digest: run.llm_config_digest ?? snapshot.config_digest,
};
}

function Item({ label, value }: { label: string; value: string }) {
return <div className="min-w-0"><dt className="font-mono text-[10px] uppercase tracking-wider text-fg-muted">{label}</dt><dd className="mt-1 break-all font-mono text-xs text-fg">{value}</dd></div>;
}
Expand Down
19 changes: 19 additions & 0 deletions apps/dashboard/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,23 @@ export type EvolutionRunStatus =
| "failed"
| "aborted";

export interface EvolutionLLMSnapshot {
config_id: string;
provider: "deepseek" | "openai" | "kimi" | "zhipu";
model: string;
base_url: string | null;
pricing: {
version: string;
currency: "USD";
input_usd_per_million: number;
output_usd_per_million: number;
assumed_input_tokens: number;
max_output_tokens: number;
estimated_max_usd_per_candidate: number;
};
config_digest: string;
}

export interface EvolutionCandidateSummary {
candidate_id: string;
run_id: string;
Expand Down Expand Up @@ -568,6 +585,8 @@ export interface EvolutionRunSummary {
seed_strategy_id: string;
budget: number;
config: Record<string, unknown>;
llm_snapshot: EvolutionLLMSnapshot | null;
llm_config_digest: string | null;
status: EvolutionRunStatus;
active_stage: string | null;
llm_cost_usd: number;
Expand Down
43 changes: 43 additions & 0 deletions apps/dashboard/src/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest } from "next/server";
import { afterEach, describe, expect, it, vi } from "vitest";

vi.mock("next-intl/middleware", () => ({
default: () => vi.fn(() => new Response(null, { status: 200 })),
}));

afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
});

/** Load middleware after setting production auth flags because they are module constants. */
async function productionMiddleware() {
vi.stubEnv("NODE_ENV", "production");
vi.stubEnv("AUTH_ENABLED", "true");
return (await import("./proxy")).default;
}

describe("dashboard production proxy", () => {
it("lets the exact internal credential exchange reach its Ed25519 route verifier", async () => {
const middleware = await productionMiddleware();
const response = await middleware(
new NextRequest("http://dashboard.test/api/internal/llm-config/config-1", {
headers: { Authorization: "Bearer signed-grant" },
}),
);

expect(response.status).toBe(200);
expect(response.headers.get("x-middleware-next")).toBe("1");
});

it("keeps other cookie-less API paths behind the session gate", async () => {
const middleware = await productionMiddleware();
const response = await middleware(
new NextRequest("http://dashboard.test/api/evolution", {
headers: { Authorization: "Bearer signed-grant" },
}),
);

expect(response.status).toBe(401);
});
});
6 changes: 5 additions & 1 deletion apps/dashboard/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ export default async function middleware(req: NextRequest): Promise<Response> {
const { pathname } = req.nextUrl;
const isApi = pathname.startsWith("/api");
// 公开:登录页 + 登录/登出/会话 API(否则登录前无从进入)。
const isPublic = pathname === "/login" || pathname.startsWith("/api/auth/");
// Evolver 凭据兑换只接受路由内自行验签的 Ed25519 Bearer grant,不依赖浏览器 cookie。
const isCredentialExchange =
req.method === "GET" && /^\/api\/internal\/llm-config\/[^/]+$/.test(pathname);
const isPublic =
pathname === "/login" || pathname.startsWith("/api/auth/") || isCredentialExchange;

if (AUTH_ENABLED && !isPublic && !(await hasValidSession(req))) {
if (isApi) {
Expand Down
1 change: 1 addition & 0 deletions infra/docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ services:
EVOLVER_LLM_TIMEOUT_S: ${EVOLVER_LLM_TIMEOUT_S:-120}
# 签名私钥只属于 orchestration;覆盖 svc-common env_file,防 Evolver 自签 owner grant。
EVOLUTION_CREDENTIAL_PRIVATE_KEY_B64: ""
EVOLUTION_CREDENTIAL_PUBLIC_KEY_B64: ${EVOLUTION_CREDENTIAL_PUBLIC_KEY_B64:?required}
deploy:
replicas: 1
depends_on:
Expand Down
108 changes: 108 additions & 0 deletions infra/migrations/tests/test_migration_0042.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""0042 durable evolution approval operation ledger integration tests."""

from __future__ import annotations

from datetime import UTC, datetime, timedelta

import psycopg
import pytest
from migration_0038_support import alembic, db_url

pytestmark = pytest.mark.integration


def test_0042_persists_one_operation_per_owner_thread_request(
migration_db_url: str,
) -> None:
alembic(migration_db_url, "upgrade", "0041")
old_grant = (
"11111111-1111-4111-8111-111111111111",
"user:alice",
"config-1",
"operation-old",
"a" * 64,
)
with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn:
conn.execute(
"""INSERT INTO evolution_credential_grant_uses
(jti,owner_sub,config_id,operation_id,config_digest)
VALUES (%s,%s,%s,%s,%s)""",
old_grant,
)

alembic(migration_db_url, "upgrade", "0042")
identity = ("user:alice", "thread-1", "evolver.run_evolution", "a" * 64)
first = "50000000-0000-4000-8000-000000000001"
second = "50000000-0000-4000-8000-000000000002"
expires = datetime.now(UTC) + timedelta(hours=24)
with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn:
assert conn.execute(
"""SELECT request_digest FROM evolution_credential_grant_uses
WHERE jti=%s""",
(old_grant[0],),
).fetchone() == ("0" * 64,)
conn.execute(
"""INSERT INTO evolution_credential_grant_uses
(jti,owner_sub,config_id,operation_id,config_digest)
VALUES ('11111111-1111-4111-8111-111111111112',
'user:alice','config-1','operation-rolling',%s)""",
("a" * 64,),
)
assert conn.execute(
"""SELECT request_digest FROM evolution_credential_grant_uses
WHERE jti='11111111-1111-4111-8111-111111111112'"""
).fetchone() == ("0" * 64,)
conn.execute(
"""INSERT INTO strategy_evo_runs
(run_id,owner_account_id,requested_by_sub,seed_strategy_id,budget,config,
status,idempotency_key,request_hash,queued_at)
VALUES ('50000000-0000-4000-8000-000000000010',
'00000000-0000-0000-0000-000000000099','user:alice','seed',1,'{}',
'queued','old-image-compatible','old-hash',NOW())"""
)
assert conn.execute(
"""SELECT llm_snapshot_required,llm_credential_grant_required
FROM strategy_evo_runs
WHERE run_id='50000000-0000-4000-8000-000000000010'"""
).fetchone() == (False, False)
with pytest.raises(psycopg.errors.CheckViolation):
conn.execute(
"""INSERT INTO strategy_evo_runs
(run_id,owner_account_id,requested_by_sub,seed_strategy_id,budget,config,
status,idempotency_key,request_hash,queued_at,
llm_snapshot_required,llm_credential_grant_required)
VALUES ('50000000-0000-4000-8000-000000000011',
'00000000-0000-0000-0000-000000000099','user:alice','seed',1,'{}',
'queued','new-image-invalid','new-hash',NOW(),TRUE,TRUE)"""
)
conn.execute(
"""INSERT INTO evolution_approval_operations
(operation_id,auth_sub,session_id,tool_name,input_digest,expires_at)
VALUES (%s,%s,%s,%s,%s,%s)""",
(first, *identity, expires),
)
conn.execute(
"""INSERT INTO evolution_approval_operations
(operation_id,auth_sub,session_id,tool_name,input_digest,expires_at)
VALUES (%s,%s,%s,%s,%s,%s)
ON CONFLICT (auth_sub,session_id,tool_name,input_digest) DO UPDATE SET
operation_id=EXCLUDED.operation_id,approved_at=NOW(),expires_at=EXCLUDED.expires_at""",
(second, *identity, expires),
)
assert conn.execute(
"""SELECT operation_id::text FROM evolution_approval_operations
WHERE auth_sub=%s AND session_id=%s AND tool_name=%s AND input_digest=%s
AND expires_at>NOW()""",
identity,
).fetchone() == (second,)

alembic(migration_db_url, "downgrade", "0041")
with psycopg.connect(db_url(migration_db_url), autocommit=True) as conn:
assert conn.execute(
"SELECT to_regclass('evolution_approval_operations')"
).fetchone() == (None,)
assert conn.execute(
"""SELECT 1 FROM information_schema.columns
WHERE table_name='evolution_credential_grant_uses'
AND column_name='request_digest'"""
).fetchone() is None
Loading
Loading