Remove pre-1.0 bridge governance scaffolding
This commit is contained in:
@@ -565,8 +565,7 @@ async function preparePluginOperations(headers, server, plugin) {
|
||||
serverInstanceId: server.id,
|
||||
operation: "install",
|
||||
targetVersion: plugin.version,
|
||||
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`,
|
||||
confirmed: false
|
||||
idempotencyKey: `browser-acceptance-plugin-install-${stamp}`
|
||||
},
|
||||
headers
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ const plugin: GamePluginResponse = {
|
||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||
runtimeProfiles: { lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install", "process.start", "process.stop"] }] },
|
||||
status: "installed"
|
||||
};
|
||||
@@ -58,7 +58,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
|
||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||
status: "installed",
|
||||
source: "platform-registry"
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ const pendingCommand = {
|
||||
commandType: "scum.diagnostic.ping",
|
||||
priority: 20,
|
||||
state: "pending",
|
||||
approvalState: "pending",
|
||||
requesterId: "user-1",
|
||||
expiresAt: later,
|
||||
createdAt: now,
|
||||
@@ -40,7 +39,6 @@ const pendingCommand = {
|
||||
const completedCommand = {
|
||||
...pendingCommand,
|
||||
state: "succeeded",
|
||||
approvalState: "approved",
|
||||
resultSummary: "diagnostic completed",
|
||||
result: {
|
||||
status: "succeeded",
|
||||
@@ -80,7 +78,6 @@ const manifestDeclaration: GameClientBridgeManifestResponse = {
|
||||
type: "scum.diagnostic.ping",
|
||||
title: "Diagnostic ping",
|
||||
permission: "server.game-client.command",
|
||||
approvalLevel: "operator",
|
||||
payloadSchemaRef: "schemas/bridge/commands/diagnostic-ping.request.json",
|
||||
resultSchemaRef: "schemas/bridge/commands/diagnostic-ping.result.json",
|
||||
timeoutSeconds: 30,
|
||||
@@ -126,8 +123,8 @@ const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pic
|
||||
describe("PlatformApiClient Game Client Bridge operator API", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("types plugin and marketplace manifest declarations with approval metadata", () => {
|
||||
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ approvalLevel: "operator" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
|
||||
it("types plugin and marketplace manifest declarations", () => {
|
||||
expect(pluginBridgeProjection.gameClientBridge).toMatchObject({ commands: [{ type: "scum.diagnostic.ping" }], queryTemplates: [{ engine: "sqlite" }], companion: { tlsPolicy: "verify-system-roots", sessionMode: "component-session" } });
|
||||
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
|
||||
});
|
||||
|
||||
@@ -157,7 +154,7 @@ describe("PlatformApiClient Game Client Bridge operator API", () => {
|
||||
|
||||
await expect(client.getGameClientBridgeStatus("server-1")).resolves.toMatchObject({ available: false, profiles: [{ profileKey: "scum-client" }] });
|
||||
await expect(client.listGameClientBridgeCommands("server-1", { profileKey: "scum-client", state: "pending", commandType: "scum.diagnostic.ping" })).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending", approvalState: "pending" });
|
||||
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending" });
|
||||
await expect(client.getGameClientBridgeCommand("server-1", pendingCommand.id)).resolves.toMatchObject({ result: { status: "succeeded", payload: { delivered: true } } });
|
||||
await expect(client.cancelGameClientBridgeCommand("server-1", pendingCommand.id, { reason: "maintenance window changed" })).resolves.toMatchObject({ state: "cancelled" });
|
||||
await expect(client.listGameClientBridgeSnapshots("server-1", { profileKey: "scum-client", type: "scum.players", streamKey: "current", observedAfter: now, limit: 20 })).resolves.toMatchObject({ count: 1, items: [{ sequence: 7 }] });
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("PlatformApiClient plugin operations", () => {
|
||||
const client = new PlatformApiClient("/api/v1", () => "session-token");
|
||||
|
||||
await client.listPluginLifecycles({ pluginId: "game.scum" });
|
||||
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
|
||||
await client.runPluginLifecycle("game.scum", { serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
|
||||
await client.listAIConfigDiffs({ state: "pending" });
|
||||
await client.approveAIConfigDiff("diff-1", "approve-1");
|
||||
|
||||
@@ -26,6 +26,6 @@ describe("PlatformApiClient plugin operations", () => {
|
||||
]);
|
||||
const serialized = JSON.stringify(calls);
|
||||
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
|
||||
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
|
||||
expect(calls[1]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,6 @@ export type RunUpdatePhase = "queued" | "downloading" | "staged" | "restart-requ
|
||||
export type ServerLifecycleAction = "create" | "start" | "stop" | "status";
|
||||
|
||||
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired" | "unknown";
|
||||
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
|
||||
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
|
||||
export type GameClientBridgeResultStatus = "succeeded" | "failed" | "cancelled" | "unknown";
|
||||
export type GameClientBridgeJsonValue = string | number | boolean | null | GameClientBridgeJsonValue[] | GameClientBridgeJsonObject;
|
||||
|
||||
@@ -27,7 +25,6 @@ export interface GameClientBridgeCommandDeclarationResponse {
|
||||
type: string;
|
||||
title: string;
|
||||
permission: string;
|
||||
approvalLevel: GameClientBridgeApprovalLevel;
|
||||
payloadSchemaRef: string;
|
||||
resultSchemaRef?: string;
|
||||
timeoutSeconds: number;
|
||||
@@ -141,7 +138,6 @@ export interface GameClientBridgeCommandResponse {
|
||||
commandType: string;
|
||||
priority: number;
|
||||
state: GameClientBridgeCommandState;
|
||||
approvalState: GameClientBridgeApprovalState;
|
||||
requesterId?: string;
|
||||
resultSummary?: string;
|
||||
result?: GameClientBridgeCommandResultResponse;
|
||||
@@ -1589,7 +1585,6 @@ export interface AIConfigRecommendationResponse {
|
||||
export interface PluginProductionLifecycleDeclaration {
|
||||
operations: PluginLifecycleOperation[];
|
||||
dependencyPolicy: "required" | "optional";
|
||||
approvalRequired: Array<"disable" | "rollback" | "retire">;
|
||||
}
|
||||
|
||||
export type PluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
|
||||
@@ -1611,7 +1606,7 @@ export interface PluginLifecycleInstallationResponse {
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface PluginLifecycleListResponse { items: PluginLifecycleInstallationResponse[]; count: number; }
|
||||
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; confirmed: boolean; }
|
||||
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; }
|
||||
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; }
|
||||
|
||||
export interface AIConfigDiffPreviewResponse {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { PluginLifecycleInstallationResponse, PluginLifecycleOperation, ServerInstanceResponse } from "../api/types";
|
||||
import { ConfirmDialog } from "./OperationControls";
|
||||
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
|
||||
|
||||
interface PluginLifecycleWorkbenchProps {
|
||||
@@ -22,7 +21,6 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
||||
const [targetVersion, setTargetVersion] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
|
||||
|
||||
@@ -60,16 +58,13 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
||||
serverInstanceId: selectedServerId,
|
||||
operation,
|
||||
targetVersion: targetVersion.trim() || undefined,
|
||||
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`,
|
||||
confirmed: disruptiveOperations.includes(operation)
|
||||
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`
|
||||
});
|
||||
const evidence = [response.job?.id && `任务 ${response.job.id}`].filter(Boolean).join(" · ");
|
||||
setResult({ status: response.status === "queued" || response.status === "accepted" ? "succeeded" : response.status === "deferred" ? "pending" : "failed", label: `${lifecycleOperationLabel(operation)}:${response.status}${evidence ? ` · ${evidence}` : ""}` });
|
||||
setConfirming(false);
|
||||
await refresh();
|
||||
} catch (caught) {
|
||||
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "插件生命周期操作失败" });
|
||||
setConfirming(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -95,7 +90,7 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
||||
{operations.map((item) => <option key={item} value={item}>{lifecycleOperationLabel(item)}</option>)}
|
||||
</select>
|
||||
{(operation === "install" || operation === "upgrade") && <input aria-label="目标版本" placeholder="目标版本" value={targetVersion} disabled={busy} onChange={(event) => setTargetVersion(event.target.value)} />}
|
||||
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
|
||||
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => void submit()}>{busy ? "提交中…" : "执行"}</button>
|
||||
</div>
|
||||
{installation ? (
|
||||
<div className="console-record plugin-lifecycle-state">
|
||||
@@ -109,14 +104,11 @@ export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = li
|
||||
) : <p className="console-empty-note">该服务器尚无插件生命周期记录。</p>}
|
||||
</>
|
||||
)}
|
||||
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}。`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const lifecycleOperations: PluginLifecycleOperation[] = ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"];
|
||||
const disruptiveOperations: PluginLifecycleOperation[] = ["disable", "rollback", "retire"];
|
||||
|
||||
function lifecycleOperationLabel(operation: PluginLifecycleOperation) {
|
||||
return ({ install: "安装", enable: "启用", disable: "停用", upgrade: "升级", rollback: "回滚", retire: "退役", "dependency-check": "依赖检查" } as Record<PluginLifecycleOperation, string>)[operation];
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const plugin: GamePluginResponse = {
|
||||
pages: [],
|
||||
tags: [],
|
||||
aiPurposes: [],
|
||||
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional", approvalRequired: [] },
|
||||
productionLifecycle: { operations: ["install"], dependencyPolicy: "optional" },
|
||||
status: "installed",
|
||||
runtimeProfiles: {
|
||||
transportProfiles: [{ key: "rcon", kind: "rcon", targetKey: "rcon.password", capabilities: ["remote.run.rcon.command"] }],
|
||||
|
||||
@@ -69,9 +69,9 @@ const plugin: GamePluginResponse = {
|
||||
}],
|
||||
tags: ["scum"],
|
||||
aiPurposes: [],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required" },
|
||||
gameClientBridge: {
|
||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
||||
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
|
||||
commandRetentionSeconds: 86400,
|
||||
maxCommands: 1000,
|
||||
|
||||
@@ -23,7 +23,7 @@ const marketplacePlugin: MarketplacePluginResponse = {
|
||||
pages: [{ key: "logs", title: "Logs", path: "/logs", permissions: ["server.logs.read"], bridgeActions: ["logs.query"] }],
|
||||
tags: ["example"],
|
||||
aiPurposes: ["logs.diagnose"],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||
runtimeProfiles: {
|
||||
dllExtensions: [{
|
||||
key: "scum-simple-rcon-ue4ss",
|
||||
|
||||
@@ -16,7 +16,6 @@ const safeCommand = {
|
||||
commandType: "scum.player.lookup",
|
||||
priority: 10,
|
||||
state: "succeeded",
|
||||
approvalState: "not_required",
|
||||
requesterId: "user-1",
|
||||
result: { status: "succeeded", summary: "player found", payload: { found: true }, completedAt: now },
|
||||
expiresAt: now,
|
||||
@@ -45,14 +44,14 @@ const safeSnapshotList = {
|
||||
};
|
||||
|
||||
describe("Game Client Bridge safe projection schema", () => {
|
||||
it("preserves declarations, approval, result, retention and typed snapshot payloads", () => {
|
||||
it("preserves declarations, result, retention and typed snapshot payloads", () => {
|
||||
expect(parseSafeGameClientBridgeStatus({
|
||||
serverInstanceId: "server-1",
|
||||
pluginId: "game.scum",
|
||||
available: true,
|
||||
profiles: [{ pluginId: "game.scum", profileKey: "scum-client", available: true, commandTypes: ["scum.player.lookup"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }]
|
||||
})).toMatchObject({ available: true, profiles: [{ queryTemplateKeys: ["scum.player.search"] }] });
|
||||
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ approvalState: "not_required", result: { payload: { found: true } } });
|
||||
expect(parseSafeGameClientBridgeCommand(safeCommand)).toMatchObject({ result: { payload: { found: true } } });
|
||||
expect(parseSafeGameClientBridgeSnapshotList(safeSnapshotList)).toMatchObject({ count: 1, items: [{ retention: { maxRecords: 24 } }] });
|
||||
expect(parseSafeGameClientBridgeSnapshotList({
|
||||
...safeSnapshotList,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
GameClientBridgeApprovalState,
|
||||
GameClientBridgeCancelResponse,
|
||||
GameClientBridgeCommandCancellationResponse,
|
||||
GameClientBridgeCommandListResponse,
|
||||
@@ -16,7 +15,6 @@ import type {
|
||||
} from "../api/types";
|
||||
|
||||
const commandStates = new Set<GameClientBridgeCommandState>(["pending", "claimed", "succeeded", "failed", "cancelled", "expired", "unknown"]);
|
||||
const approvalStates = new Set<GameClientBridgeApprovalState>(["not_required", "pending", "approved", "rejected"]);
|
||||
const resultStatuses = new Set<GameClientBridgeResultStatus>(["succeeded", "failed", "cancelled", "unknown"]);
|
||||
const forbiddenKeys = new Set([
|
||||
"apikey",
|
||||
@@ -84,7 +82,6 @@ export function parseSafeGameClientBridgeCommand(value: unknown): GameClientBrid
|
||||
commandType: string(record.commandType, "commandType"),
|
||||
priority: number(record.priority, "priority"),
|
||||
state: commandState(record.state),
|
||||
approvalState: approvalState(record.approvalState),
|
||||
expiresAt: string(record.expiresAt, "expiresAt"),
|
||||
createdAt: string(record.createdAt, "createdAt"),
|
||||
updatedAt: string(record.updatedAt, "updatedAt")
|
||||
@@ -284,12 +281,6 @@ function commandState(value: unknown): GameClientBridgeCommandState {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function approvalState(value: unknown): GameClientBridgeApprovalState {
|
||||
const parsed = string(value, "approvalState") as GameClientBridgeApprovalState;
|
||||
if (!approvalStates.has(parsed)) throw new Error("Game Client Bridge approval state is invalid");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resultStatus(value: unknown): GameClientBridgeResultStatus {
|
||||
const parsed = string(value, "result.status") as GameClientBridgeResultStatus;
|
||||
if (!resultStatuses.has(parsed)) throw new Error("Game Client Bridge result status is invalid");
|
||||
|
||||
@@ -23,7 +23,7 @@ const plugin: GamePluginResponse = {
|
||||
pages: [],
|
||||
tags: [],
|
||||
aiPurposes: [],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "optional" },
|
||||
status: "installed",
|
||||
runtimeProfiles: {
|
||||
discovery: [{ key: "root-check", kind: "file.exists", targetKey: "server-root", required: true }],
|
||||
|
||||
Reference in New Issue
Block a user