fix(awooop): 遮罩前台專案與代理敏感識別
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m30s
CD Pipeline / build-and-deploy (push) Successful in 4m46s
CD Pipeline / post-deploy-checks (push) Successful in 1m34s

This commit is contained in:
Your Name
2026-06-15 07:10:47 +08:00
parent e2ad14d34b
commit 9c4e754d33
8 changed files with 201 additions and 47 deletions

View File

@@ -28,7 +28,13 @@ import {
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import { cn } from "@/lib/utils";
import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction";
import {
publicAgentText,
publicBoundaryText,
publicContractText,
publicInternalCodeSummary,
publicProjectText,
} from "@/lib/public-security-redaction";
interface RunDetail {
run_id: string;
@@ -155,7 +161,10 @@ function runTimelineHref(runId: string, projectId?: string | null) {
function remediationRoute(item?: RemediationHistoryItem | null) {
if (!item) return "--";
return [item.agent_id, item.tool_name, item.required_scope].filter(Boolean).join("/") || "--";
const agent = publicAgentText(item.agent_id);
const tool = publicInternalCodeSummary(item.tool_name, "--");
const scope = publicInternalCodeSummary(item.required_scope, "--");
return [agent, tool, scope].filter((value) => value && value !== "--").join(" / ") || "--";
}
function booleanLabel(value: boolean | null | undefined, emptyLabel: string) {
@@ -684,20 +693,20 @@ export default function ApprovalDecisionPage({
) : run ? (
<>
<DetailRow label={t("details.runId")} value={run.run_id} emptyLabel={t("empty")} />
<DetailRow label={t("details.project")} value={run.project_id} emptyLabel={t("empty")} />
<DetailRow label={t("details.agent")} value={run.agent_id} emptyLabel={t("empty")} />
<DetailRow label={t("details.project")} value={publicProjectText(run.project_id)} emptyLabel={t("empty")} />
<DetailRow label={t("details.agent")} value={publicAgentText(run.agent_id)} emptyLabel={t("empty")} />
<DetailRow
label={t("details.state")}
value={
<span className={cn("inline-flex border px-2 py-0.5 text-xs font-semibold", stateClass)}>
{run.state || t("empty")}
{publicInternalCodeSummary(run.state, t("empty"))}
</span>
}
emptyLabel={t("empty")}
/>
<DetailRow label={t("details.traceId")} value={run.trace_id} emptyLabel={t("empty")} />
<DetailRow label={t("details.trigger")} value={run.trigger_type} emptyLabel={t("empty")} />
<DetailRow label={t("details.triggerRef")} value={run.trigger_ref} emptyLabel={t("empty")} />
<DetailRow label={t("details.traceId")} value={run.trace_id ? "已脫敏追蹤碼" : null} emptyLabel={t("empty")} />
<DetailRow label={t("details.trigger")} value={publicInternalCodeSummary(run.trigger_type, t("empty"))} emptyLabel={t("empty")} />
<DetailRow label={t("details.triggerRef")} value={publicInternalCodeSummary(run.trigger_ref, t("empty"))} emptyLabel={t("empty")} />
<DetailRow
label={t("details.cost")}
value={run.cost_usd === undefined ? null : `$${Number(run.cost_usd ?? 0).toFixed(4)}`}

View File

@@ -30,7 +30,12 @@ import {
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import type { IncidentTimelineResponse } from "@/lib/api-client";
import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction";
import {
publicAgentText,
publicBoundaryText,
publicContractText,
publicProjectText,
} from "@/lib/public-security-redaction";
// =============================================================================
// Types
@@ -565,14 +570,14 @@ function ApprovalRow({ approval }: { approval: Approval }) {
</Link>
</td>
<td className="px-4 py-3">
<span className="font-mono text-sm text-muted-foreground">
{approval.project_id || "--"}
<span className="text-sm text-muted-foreground">
{publicProjectText(approval.project_id)}
</span>
</td>
<td className="px-4 py-3">
<div className="flex min-w-[180px] flex-col items-start">
<span className="font-mono text-sm text-muted-foreground">
{approval.agent_id || "--"}
<span className="text-sm text-muted-foreground">
{publicAgentText(approval.agent_id)}
</span>
{isGate5Projection && <Gate5ProjectionBadge />}
</div>

View File

@@ -20,7 +20,7 @@ import {
GitBranch,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction";
import { publicBoundaryText, publicContractText, publicProjectText } from "@/lib/public-security-redaction";
// =============================================================================
// Types
@@ -122,8 +122,8 @@ function ContractRow({ contract }: { contract: Contract }) {
</span>
</td>
<td className="px-4 py-3">
<span className="font-mono text-sm text-muted-foreground">
{contract.project_id || "--"}
<span className="text-sm text-muted-foreground">
{publicProjectText(contract.project_id)}
</span>
</td>
<td className="px-4 py-3">
@@ -576,7 +576,7 @@ export default function ContractsPage() {
<option value=""></option>
{tenants.map((t) => (
<option key={t.project_id} value={t.project_id}>
{t.display_name || t.project_id}
{publicProjectText(t.project_id)}
</option>
))}
</select>

View File

@@ -36,7 +36,13 @@ import {
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import { cn } from "@/lib/utils";
import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction";
import {
publicAgentText,
publicBoundaryText,
publicContractText,
publicInternalCodeSummary,
publicProjectText,
} from "@/lib/public-security-redaction";
interface RunDetail {
run_id: string;
@@ -839,9 +845,9 @@ function McpGatewayPanel({
</div>
<div className="grid gap-px bg-[#e0ddd4] md:grid-cols-3">
{[
{ label: t("agent"), value: agent },
{ label: t("tool"), value: tool },
{ label: t("scope"), value: scope },
{ label: t("agent"), value: publicAgentText(agent) },
{ label: t("tool"), value: publicInternalCodeSummary(tool, emptyLabel) },
{ label: t("scope"), value: publicInternalCodeSummary(scope, emptyLabel) },
].map((item) => (
<div key={item.label} className="min-w-0 bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
@@ -854,7 +860,7 @@ function McpGatewayPanel({
{hasRecords && summary?.blockers && summary.blockers.length > 0 && (
<div className="border-t border-[#eee9dd] bg-[#fff0ef] px-4 py-3 text-sm text-[#9f2f25]">
<span className="font-semibold">{t("blockers")}</span>{" "}
<span className="font-mono text-xs">{summary.blockers.slice(0, 3).join(", ")}</span>
<span className="text-xs">{publicInternalCodeSummary(summary.blockers.slice(0, 3))}</span>
</div>
)}
{legacyTotal > 0 && (
@@ -885,13 +891,13 @@ function McpGatewayPanel({
{gatewayTools.map((item) => (
<div key={`${item.agent_id ?? "gateway"}:${item.tool_name ?? "unknown"}`} className="grid gap-3 px-3 py-3 md:grid-cols-[1fr_112px]">
<div className="min-w-0">
<p className="truncate font-mono text-xs font-semibold text-[#141413]" title={item.tool_name ?? emptyLabel}>
{item.tool_name ?? emptyLabel}
<p className="truncate text-xs font-semibold text-[#141413]">
{publicInternalCodeSummary(item.tool_name, emptyLabel)}
</p>
<p className="mt-1 truncate text-xs text-[#77736a]" title={item.agent_id ?? emptyLabel}>
<p className="mt-1 truncate text-xs text-[#77736a]">
{t("evidence.agentScope", {
agent: item.agent_id ?? emptyLabel,
scope: item.required_scope ?? emptyLabel,
agent: publicAgentText(item.agent_id),
scope: publicInternalCodeSummary(item.required_scope, emptyLabel),
})}
</p>
</div>
@@ -1522,16 +1528,16 @@ export default function RunDetailPage({
<h3 className="text-sm font-semibold text-[#141413]">{t("summary.title")}</h3>
</div>
<div className="px-4">
<DetailField label={t("summary.project")} value={run?.project_id} emptyLabel={t("empty")} />
<DetailField label={t("summary.agent")} value={run?.agent_id} emptyLabel={t("empty")} />
<DetailField label={t("summary.traceId")} value={run?.trace_id} emptyLabel={t("empty")} />
<DetailField label={t("summary.trigger")} value={run?.trigger_type} emptyLabel={t("empty")} />
<DetailField label={t("summary.triggerRef")} value={run?.trigger_ref} emptyLabel={t("empty")} />
<DetailField label={t("summary.project")} value={run ? publicProjectText(run.project_id) : null} emptyLabel={t("empty")} />
<DetailField label={t("summary.agent")} value={run ? publicAgentText(run.agent_id) : null} emptyLabel={t("empty")} />
<DetailField label={t("summary.traceId")} value={run?.trace_id ? "已脫敏追蹤碼" : null} emptyLabel={t("empty")} />
<DetailField label={t("summary.trigger")} value={publicInternalCodeSummary(run?.trigger_type, t("empty"))} emptyLabel={t("empty")} />
<DetailField label={t("summary.triggerRef")} value={publicInternalCodeSummary(run?.trigger_ref, t("empty"))} emptyLabel={t("empty")} />
<DetailField label={t("summary.cost")} value={run ? `$${Number(run.cost_usd ?? 0).toFixed(4)}` : null} emptyLabel={t("empty")} />
<DetailField label={t("summary.attempts")} value={run ? `${run.attempt_count}/${run.max_attempts}` : null} emptyLabel={t("empty")} />
<DetailField label={t("summary.created")} value={formatTime(run?.created_at, locale, t("empty"))} emptyLabel={t("empty")} />
<DetailField label={t("summary.completed")} value={formatTime(run?.completed_at, locale, t("empty"))} emptyLabel={t("empty")} />
<DetailField label={t("summary.error")} value={run?.error_detail || run?.error_code} emptyLabel={t("empty")} />
<DetailField label={t("summary.error")} value={run?.error_detail || run?.error_code ? "已脫敏錯誤摘要" : null} emptyLabel={t("empty")} />
</div>
</aside>

View File

@@ -35,7 +35,12 @@ import {
TriangleAlert,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { publicBoundaryText, publicContractText } from "@/lib/public-security-redaction";
import {
publicAgentText,
publicBoundaryText,
publicContractText,
publicProjectText,
} from "@/lib/public-security-redaction";
// =============================================================================
// Types
@@ -1902,16 +1907,16 @@ function RunRow({
</Link>
</td>
<td className="px-4 py-3">
<span className="font-mono text-sm text-muted-foreground">
{run.project_id || "--"}
<span className="text-sm text-muted-foreground">
{publicProjectText(run.project_id)}
</span>
</td>
<td className="px-4 py-3">
<IncidentIdsCell run={run} />
</td>
<td className="px-4 py-3">
<span className="font-mono text-sm text-muted-foreground">
{run.agent_id || "--"}
<span className="text-sm text-muted-foreground">
{publicAgentText(run.agent_id)}
</span>
</td>
<td className="px-4 py-3">
@@ -2396,7 +2401,7 @@ function GroupedAlertEventsPanel({ events }: { events: PlatformEvent[] }) {
{event.provider_event_id.replace("alert-group:", "")}
</p>
<p className="mt-1 text-xs text-[#77736a]">
{event.project_id} · {receivedAt}
{publicProjectText(event.project_id)} · {receivedAt}
</p>
</div>
<span className="shrink-0 border border-[#d8d3c7] bg-[#faf9f3] px-2 py-0.5 text-xs text-[#5f5b52]">
@@ -3436,7 +3441,7 @@ function CallbackReplyEvidencePanel({
{event.run_id.slice(0, 8)}
</p>
<p className="mt-1 text-xs text-[#77736a]">
{event.project_id} · {eventTime}
{publicProjectText(event.project_id)} · {eventTime}
</p>
</div>
<span
@@ -4435,7 +4440,7 @@ export default function RunsPage() {
<option value=""></option>
{tenants.map((t) => (
<option key={t.project_id} value={t.project_id}>
{t.display_name || t.project_id}
{publicProjectText(t.project_id)}
</option>
))}
</select>

View File

@@ -35,6 +35,7 @@ import {
type AwoooPStatusChain,
} from "@/components/awooop/status-chain";
import { cn } from "@/lib/utils";
import { publicInternalCodeSummary } from "@/lib/public-security-redaction";
type WorkStatus = "live" | "in_progress" | "blocked" | "watching";
@@ -1912,13 +1913,13 @@ function KnowledgeOwnerReviewSingleItemRail({
<div className="min-w-0 break-words border border-[#d8d3c7] bg-[#faf9f3] px-2 py-1.5">
<p>
{t("staleCandidates.singleItemRail.required", {
fields: requiredOwnerFields.length > 0 ? requiredOwnerFields.join(", ") : "--",
fields: publicInternalCodeSummary(requiredOwnerFields, "--"),
})}
</p>
{blockers.length > 0 ? (
<p className="text-[#9f2f25]">
{t("staleCandidates.singleItemRail.blockers", {
blockers: blockers.join(", "),
blockers: publicInternalCodeSummary(blockers),
})}
</p>
) : (
@@ -5218,15 +5219,13 @@ function KnowledgeGovernancePanel({
</p>
<p className="break-words">
{t("staleCandidates.completionQueue.required", {
fields: item.required_owner_fields.length > 0
? item.required_owner_fields.join(", ")
: "--",
fields: publicInternalCodeSummary(item.required_owner_fields, "--"),
})}
</p>
{item.blockers.length > 0 ? (
<p className="break-words text-[#9f2f25]">
{t("staleCandidates.completionQueue.blockers", {
blockers: item.blockers.join(", "),
blockers: publicInternalCodeSummary(item.blockers),
})}
</p>
) : null}

View File

@@ -75,6 +75,86 @@ export function publicContractText(value: string): string {
return labels[value] ?? "已脫敏只讀證據參照";
}
const PUBLIC_PROJECT_NAMES: Record<string, string> = {
"agent-bounty-protocol": "代理獎勵協議",
"awoooi": "核心營運平台",
"awooooi": "核心營運平台",
"bitan": "藥局服務前台",
"bitan-pharmacy": "藥局服務平台",
"clawbot-v5": "自動化助理平台",
"ewoooc": "行動商務平台",
"mo": "行動商務前台",
"open-design": "設計系統",
"source-control": "版本控管範圍",
"tsenyang-website": "品牌網站",
"vibework": "工作協作產品",
"wooo-aiops": "AI 維運平台",
"wooo-infra-config": "基礎設施設定",
};
const PUBLIC_AGENT_NAMES: Record<string, string> = {
"codex": "程式修正代理",
"openclaw": "決策分析代理",
"nemotron": "推理評估代理",
"hermes": "通訊協作代理",
"elephantalph": "資料評估代理",
"elephantalpha": "資料評估代理",
};
const RAW_REPOSITORY_IDENTIFIER_RE = /\b[a-z0-9][a-z0-9-]{1,}\/[A-Za-z0-9._-]+\b/;
const INTERNAL_CODE_RE = /^[a-z][a-z0-9]*(?:[_:.-][a-z0-9]+){1,}$/i;
const CJK_TEXT_RE = /[\u3400-\u9fff]/;
function normalizedIdentifier(value: string | null | undefined): string {
return String(value ?? "").trim().toLowerCase();
}
function hasUnsafePublicIdentifier(value: string): boolean {
const normalized = value.toLowerCase();
return (
RAW_REPOSITORY_IDENTIFIER_RE.test(value) ||
normalized.includes("owen" + "hytsai") ||
normalized.includes("nexu" + "-io") ||
normalized.includes("blocked" + "_waiting_") ||
normalized.includes("blockers" + "=") ||
normalized.includes("github.com")
);
}
export function publicProjectText(value: string | null | undefined, fallback = "已脫敏專案"): string {
const raw = String(value ?? "").trim();
if (!raw) return fallback;
const normalized = normalizedIdentifier(raw);
const key = normalized.split("/").pop() ?? normalized;
if (PUBLIC_PROJECT_NAMES[normalized]) return PUBLIC_PROJECT_NAMES[normalized];
if (PUBLIC_PROJECT_NAMES[key]) return PUBLIC_PROJECT_NAMES[key];
if (!hasUnsafePublicIdentifier(raw) && CJK_TEXT_RE.test(raw)) return raw;
return fallback;
}
export function publicAgentText(value: string | null | undefined, fallback = "已脫敏執行代理"): string {
const raw = String(value ?? "").trim();
if (!raw) return fallback;
const normalized = normalizedIdentifier(raw);
const knownKey = Object.keys(PUBLIC_AGENT_NAMES).find((key) => normalized.includes(key));
if (knownKey) return PUBLIC_AGENT_NAMES[knownKey];
if (!hasUnsafePublicIdentifier(raw) && CJK_TEXT_RE.test(raw)) return raw;
return fallback;
}
export function publicInternalCodeSummary(
values: Array<string | null | undefined> | string | null | undefined,
emptyLabel = "無公開阻塞項"
): string {
const items = Array.isArray(values) ? values.filter(Boolean) : [values].filter(Boolean);
if (items.length === 0) return emptyLabel;
const readable = items
.map((item) => String(item ?? "").trim())
.filter((item) => item && !hasUnsafePublicIdentifier(item) && CJK_TEXT_RE.test(item) && !INTERNAL_CODE_RE.test(item));
if (readable.length > 0) return readable.slice(0, 3).join("、");
return `已脫敏狀態 ${items.length}`;
}
export function redactPublicIdentifier(value: string): string {
if (value.includes("actions.runner.")) return "已脫敏 runner systemd 服務";
return value.replace(/\b[a-z0-9][a-z0-9-]{2,}\/[A-Za-z0-9._-]+\b/g, "已脫敏專案來源");

View File

@@ -366,6 +366,56 @@ def validate(root: Path) -> None:
governance_automation_inventory_tab,
"redactPublicIdentifier",
)
for helper in ["publicProjectText", "publicAgentText", "publicInternalCodeSummary"]:
assert_text_contains(f"public_security_redaction.{helper}", public_security_redaction, helper)
for label, text in [
("awooop_runs_page", awooop_runs_page),
("awooop_run_detail_page", awooop_run_detail_page),
("awooop_approvals_page", awooop_approvals_page),
("awooop_approval_detail_page", awooop_approval_detail_page),
("awooop_contracts_page", awooop_contracts_page),
]:
assert_text_contains(f"{label}.public_project_display_redaction", text, "publicProjectText")
for label, text in [
("awooop_runs_page", awooop_runs_page),
("awooop_run_detail_page", awooop_run_detail_page),
("awooop_approvals_page", awooop_approvals_page),
("awooop_approval_detail_page", awooop_approval_detail_page),
]:
assert_text_contains(f"{label}.public_agent_display_redaction", text, "publicAgentText")
for label, text in [
("awooop_work_items_page", awooop_work_items_page),
("awooop_run_detail_page", awooop_run_detail_page),
("awooop_approval_detail_page", awooop_approval_detail_page),
]:
assert_text_contains(f"{label}.public_internal_code_summary", text, "publicInternalCodeSummary")
for label, text, forbidden in [
("awooop_runs_page.raw_project_display", awooop_runs_page, "{run.project_id || \"--\"}"),
("awooop_runs_page.raw_agent_display", awooop_runs_page, "{run.agent_id || \"--\"}"),
("awooop_runs_page.raw_event_project_display", awooop_runs_page, "{event.project_id}"),
("awooop_approvals_page.raw_project_display", awooop_approvals_page, "{approval.project_id || \"--\"}"),
("awooop_approvals_page.raw_agent_display", awooop_approvals_page, "{approval.agent_id || \"--\"}"),
("awooop_contracts_page.raw_project_display", awooop_contracts_page, "{contract.project_id || \"--\"}"),
("awooop_contracts_page.raw_tenant_option", awooop_contracts_page, "{t.display_name || t.project_id}"),
("awooop_run_detail_page.raw_project_detail", awooop_run_detail_page, "value={run?.project_id}"),
("awooop_run_detail_page.raw_agent_detail", awooop_run_detail_page, "value={run?.agent_id}"),
("awooop_run_detail_page.raw_trace_detail", awooop_run_detail_page, "value={run?.trace_id}"),
("awooop_run_detail_page.raw_trigger_detail", awooop_run_detail_page, "value={run?.trigger_type}"),
("awooop_run_detail_page.raw_trigger_ref_detail", awooop_run_detail_page, "value={run?.trigger_ref}"),
("awooop_approval_detail_page.raw_project_detail", awooop_approval_detail_page, "value={run.project_id}"),
("awooop_approval_detail_page.raw_agent_detail", awooop_approval_detail_page, "value={run.agent_id}"),
("awooop_approval_detail_page.raw_trace_detail", awooop_approval_detail_page, "value={run.trace_id}"),
("awooop_approval_detail_page.raw_trigger_detail", awooop_approval_detail_page, "value={run.trigger_type}"),
("awooop_approval_detail_page.raw_trigger_ref_detail", awooop_approval_detail_page, "value={run.trigger_ref}"),
("awooop_work_items_page.raw_blockers_join", awooop_work_items_page, "blockers.join(\", \")"),
("awooop_work_items_page.raw_required_fields_join", awooop_work_items_page, "requiredOwnerFields.join(\", \")"),
(
"awooop_work_items_page.raw_completion_required_fields_join",
awooop_work_items_page,
"item.required_owner_fields.join(\", \")",
),
]:
assert_text_not_contains(label, text, forbidden)
assert_text_not_contains("public_security_redaction.owner_namespace_literal", public_security_redaction, "owenhytsai")
assert_text_not_contains("public_security_redaction.external_namespace_literal", public_security_redaction, "nexu-io")