feat(plugin): add governance release gates
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
Docker Image / docker (push) Has been cancelled

This commit is contained in:
2026-06-26 12:08:27 +08:00
parent b822993489
commit 629d6d5dbc
17 changed files with 2976 additions and 110 deletions

View File

@@ -29,19 +29,21 @@ func newAdminAPIHandler() http.HandlerFunc {
AuditLogs: handleAdminAuditLogs,
PluginArtifacts: handleAdminPluginArtifacts,
PluginArtifact: handleAdminPluginArtifact,
PluginSources: handleAdminPluginSources,
PluginBuilds: handleAdminPluginBuilds,
PluginBuild: handleAdminPluginBuild,
PluginGC: handleAdminPluginGC,
PluginsList: handleAdminPluginsList,
PluginItem: handleAdminPluginItem,
PluginAction: handleAdminPluginAction,
PluginConfig: handleAdminPluginConfig,
PluginSecrets: handleAdminPluginSecrets,
PluginRollback: handleAdminPluginRollback,
PluginDraining: handleAdminPluginDraining,
PluginDispatch: handleAdminPluginDispatchPlan,
PluginArtifacts: handleAdminPluginArtifacts,
PluginArtifact: handleAdminPluginArtifact,
PluginSources: handleAdminPluginSources,
PluginBuilds: handleAdminPluginBuilds,
PluginBuild: handleAdminPluginBuild,
PluginGC: handleAdminPluginGC,
PluginsList: handleAdminPluginsList,
PluginItem: handleAdminPluginItem,
PluginAction: handleAdminPluginAction,
PluginConfig: handleAdminPluginConfig,
PluginSecrets: handleAdminPluginSecrets,
PluginRollback: handleAdminPluginRollback,
PluginDraining: handleAdminPluginDraining,
PluginDispatch: handleAdminPluginDispatchPlan,
PluginGovernance: handleAdminPluginGovernance,
PluginAdvisories: handleAdminPluginAdvisories,
})
}

View File

@@ -314,6 +314,105 @@ func TestAdminPluginPhase4API(t *testing.T) {
}
}
func TestAdminPluginPhase5GovernanceAPI(t *testing.T) {
handler := newAdminTestHandlerWithAdmin(t)
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: adminDB,
ArtifactRoot: filepath.Join(filepath.Dir(adminDBPath), "plugins", "artifacts"),
Adapter: gatewayTestPluginAdapter{},
})
adminToken := adminTestLogin(t, handler, "admin", "secret")
resp := adminTestRequest(t, handler, http.MethodPost, "/admin/api/users", adminToken, map[string]any{
"username": "member",
"role": "member",
"password": "member-secret",
})
if resp.Code != http.StatusCreated {
t.Fatalf("create member status = %d, body=%s", resp.Code, resp.Body.String())
}
memberToken := adminTestLogin(t, handler, "member", "member-secret")
artifact := uploadGatewayPhase5ProtocolProxyArtifact(t, "phase5-proxy")
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "phase5-proxy", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
resp = adminTestRequest(t, handler, http.MethodGet, "/admin/api/plugins/phase5-proxy/governance?profile=prod", memberToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("member governance status = %d, body=%s", resp.Code, resp.Body.String())
}
if !strings.Contains(resp.Body.String(), "review_required") {
t.Fatalf("governance status body = %s, want review_required", resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugins/phase5-proxy/governance/review", memberToken, map[string]any{
"artifact_id": artifact.ID,
"profile": pluginmanager.PolicyProfileProd,
})
if resp.Code != http.StatusForbidden {
t.Fatalf("member review write status = %d, want forbidden; body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugins/phase5-proxy/governance/review", adminToken, map[string]any{
"artifact_id": artifact.ID,
"profile": pluginmanager.PolicyProfileProd,
"decision": pluginmanager.ReviewDecisionApproved,
"notes": "phase 5 approval",
})
if resp.Code != http.StatusOK {
t.Fatalf("admin review write status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugins/phase5-proxy/governance/preflight", adminToken, map[string]any{
"artifact_id": artifact.ID,
"profile": pluginmanager.PolicyProfileProd,
"action": pluginmanager.GovernanceActionEnable,
})
if resp.Code != http.StatusOK {
t.Fatalf("preflight status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugins/phase5-proxy/governance/benchmark", adminToken, map[string]any{
"artifact_id": artifact.ID,
"profile": pluginmanager.PolicyProfileProd,
"benchmark_profile": "release",
"p95_ms": 10,
"p99_ms": 20,
"baseline_diff": 0.25,
"active_proxy_capacity": 100,
})
if resp.Code != http.StatusOK {
t.Fatalf("benchmark status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugins/phase5-proxy/governance/override", adminToken, map[string]any{
"artifact_id": artifact.ID,
"profile": pluginmanager.PolicyProfileProd,
"action": pluginmanager.GovernanceActionEnable,
"reason": "accepted warning for rollout",
"ttl_seconds": 3600,
})
if resp.Code != http.StatusOK {
t.Fatalf("override status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodPost, "/admin/api/plugin-advisories", adminToken, map[string]any{
"advisory_id": "MCG-2026-ADMIN",
"status": pluginmanager.AdvisoryStatusRevoked,
"action": pluginmanager.AdvisoryActionRevoke,
"artifact_sha256": artifact.SHA256,
})
if resp.Code != http.StatusOK {
t.Fatalf("advisory status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodGet, "/admin/api/plugin-advisories?plugin_id=phase5-proxy", memberToken, nil)
if resp.Code != http.StatusOK || !strings.Contains(resp.Body.String(), "MCG-2026-ADMIN") {
t.Fatalf("member advisory read status = %d, body=%s", resp.Code, resp.Body.String())
}
resp = adminTestRequest(t, handler, http.MethodGet, "/admin/api/audit-logs", adminToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("audit status = %d, body=%s", resp.Code, resp.Body.String())
}
for _, action := range []string{"plugin_governance_review", "plugin_governance_override", "plugin_governance_advisory"} {
if !strings.Contains(resp.Body.String(), action) {
t.Fatalf("audit body missing %s: %s", action, resp.Body.String())
}
}
}
func uploadGatewayPhase4Artifact(t *testing.T, pluginID string) pluginmanager.ArtifactRecord {
t.Helper()
var manifest pluginmanager.Manifest
@@ -347,6 +446,31 @@ func uploadGatewayPhase4Artifact(t *testing.T, pluginID string) pluginmanager.Ar
return artifact
}
func uploadGatewayPhase5ProtocolProxyArtifact(t *testing.T, pluginID string) pluginmanager.ArtifactRecord {
t.Helper()
var manifest pluginmanager.Manifest
if err := json.Unmarshal(gatewayTestManifestWithCapabilities(t, pluginID, gatewayProtocolProxyCapabilities()), &manifest); err != nil {
t.Fatalf("Unmarshal manifest error = %v", err)
}
manifest.RuntimeLimits = pluginmanager.RuntimeLimits{HandlerTimeoutMS: 3000}
manifestBytes, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("Marshal manifest error = %v", err)
}
artifact, err := pluginsManager.UploadArtifact(context.Background(), pluginmanager.ArtifactUpload{
SourcePath: writeGatewayTestMCGPEntries(t, map[string][]byte{
"manifest.json": manifestBytes,
"plugin.so": []byte("fake plugin bytes " + pluginID),
}),
FileName: pluginID + ".mcgp",
Actor: "admin",
})
if err != nil {
t.Fatalf("UploadArtifact() error = %v", err)
}
return artifact
}
func newAdminTestHandler(t *testing.T) http.Handler {
t.Helper()
t.Cleanup(saveGatewayState(t))

View File

@@ -117,6 +117,42 @@ export interface PluginProxyConnection {
draining: boolean;
}
export interface GovernanceIssue {
code: string;
severity: string;
message: string;
plugin_id?: string;
artifact_id?: string;
details?: Record<string, unknown>;
}
export interface GovernanceDecision {
ok: boolean;
action: string;
profile: string;
risk_level: string;
policy_hash: string;
review_required: boolean;
warning_override_used: boolean;
issues?: GovernanceIssue[];
checks?: GovernanceIssue[];
}
export interface GovernanceStatus {
decision?: GovernanceDecision;
policy?: Record<string, unknown>;
reviews?: Record<string, unknown>[];
warning_overrides?: Record<string, unknown>[];
preflights?: Record<string, unknown>[];
benchmarks?: Record<string, unknown>[];
advisories?: Record<string, unknown>[];
conflicts?: {
ok: boolean;
issues?: GovernanceIssue[];
plan?: unknown;
};
}
export interface PluginView {
id: string;
name?: string;
@@ -151,6 +187,8 @@ export interface PluginView {
manifest?: Record<string, unknown>;
active_proxy_connections?: number;
proxy_connections?: PluginProxyConnection[];
governance?: GovernanceStatus;
governance_error?: string;
updated_at?: number;
}

View File

@@ -167,6 +167,10 @@ export function renderPluginDetail(plugin: PluginView | null = selectedPlugin())
<dt>Minecraft</dt><dd>${escapeHTML(formatJSON(plugin.minecraft))}</dd>
</dl>
</section>
<section class="panel">
<h3>Governance</h3>
${governancePanel(plugin, canWrite)}
</section>
<section class="panel">
<h3>Config</h3>
<textarea id="pluginConfigEditor" ${canWrite ? "" : "readonly"}>${escapeHTML(prettyJSON(plugin.config_json || "{}"))}</textarea>
@@ -262,6 +266,12 @@ function bindPluginDetailEvents(plugin: PluginView): void {
document.querySelectorAll<HTMLButtonElement>("[data-snapshot-diff]").forEach((button) => {
button.addEventListener("click", () => showSnapshotDiff(plugin.id, Number(button.dataset.snapshotDiff || "0")));
});
document.getElementById("pluginGovernanceReviewBtn")?.addEventListener("click", () => createGovernanceReview(plugin));
document.getElementById("pluginGovernanceOverrideBtn")?.addEventListener("click", () => createGovernanceOverride(plugin));
document.getElementById("pluginGovernancePreflightBtn")?.addEventListener("click", () => runGovernancePreflight(plugin));
document.getElementById("pluginGovernanceSelfTestBtn")?.addEventListener("click", () => runGovernanceSelfTest(plugin));
document.getElementById("pluginGovernanceBenchmarkBtn")?.addEventListener("click", () => recordGovernanceBenchmark(plugin));
document.getElementById("pluginGovernanceAdvisoryBtn")?.addEventListener("click", () => createArtifactRevokeAdvisory(plugin));
}
async function dryRunConfig(plugin: PluginView): Promise<void> {
@@ -413,6 +423,117 @@ async function showSnapshotDiff(pluginID: string, snapshotID: number): Promise<v
}
}
async function createGovernanceReview(plugin: PluginView): Promise<void> {
try {
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/review`, {
method: "POST",
body: { artifact_id: plugin.desired_artifact_id, profile: "prod", decision: "approved" },
});
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
async function createGovernanceOverride(plugin: PluginView): Promise<void> {
const reason = window.prompt("Reason");
if (!reason) {
return;
}
try {
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/override`, {
method: "POST",
body: { artifact_id: plugin.desired_artifact_id, profile: "prod", action: "enable", reason, ttl_seconds: 3600 },
});
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
async function runGovernancePreflight(plugin: PluginView): Promise<void> {
try {
const data = await api<Record<string, unknown>>(`/plugins/${encodeURIComponent(plugin.id)}/governance/preflight`, {
method: "POST",
body: { artifact_id: plugin.desired_artifact_id, config_json: configEditorValue() },
});
el("pluginDryRunResult").textContent = formatJSON(data);
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
async function runGovernanceSelfTest(plugin: PluginView): Promise<void> {
try {
const data = await api<Record<string, unknown>>(`/plugins/${encodeURIComponent(plugin.id)}/governance/self-test`, {
method: "POST",
body: { artifact_id: plugin.desired_artifact_id },
});
el("pluginDryRunResult").textContent = formatJSON(data);
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
async function recordGovernanceBenchmark(plugin: PluginView): Promise<void> {
const diff = Number(window.prompt("Baseline diff, e.g. 0.25", "0.25"));
if (!Number.isFinite(diff)) {
return;
}
try {
await api(`/plugins/${encodeURIComponent(plugin.id)}/governance/benchmark`, {
method: "POST",
body: {
artifact_id: plugin.desired_artifact_id,
profile: "prod",
benchmark_profile: "manual",
p95_ms: 0,
p99_ms: 0,
error_rate: 0,
active_proxy_capacity: 0,
baseline_diff: diff,
},
});
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
async function createArtifactRevokeAdvisory(plugin: PluginView): Promise<void> {
const artifact = plugin.desired_artifact;
if (!artifact) {
return;
}
const advisoryID = window.prompt("Advisory ID", `local-${shortID(artifact.sha256)}`);
if (!advisoryID) {
return;
}
try {
await api("/plugin-advisories", {
method: "POST",
body: {
advisory_id: advisoryID,
status: "revoked",
action: "revoke",
artifact_sha256: artifact.sha256,
recommended_action: "rollback or upgrade",
},
});
await loadPluginDetail(plugin.id);
showAlert("");
} catch (err) {
showAlert((err as Error).message);
}
}
function uploadInventoryDetail(): string {
const artifact = selectedArtifact();
if (!artifact) {
@@ -530,6 +651,50 @@ function pluginActionButtons(plugin: PluginView): string {
`;
}
function governancePanel(plugin: PluginView, canWrite: boolean): string {
if (plugin.governance_error) {
return `<div class="alert inline-alert">${escapeHTML(plugin.governance_error)}</div>`;
}
const governance = plugin.governance;
const decision = governance?.decision;
const issues = decision?.issues || [];
return `
<dl class="kv">
<dt>Profile</dt><dd>${escapeHTML(decision?.profile || "")}</dd>
<dt>Risk</dt><dd>${escapeHTML(decision?.risk_level || "")}</dd>
<dt>Policy</dt><dd>${escapeHTML(shortID(decision?.policy_hash || ""))}</dd>
<dt>Decision</dt><dd>${badge(decision?.ok ? "allowed" : "blocked", !decision?.ok)}</dd>
<dt>Review</dt><dd>${badge(decision?.review_required ? "required" : "not required", Boolean(decision?.review_required))}</dd>
<dt>Override</dt><dd>${badge(decision?.warning_override_used ? "used" : "not used", Boolean(decision?.warning_override_used))}</dd>
</dl>
${issues.length ? `<div class="mini-list">${issues.map((issue) => `
<div class="mini-row">
<span>${badge(issue.severity, issue.severity !== "info")}</span>
<span>${escapeHTML(issue.code)}</span>
<span>${escapeHTML(issue.message)}</span>
</div>
`).join("")}</div>` : `<div class="empty">No governance issues</div>`}
${canWrite ? `
<div class="row-actions">
<button class="secondary" type="button" id="pluginGovernanceReviewBtn">Review</button>
<button class="secondary" type="button" id="pluginGovernanceOverrideBtn">Override</button>
<button class="secondary" type="button" id="pluginGovernancePreflightBtn">Preflight</button>
<button class="secondary" type="button" id="pluginGovernanceSelfTestBtn">Self-test</button>
<button class="secondary" type="button" id="pluginGovernanceBenchmarkBtn">Benchmark</button>
<button class="danger" type="button" id="pluginGovernanceAdvisoryBtn">Revoke artifact</button>
</div>
` : ""}
<pre class="log-output">${escapeHTML(formatJSON({
conflicts: governance?.conflicts,
reviews: governance?.reviews || [],
warning_overrides: governance?.warning_overrides || [],
preflights: governance?.preflights || [],
benchmarks: governance?.benchmarks || [],
advisories: governance?.advisories || [],
}))}</pre>
`;
}
function artifactList(artifacts: PluginArtifact[], plugin: PluginView): string {
if (artifacts.length === 0) {
return `<div class="empty">No artifacts</div>`;

View File

@@ -671,6 +671,175 @@ func handleAdminPluginDispatchPlan(w http.ResponseWriter, r *http.Request) {
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"dispatch_plan": pluginsManager.DispatchPlan(r.Context())})
}
func handleAdminPluginGovernance(w http.ResponseWriter, r *http.Request, rawSegment string) {
session, ok := requireRole(w, r, adminRoleMember)
if !ok {
return
}
if pluginsManager == nil {
adminhttp.WriteAPIError(w, http.StatusServiceUnavailable, "plugin manager is not initialized")
return
}
pluginID, action, ok := splitPluginSubresource(w, rawSegment, "governance")
if !ok {
return
}
switch {
case r.Method == http.MethodGet && action == "":
status, err := pluginsManager.GovernanceStatus(r.Context(), pluginID, r.URL.Query().Get("artifact_id"), r.URL.Query().Get("profile"))
if err != nil {
writePluginManagerError(w, err)
return
}
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"governance": status})
case r.Method == http.MethodPost && action == "review":
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.GovernanceReviewRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
review, err := pluginsManager.CreateReview(r.Context(), session.Username, pluginID, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_review", "plugin", pluginID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_review", "plugin", pluginID, true, "governance review recorded", map[string]any{
"artifact_id": review.ArtifactID,
"profile": review.Profile,
"policy_hash": review.PolicyHash,
"decision": review.Decision,
})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"review": review})
case r.Method == http.MethodPost && action == "override":
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.WarningOverrideRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
override, err := pluginsManager.CreateWarningOverride(r.Context(), session.Username, pluginID, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_override", "plugin", pluginID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_override", "plugin", pluginID, true, "governance warning override recorded", map[string]any{
"artifact_id": override.ArtifactID,
"profile": override.Profile,
"action": override.Action,
"expires_at": override.ExpiresAt,
})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"override": override})
case r.Method == http.MethodPost && action == "preflight":
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.PreflightRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
result, err := pluginsManager.RunPreflight(r.Context(), session.Username, pluginID, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_preflight", "plugin", pluginID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_preflight", "plugin", pluginID, result.OK, "governance preflight completed", map[string]any{"profile": result.Profile})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"preflight": result})
case r.Method == http.MethodPost && action == "self-test":
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.SelfTestRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
result, err := pluginsManager.RunSelfTest(r.Context(), session.Username, pluginID, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_self_test", "plugin", pluginID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_self_test", "plugin", pluginID, result.OK, "governance self-test completed", map[string]any{"profile": result.Profile})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"self_test": result})
case r.Method == http.MethodPost && action == "benchmark":
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.BenchmarkRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
benchmark, err := pluginsManager.SaveBenchmark(r.Context(), session.Username, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_benchmark", "plugin", pluginID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_benchmark", "plugin", pluginID, true, "governance benchmark recorded", map[string]any{
"artifact_id": benchmark.ArtifactID,
"profile": benchmark.Profile,
"baseline_diff": benchmark.BaselineDiff,
})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"benchmark": benchmark})
default:
adminhttp.WriteAPIError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func handleAdminPluginAdvisories(w http.ResponseWriter, r *http.Request) {
session, ok := requireRole(w, r, adminRoleMember)
if !ok {
return
}
if pluginsManager == nil {
adminhttp.WriteAPIError(w, http.StatusServiceUnavailable, "plugin manager is not initialized")
return
}
switch r.Method {
case http.MethodGet:
advisories, err := pluginsManager.ListAdvisories(r.Context(), r.URL.Query().Get("plugin_id"))
if err != nil {
adminhttp.WriteAPIError(w, http.StatusInternalServerError, err.Error())
return
}
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"advisories": advisories})
case http.MethodPost:
if session.Role != adminRoleAdmin {
adminhttp.WriteAPIError(w, http.StatusForbidden, "forbidden")
return
}
var req pluginmanager.AdvisoryRequest
if !adminhttp.DecodeJSONRequest(w, r, &req) {
return
}
advisory, err := pluginsManager.UpsertAdvisory(r.Context(), session.Username, req)
if err != nil {
recordAudit(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_advisory", "plugin_advisory", req.AdvisoryID, false, err.Error())
writePluginManagerError(w, err)
return
}
recordAuditMetadata(r.Context(), session.Username, adminhttp.RequestSourceIP(r), "plugin_governance_advisory", "plugin_advisory", advisory.AdvisoryID, true, "plugin advisory upserted", map[string]any{
"action": advisory.Action,
"status": advisory.Status,
"artifact_sha256": advisory.ArtifactSHA256,
"plugin_id": advisory.PluginID,
})
adminhttp.WriteJSON(w, http.StatusOK, map[string]any{"advisory": advisory})
default:
adminhttp.WriteAPIError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func receivePluginArtifact(r *http.Request, actor string) (pluginmanager.ArtifactRecord, error) {
if err := r.ParseMultipartForm(64 << 20); err != nil {
return pluginmanager.ArtifactRecord{}, err
@@ -870,6 +1039,12 @@ func pluginView(r *http.Request, plugin pluginmanager.PluginRecord, detail bool)
if detail {
view["manifest"] = manifest
view["operations_path"] = "/plugin-artifacts?plugin_id=" + plugin.ID
governance, err := pluginsManager.GovernanceStatus(r.Context(), plugin.ID, plugin.DesiredArtifactID, pluginsManager.PolicyProfile())
if err != nil {
view["governance_error"] = err.Error()
} else {
view["governance"] = governance
}
view["active_proxy_connections"] = activeProxyConnections(plugin)
connections, err := pluginsManager.ActiveProxyConnections(r.Context(), plugin.ID)
if err != nil {

View File

@@ -90,10 +90,11 @@ func TestHandleRequestProtocolProxyReplaysInitialDataOnce(t *testing.T) {
return gatewayEnd, nil
}},
})
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", `{"upstream_connect":{"mode":"protocol-proxy"}}`)
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", gatewayProtocolProxyCapabilities())
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "proxy-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
approveGatewayPluginGovernanceForTest(t, "proxy-plugin", artifact.ID)
if _, err := pluginsManager.Enable(context.Background(), "admin", "proxy-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
@@ -216,10 +217,11 @@ func TestHandleRequestProtocolProxyDisableSkipsNewConnections(t *testing.T) {
return gatewayEnd, nil
}},
})
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", `{"upstream_connect":{"mode":"protocol-proxy"}}`)
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", gatewayProtocolProxyCapabilities())
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "proxy-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
approveGatewayPluginGovernanceForTest(t, "proxy-plugin", artifact.ID)
if _, err := pluginsManager.Enable(context.Background(), "admin", "proxy-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}

View File

@@ -279,6 +279,18 @@ func uploadGatewayTestArtifactWithCapabilities(t *testing.T, manager *pluginmana
return artifact
}
func approveGatewayPluginGovernanceForTest(t *testing.T, pluginID, artifactID string) {
t.Helper()
if _, err := pluginsManager.CreateReview(context.Background(), "admin", pluginID, pluginmanager.GovernanceReviewRequest{
ArtifactID: artifactID,
Profile: pluginmanager.PolicyProfileProd,
Decision: pluginmanager.ReviewDecisionApproved,
Notes: "test approval",
}); err != nil {
t.Fatalf("CreateReview(%s) error = %v", pluginID, err)
}
}
func writeGatewayTestMCGP(t *testing.T, pluginID string) string {
return writeGatewayTestMCGPWithCapabilities(t, pluginID, "")
}
@@ -322,6 +334,18 @@ func gatewayTestManifest(t *testing.T, pluginID string) []byte {
return gatewayTestManifestWithCapabilities(t, pluginID, "")
}
func gatewayProtocolProxyCapabilities() string {
return `{
"upstream_connect":{"mode":"protocol-proxy"},
"scope":{"type":"host","values":["play.example"]},
"rollout":{"mode":"canary"},
"minecraft":{
"protocol_versions":{"tested":[767]},
"forwarding":{"supported":["none"],"default":"none"}
}
}`
}
func gatewayTestManifestWithCapabilities(t *testing.T, pluginID string, capabilities string) []byte {
t.Helper()
if capabilities == "" {