From 8a133a4f788919891a93f0cbea766f6015496b85 Mon Sep 17 00:00:00 2001 From: kris Date: Sun, 29 Mar 2026 17:45:03 +0800 Subject: [PATCH] feat: surface active governance overrides --- collector-service/app/oneliner_features.py | 43 +++++++---- tests/test_main_agent_governance.py | 77 +++++++++++++++++++ web/storyforge-web-v4/assets/app.js | 55 +++++++++++++ .../tests/workbench-pages.test.mjs | 6 ++ 4 files changed, 165 insertions(+), 16 deletions(-) diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index 14de8f0..3ea6551 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -683,6 +683,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: def _resolve_project(account: dict[str, Any], project_id: str | None) -> dict[str, Any]: return legacy.resolve_target_project(account["id"], project_id or None, username=account["username"]) + def _resolve_project_for_read(account: dict[str, Any], project_id: str | None) -> dict[str, Any] | None: + if project_id: + return legacy.resolve_target_project(account["id"], project_id, username=account["username"]) + return legacy.db.fetch_one( + "SELECT * FROM projects WHERE user_id = ? ORDER BY created_at ASC LIMIT 1", + (account["id"],), + ) + def _resolve_assistant(account: dict[str, Any], assistant_id: str | None, project_id: str = "") -> dict[str, Any] | None: return legacy.resolve_target_assistant(account["id"], assistant_id or None, project_id) @@ -4522,10 +4530,10 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: platform: str | None = Query(default=None), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) return _effective_policy_payload( subject_account=account, - subject_project_id=project["id"], + subject_project_id=(project or {}).get("id", ""), platform=platform or "", ) @@ -4534,22 +4542,23 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: project_id: str | None = Query(default=None), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) + resolved_project_id = (project or {}).get("id", "") scope_row = _policy_scope_row( scope_kind="user_global", subject_user_id=account["id"], - subject_project_id=project["id"], + subject_project_id=resolved_project_id, ) payload = _bundle_with_versions( scope_row, fallback_kind="user_global", fallback_user_id=account["id"], - fallback_project_id=project["id"], + fallback_project_id=resolved_project_id, active_version_only=True, ) payload["effective_policy"] = _effective_policy_payload( subject_account=account, - subject_project_id=project["id"], + subject_project_id=resolved_project_id, platform="", )["effective_policy"] return payload @@ -4600,11 +4609,11 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: project_id: str | None = Query(default=None), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) scope_row = _policy_scope_row( scope_kind="user_global", subject_user_id=account["id"], - subject_project_id=project["id"], + subject_project_id=(project or {}).get("id", ""), ) items = _list_policy_versions(scope_row) return {"items": items, "count": len(items)} @@ -4616,13 +4625,14 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: limit: int = Query(default=20, ge=1, le=100), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) + resolved_project_id = (project or {}).get("id", "") normalized_platform = _normalize_policy_platform(platform) where_clauses = [ "WHERE scope.subject_user_id = ?", "AND (scope.subject_project_id = ? OR scope.subject_project_id = '')", ] - params: list[Any] = [account["id"], project["id"]] + params: list[Any] = [account["id"], resolved_project_id] if normalized_platform: where_clauses.append("AND (scope.platform = '' OR scope.platform = ?)") params.append(normalized_platform) @@ -4670,12 +4680,13 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: project_id: str | None = Query(default=None), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) + resolved_project_id = (project or {}).get("id", "") normalized_platform = _normalize_policy_platform(platform) scope_row = _policy_scope_row( scope_kind="user_platform", subject_user_id=account["id"], - subject_project_id=project["id"], + subject_project_id=resolved_project_id, platform=normalized_platform, ) payload = _bundle_with_versions( @@ -4683,12 +4694,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: fallback_kind="user_platform", fallback_platform=normalized_platform, fallback_user_id=account["id"], - fallback_project_id=project["id"], + fallback_project_id=resolved_project_id, active_version_only=True, ) payload["effective_policy"] = _effective_policy_payload( subject_account=account, - subject_project_id=project["id"], + subject_project_id=resolved_project_id, platform=normalized_platform, )["effective_policy"] return payload @@ -4743,12 +4754,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: project_id: str | None = Query(default=None), account: dict[str, Any] = Depends(legacy.require_approved), ) -> dict[str, Any]: - project = _resolve_project(account, project_id or None) + project = _resolve_project_for_read(account, project_id or None) normalized_platform = _normalize_policy_platform(platform) scope_row = _policy_scope_row( scope_kind="user_platform", subject_user_id=account["id"], - subject_project_id=project["id"], + subject_project_id=(project or {}).get("id", ""), platform=normalized_platform, ) items = _list_policy_versions(scope_row) diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index 7cba927..ef4e071 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -130,6 +130,56 @@ class MainAgentGovernanceTests(unittest.TestCase): "member_headers": {"Authorization": f"Bearer {member_token}"}, } + def _seed_approved_member_without_project(self) -> dict[str, Any]: + now = self.db_module.utc_now() + admin_id = "acct_admin" + member_id = "acct_member_noproject" + model_id = "model_default" + admin_token = "token_admin" + member_token = "token_member_noproject" + + self.core.db.execute( + """ + INSERT INTO accounts ( + id, username, password_hash, password_salt, display_name, role, approval_status, + approved_by, approved_at, preferred_analysis_model_id, created_at, updated_at + ) VALUES (?, ?, 'hash', 'salt', ?, ?, 'approved', ?, ?, ?, ?, ?) + """, + (admin_id, "admin", "Admin", "super_admin", admin_id, now, model_id, now, now), + ) + self.core.db.execute( + """ + INSERT INTO accounts ( + id, username, password_hash, password_salt, display_name, role, approval_status, + approved_by, approved_at, preferred_analysis_model_id, created_at, updated_at + ) VALUES (?, ?, 'hash', 'salt', ?, ?, 'approved', ?, ?, ?, ?, ?) + """, + (member_id, "member_noproject", "Member No Project", "operator", admin_id, now, model_id, now, now), + ) + self.core.db.execute( + """ + INSERT INTO model_profiles ( + id, owner_account_id, name, provider, base_url, api_key, model_name, + is_system, is_default, created_at, updated_at + ) VALUES (?, NULL, 'Default Model', 'openai_compat', 'http://127.0.0.1:8317/v1', '', 'GLM-5', 1, 1, ?, ?) + """, + (model_id, now, now), + ) + self.core.db.execute( + "INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)", + (admin_token, admin_id, now), + ) + self.core.db.execute( + "INSERT INTO auth_tokens (token, account_id, created_at) VALUES (?, ?, ?)", + (member_token, member_id, now), + ) + return { + "admin_id": admin_id, + "member_id": member_id, + "admin_headers": {"Authorization": f"Bearer {admin_token}"}, + "member_headers": {"Authorization": f"Bearer {member_token}"}, + } + def test_effective_policy_merges_system_user_global_and_platform_layers(self) -> None: system_response = self.client.put( "/v2/admin/oneliner/governance/system/main-agent", @@ -378,6 +428,33 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertEqual(system_read.status_code, 200, system_read.text) self.assertEqual(system_read.json()["current_version"]["title"], "System baseline") + def test_governance_read_endpoints_do_not_create_default_project_when_project_is_missing(self) -> None: + self._clear_tables() + ctx = self._seed_approved_member_without_project() + before_count = self.core.db.fetch_one("SELECT COUNT(*) AS count FROM projects WHERE user_id = ?", (ctx["member_id"],)) + self.assertEqual(int((before_count or {}).get("count") or 0), 0) + + effective_response = self.client.get( + "/v2/oneliner/governance/effective", + headers=ctx["member_headers"], + params={"platform": "douyin"}, + ) + self.assertEqual(effective_response.status_code, 200, effective_response.text) + effective_payload = effective_response.json() + self.assertEqual(effective_payload["project_id"], "") + self.assertEqual(effective_payload["layers"], []) + + global_response = self.client.get( + "/v2/oneliner/governance/user/global", + headers=ctx["member_headers"], + ) + self.assertEqual(global_response.status_code, 200, global_response.text) + self.assertEqual(global_response.json()["scope"]["subject_project_id"], "") + self.assertIsNone(global_response.json()["current_version"]) + + after_count = self.core.db.fetch_one("SELECT COUNT(*) AS count FROM projects WHERE user_id = ?", (ctx["member_id"],)) + self.assertEqual(int((after_count or {}).get("count") or 0), 0) + def test_admin_governance_directory_lists_accounts_and_projects(self) -> None: response = self.client.get( "/v2/admin/oneliner/governance/directory", diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index df53df4..7bc4de5 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -965,6 +965,7 @@ function renderOneLinerMessagesHtml() { const result = message.result || {}; const plan = message.plan || {}; const executionCard = result.execution_card || {}; + const activeAdminOverrideNotice = executionCard.active_admin_override_notice || null; const actions = safeArray(plan.suggested_actions); const secondaryActions = safeArray(executionCard.secondary_actions); return ` @@ -995,6 +996,16 @@ function renderOneLinerMessagesHtml() { ${executionCard.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(executionCard.readiness_label)} ${escapeHtml(formatNumber(executionCard.readiness_score || 0))}` : ""} ${executionCard.primary_action?.key ? `${escapeHtml(executionCard.primary_action.label || "执行下一步")}` : ""} + ${activeAdminOverrideNotice?.title ? ` +
+

管理员覆盖生效中

+

${escapeHtml(activeAdminOverrideNotice.summary || "当前这轮执行会优先遵循管理员覆盖,再叠加你的个人策略。")}

+
+ ${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")} + ${activeAdminOverrideNotice.platform_label ? `${escapeHtml(activeAdminOverrideNotice.platform_label)}` : ""} +
+
+ ` : ""} ${safeArray(executionCard.evidence).length ? `
${safeArray(executionCard.evidence).slice(0, 2).map((item) => ` @@ -1053,6 +1064,7 @@ function renderOneLinerUi() { const input = document.querySelector('[data-role="oneliner-input"]'); const profile = appState.onelinerProfile; const effective = appState.onelinerGovernanceEffective; + const activeAdminOverrideNotice = effective?.active_admin_override_notice || null; const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || ""); const layers = safeArray(effective?.layers); if (fab) { @@ -1072,6 +1084,16 @@ function renderOneLinerUi() { ${highlights.map((item) => `${escapeHtml(item)}`).join("")} 我的策略
+ ${activeAdminOverrideNotice?.title ? ` +
+

管理员覆盖生效中

+

${escapeHtml(activeAdminOverrideNotice.summary || "当前主 Agent 会优先遵循管理员覆盖层。")}

+
+ ${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")} + 查看我的策略 +
+
+ ` : ""} `; } if (sessions) sessions.innerHTML = renderOneLinerSessionTabs(); @@ -3333,6 +3355,7 @@ function getAdminOverrideTargetSummary(target = appState.adminOverrideTarget) { function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction = "", primaryLabel = "编辑策略", secondaryAction = "", secondaryLabel = "", secondaryPlatform = "", actions = null }) { const layers = safeArray(effective?.layers); const highlights = summarizePolicyHighlights(effective?.effective_policy || {}, effective?.platform || secondaryPlatform || ""); + const activeAdminOverrideNotice = effective?.active_admin_override_notice || null; const resolvedActions = safeArray(actions?.length ? actions : [ primaryAction ? { action: primaryAction, label: primaryLabel } : null, secondaryAction ? { action: secondaryAction, label: secondaryLabel, platform: secondaryPlatform } : null @@ -3345,6 +3368,16 @@ function renderGovernanceSummaryCard({ title, subtitle, effective, primaryAction ${layers.map((layer) => `${escapeHtml(policyScopeTagLabel(layer.scope_kind, layer.scope?.platform || effective?.platform || ""))}`).join("") || `尚未发布`} ${highlights.map((item) => `${escapeHtml(item)}`).join("")} + ${activeAdminOverrideNotice?.title ? ` +
+

管理员覆盖生效中

+

${escapeHtml(activeAdminOverrideNotice.summary || activeAdminOverrideNotice.title || "当前这层管理员覆盖会优先于你的个人策略生效。")}

+
+ ${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")} + ${activeAdminOverrideNotice.platform_label ? `${escapeHtml(activeAdminOverrideNotice.platform_label)}` : ""} +
+
+ ` : ""} ${resolvedActions.length ? `
${resolvedActions.map((item) => ` @@ -4900,6 +4933,7 @@ function renderPlaybookScreen() { const currentModel = getCurrentModelProfile(); const currentAssistant = getSelectedAssistant(); const localCatalog = appState.localModelCatalog || {}; + const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null; const gatewayModels = safeArray(localCatalog.models).map((item) => item.id).filter(Boolean); const tabs = [ { value: "workspace", label: "当前 Agent 工作台" }, @@ -4951,6 +4985,16 @@ function renderPlaybookScreen() { 编辑配置
+ ${activeAdminOverrideNotice?.title ? ` +
+

管理员覆盖生效中

+

${escapeHtml(activeAdminOverrideNotice.summary || "当前 OneLiner 和平台 Agent 都会先遵循管理员覆盖层。")}

+
+ ${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")} + 看我的策略 +
+
+ ` : ""} ${renderGovernanceSummaryCard({ title: "我的策略与历史", subtitle: appState.userGlobalPolicy?.current_version?.summary || "你和主 Agent 的策略对话,会先沉淀成用户全局策略,再按需要下放到单平台。", @@ -5320,6 +5364,7 @@ function renderStrategyScreen() { const activeTab = getActiveDetailTab("strategyDetailTab", tabs); const project = getSelectedProject(); const platform = appState.onelinerGovernanceEffective?.platform || appState.onelinerProfile?.default_platform || getPreferredPlatform(); + const activeAdminOverrideNotice = appState.onelinerGovernanceEffective?.active_admin_override_notice || null; return screenShell( "我的策略", "把你和主 Agent 的对话沉淀成可查看、可回滚、可追溯的个人策略层。", @@ -5328,6 +5373,16 @@ function renderStrategyScreen() {

当前策略工作区

${escapeHtml(project?.name || "当前项目")} · ${escapeHtml(platformLabel(platform))}。这里展示系统默认、你的个性化策略和管理员覆盖是如何叠加生效的。

+ ${activeAdminOverrideNotice?.title ? ` +
+

管理员覆盖生效中

+

${escapeHtml(activeAdminOverrideNotice.summary || "当前项目下的部分策略被管理员覆盖层托底。")}

+
+ ${escapeHtml(activeAdminOverrideNotice.title || "管理员覆盖")} + ${activeAdminOverrideNotice.platform_label ? `${escapeHtml(activeAdminOverrideNotice.platform_label)}` : ""} +
+
+ ` : ""}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 0091089..d3bcf47 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -47,6 +47,7 @@ test("agent screen excludes quota and registry panels and uses page tabs", () => assert.match(source, /renderGovernanceSummaryCard\(/); assert.match(source, /open-user-global-policy/); assert.match(source, /open-user-platform-policy/); + assert.match(source, /active_admin_override_notice/); }); test("discovery, production, and admin screens use page tabs for heavy content", () => { @@ -125,9 +126,11 @@ test("agent control surfaces load governance endpoints for user and admin summar test("oneliner meta and action handlers expose governance entry points", () => { const meta = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()"); + const messages = extractBetween(APP, "function renderOneLinerMessagesHtml()", "function renderOneLinerUi()"); const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); assert.match(meta, /open-user-global-policy/); assert.match(meta, /policyScopeTagLabel/); + assert.match(messages, /active_admin_override_notice/); assert.match(actions, /name === "open-user-global-policy"/); assert.match(actions, /name === "open-system-main-policy"/); }); @@ -181,10 +184,13 @@ test("governance UI exposes admin override target picker and history rollback en test("user governance UI exposes personal history and rollback entrypoints", () => { const playbook = extractBetween(APP, "function renderPlaybookScreen()", "function renderProductionScreen()"); + const strategy = extractBetween(APP, "function renderStrategyScreen()", "function renderCreditsScreen()"); const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); assert.match(playbook, /open-user-global-policy-history/); assert.match(playbook, /open-user-platform-policy-history/); + assert.match(strategy, /active_admin_override_notice/); + assert.match(strategy, /管理员覆盖生效中/); assert.match(actions, /name === "open-user-global-policy-history"/); assert.match(actions, /name === "open-user-platform-policy-history"/);