fix(security): W4-E1 approvals admin-session only, shared redact table, TMDB key redaction, prod mock gate
- POST /approvals and PATCH /approvals/{id} require a console admin session
(EndpointDef auth:"session"); API keys incl. admin scope get 403, so an
organize.execute key can no longer approve its own plan (S1)
- tests for single-use approval checks: expired, revision/payload hash
mismatch, consumed, revoked, pending/rejected, plan mismatch; re-approve
after plan change succeeds once and cannot be replayed (S4)
- one redact key table in @umt/contracts shared by server logger and agent
redactForModel: rsskey, torrent_pass, authkey, downhash, c_secure_pass,
passkey variants, x-api-key, cookie headers, nested objects/arrays (S2)
- TMDB network errors: message and cause redacted via redactUrl (S3)
- web Agent ?mock=1 only in DEV builds or when pi.provider is fake (S7)
- EndpointDef auth annotations for /auth/*, asserted against
PUBLIC_AUTH_PATHS (S10); doc drift fixes (S11)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
79cdd26457
commit
f48e1aac96
25 files changed
+825
-235
No files matched your search
@@ -1,3 +1,20 @@
|
||||
# @umt/web
|
||||
|
||||
React Router 7 SPA 控制台(`ssr:false`),沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 代理到 http://localhost:4380。
|
||||
|
||||
## 命令
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
- `deno task dev:web`:开发服务器(5180)。
|
||||
- `cd apps/web && deno task build`:生产构建到 `build/client`,由 server 的 `WEB_ROOT` 同端口托管。
|
||||
- `cd apps/web && deno task test`:单测(`app/**/*.test.ts`)。
|
||||
- `deno task test:web:smoke`:Playwright 全页面冒烟,前提与环境变量见仓库 README。
|
||||
- `deno task check:tokens`:设计 token 守卫。
|
||||
|
||||
## 目录
|
||||
|
||||
- `app/routes/`:页面(`routes.ts` 登记全部路由);`app/routes/console/` 为控制台。
|
||||
- `app/api/`:按资源分的 react-query hook,路径与 schema 取 `@umt/contracts` 的 `Api` 注册表。
|
||||
- `app/components/`:业务组件与 `ui/` 基础组件。
|
||||
- Agent 页的 `?mock=1` 演示模式只在开发构建或后端 `pi.provider = "fake"` 时生效,生产构建连真实模型时忽略。
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { agentMockAllowed, resolveAgentMock } from "./agent.ts";
|
||||
|
||||
class MemStore {
|
||||
m = new Map<string, string>();
|
||||
getItem(k: string) {
|
||||
return this.m.get(k) ?? null;
|
||||
}
|
||||
setItem(k: string, v: string) {
|
||||
this.m.set(k, v);
|
||||
}
|
||||
removeItem(k: string) {
|
||||
this.m.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
// 验收 S7:生产构建里 ?mock=1 不得把演示数据当真实运行展示
|
||||
describe("Agent 演示开关", () => {
|
||||
it("只在开发构建或后端 provider=fake 时允许", () => {
|
||||
expect(agentMockAllowed({ dev: true })).toBe(true);
|
||||
expect(agentMockAllowed({ dev: false, provider: "fake" })).toBe(true);
|
||||
expect(agentMockAllowed({ dev: false, provider: "anthropic" })).toBe(false);
|
||||
expect(agentMockAllowed({ dev: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("生产 + 真实模型:?mock=1 与 VITE_AGENT_MOCK 都被忽略,并清掉记住的开关", () => {
|
||||
const storage = new MemStore();
|
||||
storage.setItem("umt-agent-mock", "1");
|
||||
expect(resolveAgentMock({ allowed: false, envFlag: "1", search: "?mock=1", storage })).toBe(false);
|
||||
expect(storage.getItem("umt-agent-mock")).toBeNull();
|
||||
});
|
||||
|
||||
it("允许时:?mock=1 记住,?mock=0 退出,构建期开关直接打开", () => {
|
||||
const storage = new MemStore();
|
||||
expect(resolveAgentMock({ allowed: true, search: "?mock=1", storage })).toBe(true);
|
||||
expect(resolveAgentMock({ allowed: true, search: "", storage })).toBe(true);
|
||||
expect(resolveAgentMock({ allowed: true, search: "?mock=0", storage })).toBe(false);
|
||||
expect(resolveAgentMock({ allowed: true, envFlag: "1", search: "", storage })).toBe(true);
|
||||
expect(resolveAgentMock({ allowed: true, search: "?mock=1", storage: null })).toBe(true);
|
||||
});
|
||||
});
|
||||
+69
-10
@@ -13,10 +13,12 @@ import {
|
||||
import { MOCK_RUNS, mockCreateRun, mockDetail, mockEventsAfter } from "~/components/agent/mock.ts";
|
||||
import { TTL_SECOND } from "~/lib/api.ts";
|
||||
import { apiMutate, apiParsed, cleanParams } from "./parse.ts";
|
||||
import { useSettings } from "./settings.ts";
|
||||
|
||||
// Agent 会话(W2-D:GET/POST /agent/runs、GET /agent/runs/{id}?afterSeq=、/messages、/abort),
|
||||
// 形状一律取 contracts(AgentRun / AgentRunDetail / AgentGoalReport)。
|
||||
// ?mock=1(或 VITE_AGENT_MOCK=1)只作演示开关:不连后端,用本地假数据预览交互。
|
||||
// ?mock=1(或 VITE_AGENT_MOCK=1)只作演示开关:不连后端,用本地假数据预览交互;
|
||||
// 仅开发构建或后端 pi.provider = "fake" 时生效,生产构建连真实模型时忽略(见 agentMockAllowed)。
|
||||
|
||||
export const AGENT_BASE = "/api/v1/agent/runs";
|
||||
|
||||
@@ -34,19 +36,76 @@ export type RunSnapshot = Omit<RunDetail, "events" | "nextAfterSeq">;
|
||||
|
||||
const RunPage = cursorPage(AgentRun);
|
||||
|
||||
export function agentMockEnabled(): boolean {
|
||||
if (import.meta.env.VITE_AGENT_MOCK === "1") return true;
|
||||
if (typeof window === "undefined") return false;
|
||||
const p = new URLSearchParams(window.location.search);
|
||||
/**
|
||||
* 演示开关是否可用:只在开发构建(import.meta.env.DEV)或后端 pi.provider = "fake"(脚本化假模型)时生效。
|
||||
* 生产构建连着真实模型时一律忽略 ?mock=1 / VITE_AGENT_MOCK,并清掉 sessionStorage 里记住的开关,
|
||||
* 避免把演示数据当成真实运行展示(验收 S7 / AT78)。
|
||||
*/
|
||||
export function agentMockAllowed(o: { dev: boolean; provider?: string | null | undefined }): boolean {
|
||||
return o.dev || o.provider === "fake";
|
||||
}
|
||||
|
||||
interface MockFlagStore {
|
||||
getItem(k: string): string | null;
|
||||
setItem(k: string, v: string): void;
|
||||
removeItem(k: string): void;
|
||||
}
|
||||
|
||||
const MOCK_KEY = "umt-agent-mock";
|
||||
|
||||
/** 纯函数:按是否允许、构建期开关、URL 参数与会话存储决定是否进演示模式(单测覆盖) */
|
||||
export function resolveAgentMock(o: {
|
||||
allowed: boolean;
|
||||
envFlag?: string | undefined;
|
||||
search: string;
|
||||
storage?: MockFlagStore | null;
|
||||
}): boolean {
|
||||
if (!o.allowed) {
|
||||
try {
|
||||
o.storage?.removeItem(MOCK_KEY);
|
||||
} catch {
|
||||
// 存储不可用时无事可清
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (o.envFlag === "1") return true;
|
||||
const p = new URLSearchParams(o.search);
|
||||
try {
|
||||
if (p.get("mock") === "1") sessionStorage.setItem("umt-agent-mock", "1");
|
||||
if (p.get("mock") === "0") sessionStorage.removeItem("umt-agent-mock");
|
||||
return sessionStorage.getItem("umt-agent-mock") === "1";
|
||||
if (!o.storage) return p.get("mock") === "1";
|
||||
if (p.get("mock") === "1") o.storage.setItem(MOCK_KEY, "1");
|
||||
if (p.get("mock") === "0") o.storage.removeItem(MOCK_KEY);
|
||||
return o.storage.getItem(MOCK_KEY) === "1";
|
||||
} catch {
|
||||
return p.get("mock") === "1";
|
||||
}
|
||||
}
|
||||
|
||||
/** 最近一次读到的后端 pi.provider 是否为 fake(由 useAgentMockGate 从 /settings 缓存同步) */
|
||||
let fakeProvider = false;
|
||||
|
||||
export function agentMockEnabled(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
let storage: MockFlagStore | null = null;
|
||||
try {
|
||||
storage = window.sessionStorage;
|
||||
} catch {
|
||||
storage = null;
|
||||
}
|
||||
return resolveAgentMock({
|
||||
allowed: agentMockAllowed({ dev: import.meta.env.DEV, provider: fakeProvider ? "fake" : null }),
|
||||
envFlag: import.meta.env.VITE_AGENT_MOCK,
|
||||
search: window.location.search,
|
||||
storage,
|
||||
});
|
||||
}
|
||||
|
||||
/** Agent 页的列表/详情 hook 里调用:同步后端 provider,再判断演示开关 */
|
||||
function useAgentMockGate(): boolean {
|
||||
const settings = useSettings();
|
||||
fakeProvider = settings.data?.pi?.provider === "fake";
|
||||
return agentMockEnabled();
|
||||
}
|
||||
|
||||
export const agentKeys = {
|
||||
all: ["agent"] as const,
|
||||
runs: (status?: AgentRunStatus) => ["agent", "list", status ?? "all"] as const,
|
||||
@@ -57,7 +116,7 @@ const RUNNING: AgentRunStatus[] = ["queued", "running", "waiting"];
|
||||
export const isRunActive = (s: AgentRunStatus | undefined) => !!s && RUNNING.includes(s);
|
||||
|
||||
export function useAgentRuns(status?: AgentRunStatus) {
|
||||
const mock = agentMockEnabled();
|
||||
const mock = useAgentMockGate();
|
||||
return useInfiniteQuery({
|
||||
queryKey: [...agentKeys.runs(status), mock],
|
||||
initialPageParam: undefined as string | undefined,
|
||||
@@ -75,7 +134,7 @@ export function useAgentRuns(status?: AgentRunStatus) {
|
||||
const NO_EVENTS = 2 ** 31 - 1;
|
||||
|
||||
export function useAgentRun(id: string | undefined) {
|
||||
const mock = agentMockEnabled();
|
||||
const mock = useAgentMockGate();
|
||||
return useQuery({
|
||||
queryKey: [...agentKeys.run(id ?? ""), mock],
|
||||
enabled: !!id,
|
||||
|
||||
Reference in new issue
Block a user