Skip to content
Closed
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
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,13 @@ impl Default for LauncherDataService {

#[async_trait::async_trait]
impl BridgeDataService for LauncherDataService {
async fn provider_guard_status(&self) -> anyhow::Result<Value> {
let status = tokio::task::spawn_blocking(|| codex_plus_data::inspect_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard status task failed: {error}"))??;
Ok(serde_json::to_value(status)?)
}

async fn delete(&self, session: SessionRef) -> anyhow::Result<DeleteResult> {
let db_paths = self.candidate_db_paths();
let backup_store = codex_plus_data::BackupStore::new(self.backup_dir.clone());
Expand Down
35 changes: 35 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,41 @@ pub async fn load_provider_sync_targets() -> CommandResult<Value> {
}
}

#[tauri::command]
pub async fn load_provider_guard_status() -> CommandResult<Value> {
let result = tauri::async_runtime::spawn_blocking(|| codex_plus_data::inspect_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard status task failed: {error}"));
match result {
Ok(Ok(status)) => ok(
"Provider Guard 状态已加载。",
serde_json::to_value(status).unwrap_or_else(|_| json!({})),
),
Ok(Err(error)) | Err(error) => {
failed(&format!("Provider Guard 状态加载失败:{error}"), json!({}))
}
}
}

#[tauri::command]
pub async fn repair_provider_guard(confirmed: bool) -> CommandResult<Value> {
if !confirmed {
return failed("必须在原生管理器中确认后才能执行 Provider Guard 修复。", json!({}));
}
let result = tauri::async_runtime::spawn_blocking(|| codex_plus_data::repair_provider_guard(None))
.await
.map_err(|error| anyhow::anyhow!("provider guard repair task failed: {error}"));
match result {
Ok(Ok(repair)) => ok(
"Provider Guard 已完成备份和修复。",
serde_json::to_value(repair).unwrap_or_else(|_| json!({})),
),
Ok(Err(error)) | Err(error) => {
failed(&format!("Provider Guard 修复失败:{error}"), json!({}))
}
}
}

fn merge_manual_provider_sync_targets(
targets: &mut codex_plus_data::ProviderSyncTargetList,
manual: &[String],
Expand Down
2 changes: 2 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub fn run() {
commands::forget_zed_remote_project,
commands::delete_local_session,
commands::load_provider_sync_targets,
commands::load_provider_guard_status,
commands::repair_provider_guard,
commands::preview_session_index_cleanup,
commands::apply_session_index_cleanup,
commands::sync_providers_now,
Expand Down
116 changes: 115 additions & 1 deletion apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,28 @@ type ProviderSyncTargetsPayload = {

type ProviderSyncTargetsResult = CommandResult<ProviderSyncTargetsPayload>;

type ProviderGuardFinding = {
code: string;
severity: "warning" | "critical" | string;
message: string;
};

type ProviderGuardStatusPayload = {
level: "ok" | "warning" | "critical" | string;
stableProvider: string;
currentProvider: string;
stableProviderConfigured: boolean;
totalThreads: number;
databasesScanned: number;
providerBuckets: Array<{ provider: string; threads: number }>;
endpoint: { kind: string; loopback: boolean; port?: number | null };
findings: ProviderGuardFinding[];
canRepair: boolean;
repairRequiresNativeConfirmation: boolean;
};

type ProviderGuardResult = CommandResult<ProviderGuardStatusPayload>;

type ProviderSyncProgress = {
active: boolean;
percent: number;
Expand Down Expand Up @@ -1183,6 +1205,7 @@ export function App() {
message: t("尚未检查官方远端插件缓存。"),
});
const [providerSyncTargets, setProviderSyncTargets] = useState<ProviderSyncTargetsResult | null>(null);
const [providerGuard, setProviderGuard] = useState<ProviderGuardResult | null>(null);
const [selectedProviderSyncTarget, setSelectedProviderSyncTarget] = useState("");
const [removeOwnedData, setRemoveOwnedData] = useState(false);
const [relaySwitching, setRelaySwitching] = useState(false);
Expand Down Expand Up @@ -2151,6 +2174,7 @@ export function App() {
await refreshSettings(true);
await refreshLocalSessions(true);
await refreshProviderSyncTargets(true);
await refreshProviderGuard(true);
}
if (next === "zedRemote") {
await refreshSettings(true);
Expand Down Expand Up @@ -2608,6 +2632,35 @@ export function App() {
return result;
};

const refreshProviderGuard = async (silent = false) => {
const result = await run(() => call<ProviderGuardResult>("load_provider_guard_status"));
if (result) {
setProviderGuard(result);
if (!silent && !isSuccessStatus(result.status)) showNotice(t("Provider Guard"), result.message, result.status);
}
return result;
};

const repairProviderGuard = async () => {
if (!providerGuard?.canRepair) {
showNotice(t("Provider Guard"), t("当前配置不满足安全修复条件,请先配置 model_providers.custom。"), "failed");
return;
}
const confirmed = window.confirm(
t("修复前会备份 config.toml、会话文件和 SQLite 索引,并将稳定供应商 ID 设为 custom。是否继续?"),
);
if (!confirmed) return;
const result = await run(() =>
call<CommandResult<{ guard?: ProviderGuardStatusPayload }>>("repair_provider_guard", { confirmed: true }),
);
if (result) {
showNotice(t("Provider Guard"), result.message, result.status);
await refreshProviderGuard(true);
await refreshProviderSyncTargets(true);
await refreshLocalSessions(true);
}
};

const syncProvidersNow = async () => {
if (providerSyncProgress.active) return;
setProviderSyncProgress({
Expand Down Expand Up @@ -3081,6 +3134,7 @@ export function App() {
await refreshRelay(true);
await refreshEnvConflicts(true);
await refreshProviderSyncTargets(true);
await refreshProviderGuard(true);
await refreshPendingProviderImport(true);
await refreshPendingSessionShare(true);
await refreshPendingDreamSkinCommunity();
Expand Down Expand Up @@ -3360,6 +3414,8 @@ export function App() {
},
syncProvidersNow,
refreshProviderSyncTargets,
refreshProviderGuard,
repairProviderGuard,
setProviderSyncTarget: (provider: string) => {
setSelectedProviderSyncTarget(provider);
setSettingsForm((current) => ({ ...current, providerSyncLastSelectedProvider: provider }));
Expand Down Expand Up @@ -3445,7 +3501,7 @@ export function App() {
disableWatcher: () => watcherAction("disable_watcher"),
toggleTheme: () => setTheme((current) => (current === "dark" ? "light" : "dark")),
}),
[route, launchForm, settingsForm, settings, overview, removeOwnedData, update, updateInstallProgress.active, logs, diagnostics, theme, relayFiles, localSessions, sessionShareUrl, importSessionUrl, zedRemoteProjects, selectedProviderSyncTarget, envConflicts, relayEnvironment, ccsProviders, dreamSkinLibrary, dreamSkinMarket, dreamSkinCommunity, selectedDreamSkinTheme, savedDreamSkinThemeDraft, dreamSkinThemeDraft, dreamSkinDraftDirty, pendingDreamSkinRestart],
[route, launchForm, settingsForm, settings, overview, removeOwnedData, update, updateInstallProgress.active, logs, diagnostics, theme, relayFiles, localSessions, sessionShareUrl, importSessionUrl, zedRemoteProjects, selectedProviderSyncTarget, providerGuard, envConflicts, relayEnvironment, ccsProviders, dreamSkinLibrary, dreamSkinMarket, dreamSkinCommunity, selectedDreamSkinTheme, savedDreamSkinThemeDraft, dreamSkinThemeDraft, dreamSkinDraftDirty, pendingDreamSkinRestart],
);
const hasUpdate = update?.updateAvailable === true;

Expand Down Expand Up @@ -3568,6 +3624,7 @@ export function App() {
sessions={localSessions}
providerSyncProgress={providerSyncProgress}
providerSyncTargets={providerSyncTargets}
providerGuard={providerGuard}
selectedProviderSyncTarget={selectedProviderSyncTarget}
onFormChange={setSettingsForm}
actions={actions}
Expand Down Expand Up @@ -3784,6 +3841,8 @@ type Actions = {
saveManualCodexAppPath: () => Promise<void>;
syncProvidersNow: () => Promise<void>;
refreshProviderSyncTargets: (silent?: boolean) => Promise<ProviderSyncTargetsResult | null>;
refreshProviderGuard: (silent?: boolean) => Promise<ProviderGuardResult | null>;
repairProviderGuard: () => Promise<void>;
setProviderSyncTarget: (provider: string) => void;
setLaunchMode: (launchMode: LaunchMode) => Promise<void>;
refreshRelay: () => Promise<void>;
Expand Down Expand Up @@ -6838,6 +6897,7 @@ function SessionsScreen({
sessions,
providerSyncProgress,
providerSyncTargets,
providerGuard,
selectedProviderSyncTarget,
onFormChange,
actions,
Expand All @@ -6847,6 +6907,7 @@ function SessionsScreen({
sessions: LocalSessionsResult | null;
providerSyncProgress: ProviderSyncProgress;
providerSyncTargets: ProviderSyncTargetsResult | null;
providerGuard: ProviderGuardResult | null;
selectedProviderSyncTarget: string;
onFormChange: (value: BackendSettings) => void;
actions: Actions;
Expand Down Expand Up @@ -6909,6 +6970,59 @@ function SessionsScreen({

return (
<>
<Panel>
<CardHead
title={t("Provider Guard")}
detail={t("固定稳定供应商 ID,检查会话分桶,并阻止脚本市场静默修改配置或 SQLite")}
/>
<CardContent>
<div className="metric-list">
<Metric label={t("安全状态")} value={providerGuard?.level ?? t("尚未检查")} />
<Metric label={t("当前 provider")} value={providerGuard?.currentProvider ?? "-"} />
<Metric label={t("稳定 provider")} value={providerGuard?.stableProvider ?? "custom"} />
<Metric label={t("索引会话")} value={tf("{0} 个", [providerGuard?.totalThreads ?? 0])} />
<Metric
label={t("接口类型")}
value={providerGuard?.endpoint ? `${providerGuard.endpoint.kind}${providerGuard.endpoint.port ? `:${providerGuard.endpoint.port}` : ""}` : "-"}
/>
</div>
{(providerGuard?.providerBuckets ?? []).length ? (
<div className="hint-line">
<Info className="h-4 w-4" />
<span>
{t("会话分桶:")}
{providerGuard?.providerBuckets.map((bucket) => `${bucket.provider}=${bucket.threads}`).join(",")}
</span>
</div>
) : null}
{(providerGuard?.findings ?? []).map((finding) => (
<div className="hint-line" key={finding.code}>
{finding.severity === "critical" ? <ShieldAlert className="h-4 w-4" /> : <Info className="h-4 w-4" />}
<span>{finding.message}</span>
</div>
))}
{!providerGuard?.findings?.length && providerGuard ? (
<div className="hint-line">
<ShieldCheck className="h-4 w-4" />
<span>{t("配置与会话分桶保持稳定。")}</span>
</div>
) : null}
<Toolbar>
<Button onClick={() => void actions.refreshProviderGuard()} variant="outline">
<RefreshCw className="h-4 w-4" />
{t("重新检查")}
</Button>
<Button disabled={!providerGuard?.canRepair} onClick={() => void actions.repairProviderGuard()}>
<ShieldCheck className="h-4 w-4" />
{t("备份并修复")}
</Button>
</Toolbar>
<div className="hint-line">
<ShieldCheck className="h-4 w-4" />
<span>{t("修复只能从原生管理器执行;脚本市场仅拥有只读检查权限。")}</span>
</div>
</CardContent>
</Panel>
<Panel className="sessions-overview-panel">
<CardHead title={t("会话管理")} detail={t("读取 Codex 本地 SQLite 会话库,会删除数据库记录和对应 rollout 文件")} />
<CardContent className="sessions-overview-content">
Expand Down
17 changes: 17 additions & 0 deletions apps/codex-plus-manager/src/i18n-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ export const EN_PLAIN: Record<string, string> = {
"正在等待 Codex 重新启动…": "Waiting for Codex to restart...",
"正在等待 Codex 启动结果…": "Waiting for the Codex startup result...",
"运行中(增强等待中)": "Running (waiting for enhancements)",
"Provider Guard": "Provider Guard",
"当前配置不满足安全修复条件,请先配置 model_providers.custom。":
"The current configuration cannot be repaired safely. Configure model_providers.custom first.",
"修复前会备份 config.toml、会话文件和 SQLite 索引,并将稳定供应商 ID 设为 custom。是否继续?":
"Before repairing, Codex++ will back up config.toml, session files, and SQLite indexes, then set the stable provider ID to custom. Continue?",
"固定稳定供应商 ID,检查会话分桶,并阻止脚本市场静默修改配置或 SQLite":
"Keep a stable provider ID, inspect session buckets, and prevent marketplace scripts from silently changing config or SQLite.",
"安全状态": "Safety status",
"稳定 provider": "Stable provider",
"索引会话": "Indexed sessions",
"接口类型": "Endpoint type",
"会话分桶:": "Session buckets: ",
"配置与会话分桶保持稳定。": "Configuration and session buckets are stable.",
"重新检查": "Check again",
"备份并修复": "Back up and repair",
"修复只能从原生管理器执行;脚本市场仅拥有只读检查权限。":
"Repairs can only run from the native manager; marketplace scripts have read-only inspection access.",
"API Key 模式下扩展插件市场请求,尽量显示完整插件列表;官方/混合模式通常不需要。":
"Expands plugin marketplace requests in API Key mode to show the full plugin list. Usually unnecessary in official/mixed mode.",
"API Key 环境变量": "API Key environment variable",
Expand Down
4 changes: 4 additions & 0 deletions crates/codex-plus-core/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ pub trait BridgeRuntimeService: Send + Sync {

#[async_trait]
pub trait BridgeDataService: Send + Sync {
async fn provider_guard_status(&self) -> anyhow::Result<Value> {
anyhow::bail!("provider guard is not wired in this launcher")
}
async fn delete(&self, session: SessionRef) -> anyhow::Result<DeleteResult>;
async fn undo(&self, undo_token: String) -> anyhow::Result<DeleteResult>;
async fn export_markdown(&self, session: SessionRef) -> anyhow::Result<ExportResult>;
Expand Down Expand Up @@ -188,6 +191,7 @@ pub async fn handle_bridge_request(
ctx.runtime.backend_status().await,
ctx.settings.get_settings().await,
),
"/provider-guard/status" => ctx.data.provider_guard_status().await,
"/codex-model-catalog" | "/codex-config-model" => ctx.runtime.codex_model_catalog().await,
"/diagnostics/log" => diagnostic_log_value(payload.clone()),
"/llm-proxy" => llm_proxy_value(payload.clone()).await,
Expand Down
14 changes: 14 additions & 0 deletions crates/codex-plus-core/tests/bridge_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async fn bridge_routes_cover_all_current_paths() {
("/manager/open", json!({})),
("/manager/open-transient", json!({})),
("/backend/status", json!({})),
("/provider-guard/status", json!({})),
("/codex-model-catalog", json!({})),
("/codex-config-model", json!({})),
(
Expand Down Expand Up @@ -412,6 +413,19 @@ async fn unknown_bridge_path_preserves_empty_session_id_shape() {
);
}

#[tokio::test]
async fn provider_guard_repair_is_not_exposed_to_injected_user_scripts() {
let result = handle_bridge_request(
test_context(),
"/provider-guard/repair",
json!({"confirmed": true}),
)
.await;

assert_eq!(result["status"], "failed");
assert_eq!(result["message"], "Unknown bridge path");
}

#[tokio::test]
async fn settings_routes_use_settings_service() {
let ctx = test_context();
Expand Down
3 changes: 3 additions & 0 deletions crates/codex-plus-data/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ serde.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] }
sha2.workspace = true
thiserror.workspace = true
toml.workspace = true
toml_edit.workspace = true
url.workspace = true
uuid.workspace = true

[dev-dependencies]
Expand Down
5 changes: 5 additions & 0 deletions crates/codex-plus-data/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
pub mod backup;
pub mod markdown;
pub mod provider_guard;
pub mod provider_sync;
pub mod storage;

pub use backup::BackupStore;
pub use markdown::{MarkdownExportService, export_markdown_from_paths};
pub use provider_guard::{
ProviderBucket, ProviderEndpoint, ProviderGuardFinding, ProviderGuardRepairResult,
ProviderGuardStatus, inspect_provider_guard, repair_provider_guard,
};
pub use provider_sync::{
ProviderSyncAudit, ProviderSyncLockState, ProviderSyncResult, ProviderSyncStatus,
ProviderSyncTargetList, ProviderSyncTargetOption, ProviderSyncTargetSource,
Expand Down
Loading