功能修改
This commit is contained in:
@@ -9,7 +9,7 @@ const provider: AiProviderResponse = {
|
||||
id: "ai.openai",
|
||||
name: "OpenAI",
|
||||
kind: "openai",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
baseUrlConfigured: true,
|
||||
apiKeyConfigured: true,
|
||||
models: ["gpt-4.1"],
|
||||
defaultModel: "gpt-4.1",
|
||||
@@ -35,6 +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"] },
|
||||
runtimeProfiles: { lifecycleProfiles: [{ key: "local", mode: "local-process", capabilities: ["process.install", "process.start", "process.stop"] }] },
|
||||
status: "installed"
|
||||
};
|
||||
@@ -57,6 +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"] },
|
||||
status: "installed",
|
||||
source: "platform-registry"
|
||||
};
|
||||
@@ -181,7 +183,7 @@ describe("PlatformApiClient AI providers", () => {
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
kind: provider.kind,
|
||||
baseUrl: provider.baseUrl,
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKeyRef: "secret://providers/openai",
|
||||
models: provider.models,
|
||||
defaultModel: provider.defaultModel,
|
||||
@@ -725,11 +727,13 @@ describe("PlatformApiClient AI providers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps raw key fields out of provider responses", () => {
|
||||
it("keeps raw key and base URL fields out of provider responses", () => {
|
||||
expect("apiKey" in provider).toBe(false);
|
||||
expect("rawApiKey" in provider).toBe(false);
|
||||
expect(provider.apiKeyConfigured).toBe(true);
|
||||
expect("apiKeyRef" in provider).toBe(false);
|
||||
expect("baseUrl" in provider).toBe(false);
|
||||
expect(provider.baseUrlConfigured).toBe(true);
|
||||
});
|
||||
|
||||
it("calls auth endpoints and attaches bearer sessions", async () => {
|
||||
@@ -804,6 +808,24 @@ describe("PlatformApiClient AI providers", () => {
|
||||
});
|
||||
expect(onAuthFailure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces allow-listed plugin capability denials", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({
|
||||
code: "forbidden",
|
||||
message: "plugin does not declare required permission: server.run.distribution"
|
||||
}), { status: 403, headers: { "Content-Type": "application/json" } })));
|
||||
|
||||
const client = new PlatformApiClient("/api/v1", () => "admin-session");
|
||||
await expect(client.generateRunDistribution("server-local-debug", {
|
||||
targetOs: "linux",
|
||||
targetArch: "amd64",
|
||||
idempotencyKey: "test"
|
||||
})).rejects.toMatchObject({
|
||||
status: 403,
|
||||
code: "forbidden",
|
||||
message: "插件未声明所需权限:server.run.distribution"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
|
||||
+169
-1
@@ -8,6 +8,11 @@ import type {
|
||||
AiProviderUpdateRequest,
|
||||
AIInvocationRequest,
|
||||
AIInvocationResponse,
|
||||
AIConfigDiffApprovalResponse,
|
||||
AIConfigDiffListResponse,
|
||||
AlertListResponse,
|
||||
AlertResponse,
|
||||
AlertRetryResponse,
|
||||
ApiErrorResponse,
|
||||
ArtifactContentChunk,
|
||||
ArtifactDownloadReferenceResponse,
|
||||
@@ -16,8 +21,16 @@ import type {
|
||||
AuthSessionResponse,
|
||||
AuditEventListResponse,
|
||||
ClientManagerBuildRequest,
|
||||
ClientManagerControlRequest,
|
||||
ClientManagerDeployRequest,
|
||||
ClientManagerDistributionResponse,
|
||||
ClientManagerDownloadRequest,
|
||||
ClientManagerInstallationListResponse,
|
||||
ClientManagerInstallationResponse,
|
||||
ClientManagerRetryRequest,
|
||||
ClientManagerRevokeSessionRequest,
|
||||
ClientManagerUninstallRequest,
|
||||
ClientManagerUpdateRequest,
|
||||
ComponentKeyResponse,
|
||||
ComponentKeyResetRequest,
|
||||
CurrentUserResponse,
|
||||
@@ -25,6 +38,15 @@ import type {
|
||||
DependencyJobRequest,
|
||||
FileOperationDispatchRequest,
|
||||
FileOperationDispatchResponse,
|
||||
GameClientBridgeCancelRequest,
|
||||
GameClientBridgeCancelResponse,
|
||||
GameClientBridgeCommandFilterRequest,
|
||||
GameClientBridgeCommandListResponse,
|
||||
GameClientBridgeCommandResponse,
|
||||
GameClientBridgeQueueRequest,
|
||||
GameClientBridgeSnapshotListResponse,
|
||||
GameClientBridgeSnapshotQuery,
|
||||
GameClientBridgeStatusResponse,
|
||||
GamePluginListResponse,
|
||||
HealthResponse,
|
||||
JobCreateRequest,
|
||||
@@ -42,6 +64,11 @@ import type {
|
||||
MarketplacePluginResponse,
|
||||
MarketplacePluginStateRequest,
|
||||
PlatformResourceUsageResponse,
|
||||
ProductionCapacitySummaryResponse,
|
||||
CapacityAdmissionDecisionResponse,
|
||||
PluginLifecycleActionRequest,
|
||||
PluginLifecycleActionResponse,
|
||||
PluginLifecycleListResponse,
|
||||
PluginBridgeAuthorizeRequest,
|
||||
PluginBridgeAuthorizeResponse,
|
||||
PluginBridgeExecuteRequest,
|
||||
@@ -86,6 +113,15 @@ import type {
|
||||
} from "./types";
|
||||
import { readWebRuntimeEnv } from "../schemas/env";
|
||||
import { parseSafeDependencyCatalog, parseSafeRunUpdate, parseSafeRunUpdateList } from "../schemas/runtimeUpdates";
|
||||
import { parseSafeClientManagerLifecycle, parseSafeClientManagerLifecycleList } from "../schemas/clientManagerLifecycle";
|
||||
import {
|
||||
parseSafeGameClientBridgeCancellation,
|
||||
parseSafeGameClientBridgeCommand,
|
||||
parseSafeGameClientBridgeCommandList,
|
||||
parseSafeGameClientBridgeSnapshotList,
|
||||
parseSafeGameClientBridgeStatus
|
||||
} from "../schemas/gameClientBridge";
|
||||
import { safeDiagnosticText } from "../utils/safeDiagnosticText";
|
||||
|
||||
let platformApiSessionToken: string | null = null;
|
||||
let platformApiAuthFailureHandler: ((error: PlatformApiError) => void) | null = null;
|
||||
@@ -298,6 +334,74 @@ export class PlatformApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
async listClientManagerLifecycles(id: string): Promise<ClientManagerInstallationListResponse> {
|
||||
return parseSafeClientManagerLifecycleList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers`));
|
||||
}
|
||||
|
||||
async getClientManagerLifecycle(id: string, profileKey: string): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/${encodeURIComponent(profileKey)}`));
|
||||
}
|
||||
|
||||
async deployClientManager(id: string, request: ClientManagerDeployRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/deploy`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async controlClientManager(id: string, request: ClientManagerControlRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/control`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async updateClientManager(id: string, request: ClientManagerUpdateRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/update`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async retryClientManagerLifecycle(id: string, request: ClientManagerRetryRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/retry`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async revokeClientManagerSession(id: string, request: ClientManagerRevokeSessionRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/revoke-session`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async uninstallClientManager(id: string, request: ClientManagerUninstallRequest): Promise<ClientManagerInstallationResponse> {
|
||||
return parseSafeClientManagerLifecycle(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/client-managers/uninstall`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async getGameClientBridgeStatus(id: string): Promise<GameClientBridgeStatusResponse> {
|
||||
return parseSafeGameClientBridgeStatus(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge`));
|
||||
}
|
||||
|
||||
async listGameClientBridgeCommands(id: string, filter: GameClientBridgeCommandFilterRequest = {}): Promise<GameClientBridgeCommandListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.profileKey) params.set("profileKey", filter.profileKey);
|
||||
if (filter.state) params.set("state", filter.state);
|
||||
if (filter.commandType) params.set("commandType", filter.commandType);
|
||||
const query = params.toString();
|
||||
return parseSafeGameClientBridgeCommandList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands${query ? `?${query}` : ""}`));
|
||||
}
|
||||
|
||||
async queueGameClientBridgeCommand(id: string, request: GameClientBridgeQueueRequest): Promise<GameClientBridgeCommandResponse> {
|
||||
return parseSafeGameClientBridgeCommand(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async getGameClientBridgeCommand(id: string, commandId: string): Promise<GameClientBridgeCommandResponse> {
|
||||
return parseSafeGameClientBridgeCommand(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands/${encodeURIComponent(commandId)}`));
|
||||
}
|
||||
|
||||
async cancelGameClientBridgeCommand(id: string, commandId: string, request: GameClientBridgeCancelRequest = {}): Promise<GameClientBridgeCancelResponse> {
|
||||
return parseSafeGameClientBridgeCancellation(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/commands/${encodeURIComponent(commandId)}/cancel`, { method: "POST", body: request }));
|
||||
}
|
||||
|
||||
async listGameClientBridgeSnapshots(id: string, query: GameClientBridgeSnapshotQuery = {}): Promise<GameClientBridgeSnapshotListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (query.profileKey) params.set("profileKey", query.profileKey);
|
||||
if (query.type) params.set("type", query.type);
|
||||
if (query.streamKey) params.set("streamKey", query.streamKey);
|
||||
if (query.observedAfter) params.set("observedAfter", query.observedAfter);
|
||||
if (query.limit !== undefined) params.set("limit", String(query.limit));
|
||||
const search = params.toString();
|
||||
return parseSafeGameClientBridgeSnapshotList(await this.request<unknown>(`/server-instances/${encodeURIComponent(id)}/game-client-bridge/snapshots${search ? `?${search}` : ""}`));
|
||||
}
|
||||
|
||||
async checkDependencies(id: string, request: DependencyJobRequest): Promise<JobResponse> {
|
||||
return this.request<JobResponse>(`/server-instances/${encodeURIComponent(id)}/dependencies/check`, {
|
||||
method: "POST",
|
||||
@@ -386,6 +490,55 @@ export class PlatformApiClient {
|
||||
return this.request<PlatformResourceUsageResponse>("/metrics/platform");
|
||||
}
|
||||
|
||||
async getProductionCapacity(): Promise<ProductionCapacitySummaryResponse> {
|
||||
return this.request<ProductionCapacitySummaryResponse>("/production/capacity");
|
||||
}
|
||||
|
||||
async checkCapacityAdmission(request: { serverInstanceId?: string; runEndpointId?: string; capability: string; targetKey?: string; idempotencyKey?: string }): Promise<CapacityAdmissionDecisionResponse> {
|
||||
return this.request<CapacityAdmissionDecisionResponse>("/production/capacity/admission", { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async listAlerts(filter: { state?: string; sourceKind?: string; sourceId?: string; severity?: string } = {}): Promise<AlertListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filter).forEach(([key, value]) => { if (value) params.set(key, value); });
|
||||
const query = params.toString();
|
||||
return this.request<AlertListResponse>(`/alerts${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async acknowledgeAlert(id: string, note = ""): Promise<AlertResponse> {
|
||||
return this.request<AlertResponse>(`/alerts/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: { note } });
|
||||
}
|
||||
|
||||
async resolveAlert(id: string, note = ""): Promise<AlertResponse> {
|
||||
return this.request<AlertResponse>(`/alerts/${encodeURIComponent(id)}/resolve`, { method: "POST", body: { note } });
|
||||
}
|
||||
|
||||
async retryAlert(id: string, idempotencyKey: string): Promise<AlertRetryResponse> {
|
||||
return this.request<AlertRetryResponse>(`/alerts/${encodeURIComponent(id)}/retry`, { method: "POST", body: { idempotencyKey } });
|
||||
}
|
||||
|
||||
async listPluginLifecycles(filter: { pluginId?: string; serverInstanceId?: string; currentState?: string } = {}): Promise<PluginLifecycleListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filter).forEach(([key, value]) => { if (value) params.set(key, value); });
|
||||
const query = params.toString();
|
||||
return this.request<PluginLifecycleListResponse>(`/plugin-lifecycles${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async runPluginLifecycle(pluginId: string, request: PluginLifecycleActionRequest): Promise<PluginLifecycleActionResponse> {
|
||||
return this.request<PluginLifecycleActionResponse>(`/plugin-lifecycles/${encodeURIComponent(pluginId)}/actions`, { method: "POST", body: request });
|
||||
}
|
||||
|
||||
async listAIConfigDiffs(filter: { serverInstanceId?: string; pluginId?: string; state?: string } = {}): Promise<AIConfigDiffListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(filter).forEach(([key, value]) => { if (value) params.set(key, value); });
|
||||
const query = params.toString();
|
||||
return this.request<AIConfigDiffListResponse>(`/ai/config-diffs${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async approveAIConfigDiff(id: string, idempotencyKey: string): Promise<AIConfigDiffApprovalResponse> {
|
||||
return this.request<AIConfigDiffApprovalResponse>(`/ai/config-diffs/${encodeURIComponent(id)}/approve`, { method: "POST", body: { idempotencyKey } });
|
||||
}
|
||||
|
||||
async listServerMetrics(): Promise<ServerMetricsListResponse> {
|
||||
return this.request<ServerMetricsListResponse>("/metrics/server-instances");
|
||||
}
|
||||
@@ -556,7 +709,7 @@ async function responseError(response: Response): Promise<PlatformApiError> {
|
||||
const message = response.status === 401
|
||||
? "会话已失效,请重新登录。"
|
||||
: response.status === 403
|
||||
? "没有权限访问该资源。"
|
||||
? safeForbiddenMessage(apiError?.message)
|
||||
: apiError?.message ?? `request failed: ${response.status}`;
|
||||
const error = new PlatformApiError(response.status, apiError?.code ?? "request_failed", message);
|
||||
if (response.status === 401) {
|
||||
@@ -566,6 +719,21 @@ async function responseError(response: Response): Promise<PlatformApiError> {
|
||||
return error;
|
||||
}
|
||||
|
||||
function safeForbiddenMessage(apiMessage?: string): string {
|
||||
const sanitized = safeDiagnosticText(apiMessage, "")?.trim();
|
||||
if (!sanitized || sanitized === "account is not allowed to access this resource") {
|
||||
return "没有权限访问该资源。";
|
||||
}
|
||||
const missingPermission = sanitized.match(/^plugin does not declare required permission:\s*([a-z0-9._-]+)$/i);
|
||||
if (missingPermission) {
|
||||
return `插件未声明所需权限:${missingPermission[1]}`;
|
||||
}
|
||||
if (sanitized === "plugin is not installed") {
|
||||
return "插件未安装,不能执行该操作。";
|
||||
}
|
||||
return "没有权限访问该资源。";
|
||||
}
|
||||
|
||||
function marketplaceQuery(filter: MarketplacePluginFilterRequest): string {
|
||||
const params = new URLSearchParams();
|
||||
if (filter.status && filter.status !== "all") {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PlatformApiClient } from "./client";
|
||||
|
||||
const lifecycle = {
|
||||
id: "installation-1", serverInstanceId: "server-1", pluginId: "game.scum", profileKey: "scum-client-manager", targetOs: "windows", targetArch: "amd64",
|
||||
status: "available", phase: "artifact available", desiredVersion: "1.0.0", desiredRevision: "rev-1", desiredArtifactId: "artifact-1", keyGeneration: 1,
|
||||
deploymentGeneration: 0, health: "unknown", healthReason: "component is not installed", retryable: false, requiresRedeploy: false, updatedAt: "2026-07-18T08:00:00Z",
|
||||
distribution: { id: "distribution-1", artifactId: "artifact-1", sourceRevision: "rev-1", targetOs: "windows", targetArch: "amd64", checksum: `sha256:${"a".repeat(64)}`, keyGeneration: 1, status: "available" },
|
||||
actions: [{ operation: "deploy", available: true }, { operation: "uninstall", available: false, reason: "not installed" }]
|
||||
};
|
||||
|
||||
describe("PlatformApiClient Client Manager lifecycle", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("uses typed Platform lifecycle routes and preserves action bodies", async () => {
|
||||
const calls: Array<{ url: string; method: string; body?: unknown }> = [];
|
||||
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
calls.push({ url, method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : undefined });
|
||||
return new Response(JSON.stringify(url.endsWith("/client-managers") && (init?.method ?? "GET") === "GET" ? { items: [lifecycle], count: 1 } : lifecycle), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}));
|
||||
const client = new PlatformApiClient("/api/v1", () => "session-token");
|
||||
|
||||
await expect(client.listClientManagerLifecycles("server-1")).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.getClientManagerLifecycle("server-1", "scum-client-manager")).resolves.toMatchObject({ profileKey: "scum-client-manager" });
|
||||
await client.deployClientManager("server-1", { profileKey: "scum-client-manager", distributionId: "distribution-1", expectedDeploymentGeneration: 0, idempotencyKey: "deploy-1" });
|
||||
await client.controlClientManager("server-1", { profileKey: "scum-client-manager", operation: "start", expectedDeploymentGeneration: 1, idempotencyKey: "start-1" });
|
||||
await client.updateClientManager("server-1", { profileKey: "scum-client-manager", distributionId: "distribution-2", expectedDeploymentGeneration: 1, approved: true, idempotencyKey: "update-1" });
|
||||
await client.retryClientManagerLifecycle("server-1", { profileKey: "scum-client-manager", expectedDeploymentGeneration: 2, idempotencyKey: "retry-1" });
|
||||
await client.revokeClientManagerSession("server-1", { profileKey: "scum-client-manager", reason: "operator revoked component session" });
|
||||
await client.uninstallClientManager("server-1", { profileKey: "scum-client-manager", expectedDeploymentGeneration: 2, confirmed: true, idempotencyKey: "uninstall-1" });
|
||||
|
||||
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
||||
"GET /api/v1/server-instances/server-1/client-managers",
|
||||
"GET /api/v1/server-instances/server-1/client-managers/scum-client-manager",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/deploy",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/control",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/update",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/retry",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/revoke-session",
|
||||
"POST /api/v1/server-instances/server-1/client-managers/uninstall"
|
||||
]);
|
||||
expect(calls[4]?.body).toMatchObject({ approved: true, distributionId: "distribution-2" });
|
||||
expect(calls[7]?.body).toMatchObject({ confirmed: true });
|
||||
});
|
||||
});
|
||||
@@ -61,3 +61,6 @@ Existing platform APIs already cover server lifecycle, jobs, log stream metadata
|
||||
|
||||
Browser Job contracts explicitly exclude raw or hashed lease tokens, Run session tokens/generations, secret refs, host paths, sockets, and credentials. The safe schema rejects those keys, and existing API client 401/403 behavior remains authoritative for expired sessions and cross-owner access.
|
||||
- Log filtering by level/keyword/time/source is applied client-side over `POST /api/v1/log-streams/query` (`LogStreamCursorRequest`) results until the platform exposes server-side filters.
|
||||
# Client Manager API projection
|
||||
|
||||
`PlatformApiClient` exposes list/detail and typed deploy, control, update, retry, revoke-session, and uninstall methods. `schemas/clientManagerLifecycle.ts` validates status/action/job/health fields and rejects forbidden machine or credential fields before rendering. Lifecycle commands carry profile, distribution, expected deployment generation, approval/confirmation, and idempotency only. Artifact bytes remain in the platform-owned artifact transfer client.
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PlatformApiClient } from "./client";
|
||||
import type { GameClientBridgeManifestResponse, GameClientBridgeQueueRequest, GamePluginResponse, MarketplacePluginResponse } from "./types";
|
||||
|
||||
const now = "2026-07-20T08:00:00Z";
|
||||
const later = "2026-07-20T09:00:00Z";
|
||||
|
||||
const status = {
|
||||
serverInstanceId: "server-1",
|
||||
pluginId: "game.scum",
|
||||
available: false,
|
||||
reason: "compatible companion is offline",
|
||||
profiles: [{
|
||||
pluginId: "game.scum",
|
||||
profileKey: "scum-client",
|
||||
available: false,
|
||||
reason: "component heartbeat is unavailable",
|
||||
commandTypes: ["scum.announcement.send"],
|
||||
snapshotTypes: ["scum.players"],
|
||||
queryTemplateKeys: ["scum.player.search"]
|
||||
}]
|
||||
} as const;
|
||||
|
||||
const pendingCommand = {
|
||||
id: "command-1",
|
||||
serverInstanceId: "server-1",
|
||||
pluginId: "game.scum",
|
||||
profileKey: "scum-client",
|
||||
commandType: "scum.announcement.send",
|
||||
priority: 20,
|
||||
state: "pending",
|
||||
approvalState: "pending",
|
||||
requesterId: "user-1",
|
||||
auditReferences: ["audit-command-1"],
|
||||
expiresAt: later,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
} as const;
|
||||
|
||||
const completedCommand = {
|
||||
...pendingCommand,
|
||||
state: "succeeded",
|
||||
approvalState: "approved",
|
||||
resultSummary: "announcement delivered",
|
||||
result: {
|
||||
status: "succeeded",
|
||||
summary: "announcement delivered",
|
||||
payload: { delivered: true, recipientCount: 12 },
|
||||
completedAt: later
|
||||
},
|
||||
completedAt: later,
|
||||
updatedAt: later
|
||||
} as const;
|
||||
|
||||
const cancellation = {
|
||||
commandId: pendingCommand.id,
|
||||
state: "cancelled",
|
||||
cancellation: { requestedBy: "user-1", reason: "maintenance window changed", cancelledAt: later },
|
||||
auditReferences: ["audit-command-1", "audit-command-cancel-1"],
|
||||
updatedAt: later
|
||||
} as const;
|
||||
|
||||
const snapshot = {
|
||||
id: "snapshot-1",
|
||||
serverInstanceId: "server-1",
|
||||
pluginId: "game.scum",
|
||||
profileKey: "scum-client",
|
||||
type: "scum.players",
|
||||
schemaVersion: "1",
|
||||
streamKey: "current",
|
||||
sequence: 7,
|
||||
observedAt: now,
|
||||
payload: { players: [{ playerId: "player-1", displayName: "Moonlight" }] },
|
||||
retention: { keepForSeconds: 3600, maxRecords: 24 },
|
||||
auditReferences: ["audit-snapshot-1"],
|
||||
createdAt: now,
|
||||
expiresAt: later
|
||||
} as const;
|
||||
|
||||
const manifestDeclaration: GameClientBridgeManifestResponse = {
|
||||
commands: [{
|
||||
type: "scum.announcement.send",
|
||||
title: "Send announcement",
|
||||
permission: "server.game-client.command",
|
||||
approvalLevel: "operator",
|
||||
payloadSchemaRef: "schemas/bridge/commands/announcement.request.json",
|
||||
resultSchemaRef: "schemas/bridge/commands/announcement.result.json",
|
||||
timeoutSeconds: 30,
|
||||
maxPayloadBytes: 4096
|
||||
}],
|
||||
snapshots: [{ type: "scum.players", schemaVersion: "1", schemaRef: "schemas/bridge/snapshots/players.json", keepForSeconds: 3600, maxRecords: 24 }],
|
||||
queryTemplates: [{
|
||||
key: "scum.player.search",
|
||||
title: "Search players",
|
||||
permission: "server.game-client.read",
|
||||
engine: "sqlite",
|
||||
transportKey: "scum-database",
|
||||
targetKey: "scum-db",
|
||||
parameterSchemaRef: "schemas/bridge/queries/player-search.request.json",
|
||||
resultSchemaRef: "schemas/bridge/queries/player-search.result.json",
|
||||
maxRows: 50,
|
||||
timeoutSeconds: 10
|
||||
}],
|
||||
commandRetentionSeconds: 86400,
|
||||
maxCommands: 1000,
|
||||
pages: [{ pageKey: "operations", commandTypes: ["scum.announcement.send"], snapshotTypes: ["scum.players"], queryTemplateKeys: ["scum.player.search"] }],
|
||||
companion: {
|
||||
profileKey: "scum-client-manager",
|
||||
configTemplateKey: "client-config",
|
||||
configSchemaRef: "schemas/companion/config.schema.json",
|
||||
configFormat: "yaml",
|
||||
platformBaseUrlSource: "run-control",
|
||||
registrationProof: "hmac-sha256",
|
||||
proofMaterialSource: "component-package",
|
||||
proofMaterialEnv: "SCUM_COMPONENT_PROOF",
|
||||
sessionMode: "component-session",
|
||||
tlsPolicy: "verify-system-roots",
|
||||
heartbeatIntervalSeconds: 30,
|
||||
commandPollIntervalSeconds: 5,
|
||||
requestTimeoutSeconds: 15
|
||||
}
|
||||
};
|
||||
|
||||
const pluginBridgeProjection: Pick<GamePluginResponse, "gameClientBridge"> & Pick<MarketplacePluginResponse, "gameClientBridge"> = {
|
||||
gameClientBridge: manifestDeclaration
|
||||
};
|
||||
|
||||
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" } });
|
||||
expect(JSON.stringify(pluginBridgeProjection)).not.toMatch(/authKey|componentKey|sessionToken|credential|secretRef/i);
|
||||
});
|
||||
|
||||
it("uses only server-scoped operator routes and preserves bounded filters and bodies", async () => {
|
||||
const calls: Array<{ url: string; method: string; body?: unknown }> = [];
|
||||
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = init?.method ?? "GET";
|
||||
calls.push({ url, method, body: init?.body ? JSON.parse(String(init.body)) : undefined });
|
||||
if (url.endsWith("/game-client-bridge")) return jsonResponse(status);
|
||||
if (url.includes("/game-client-bridge/commands?") && method === "GET") return jsonResponse({ items: [pendingCommand], count: 1 });
|
||||
if (url.endsWith("/game-client-bridge/commands") && method === "POST") return jsonResponse(pendingCommand, 202);
|
||||
if (url.endsWith(`/game-client-bridge/commands/${pendingCommand.id}`) && method === "GET") return jsonResponse(completedCommand);
|
||||
if (url.endsWith(`/game-client-bridge/commands/${pendingCommand.id}/cancel`) && method === "POST") return jsonResponse(cancellation);
|
||||
if (url.includes("/game-client-bridge/snapshots?")) return jsonResponse({ items: [snapshot], count: 1 });
|
||||
throw new Error(`unexpected request: ${method} ${url}`);
|
||||
}));
|
||||
const client = new PlatformApiClient("/api/v1", () => "operator-session");
|
||||
const queueRequest: GameClientBridgeQueueRequest = {
|
||||
profileKey: "scum-client",
|
||||
commandType: "scum.announcement.send",
|
||||
payload: { message: "Restart in ten minutes", channels: ["global"] },
|
||||
idempotencyKey: "announcement-1",
|
||||
priority: 20,
|
||||
expiresAt: later
|
||||
};
|
||||
|
||||
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.announcement.send" })).resolves.toMatchObject({ count: 1 });
|
||||
await expect(client.queueGameClientBridgeCommand("server-1", queueRequest)).resolves.toMatchObject({ state: "pending", approvalState: "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 }] });
|
||||
|
||||
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
||||
"GET /api/v1/server-instances/server-1/game-client-bridge",
|
||||
"GET /api/v1/server-instances/server-1/game-client-bridge/commands?profileKey=scum-client&state=pending&commandType=scum.announcement.send",
|
||||
"POST /api/v1/server-instances/server-1/game-client-bridge/commands",
|
||||
"GET /api/v1/server-instances/server-1/game-client-bridge/commands/command-1",
|
||||
"POST /api/v1/server-instances/server-1/game-client-bridge/commands/command-1/cancel",
|
||||
"GET /api/v1/server-instances/server-1/game-client-bridge/snapshots?profileKey=scum-client&type=scum.players&streamKey=current&observedAfter=2026-07-20T08%3A00%3A00Z&limit=20"
|
||||
]);
|
||||
expect(calls[2]?.body).toEqual(queueRequest);
|
||||
expect(calls[4]?.body).toEqual({ reason: "maintenance window changed" });
|
||||
expect(JSON.stringify(calls)).not.toMatch(/sessionToken|componentKey|secretRef|hostPath|dsn|socket|credential|runEndpoint/i);
|
||||
expect(calls.every((call) => !call.url.includes("/companion/"))).toBe(true);
|
||||
});
|
||||
|
||||
it("URL-encodes server and command identifiers", async () => {
|
||||
const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) => jsonResponse(completedCommand));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await new PlatformApiClient("/api/v1").getGameClientBridgeCommand("server/unsafe", "command/unsafe");
|
||||
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toBe("/api/v1/server-instances/server%2Funsafe/game-client-bridge/commands/command%2Funsafe");
|
||||
});
|
||||
});
|
||||
|
||||
function jsonResponse(value: unknown, statusCode = 200): Response {
|
||||
return new Response(JSON.stringify(value), { status: statusCode, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PlatformApiClient } from "./client";
|
||||
|
||||
describe("PlatformApiClient production operations", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("uses Platform-only governance routes and bounded request bodies", async () => {
|
||||
const calls: Array<{ url: string; method: string; body?: unknown }> = [];
|
||||
vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
calls.push({ url: String(input), method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : undefined });
|
||||
return new Response(JSON.stringify({ items: [], count: 0, endpoints: [], totalMaxJobs: 0, totalRunningJobs: 0, totalQueuedJobs: 0, activeAlerts: 0, generatedAt: "2026-07-18T00:00:00Z", status: "queued", installation: {}, job: {}, decision: {}, alert: {} }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}));
|
||||
const client = new PlatformApiClient("/api/v1", () => "session-token");
|
||||
|
||||
await client.getProductionCapacity();
|
||||
await client.listAlerts({ state: "active" });
|
||||
await client.acknowledgeAlert("alert-1", "reviewed");
|
||||
await client.resolveAlert("alert-1", "resolved");
|
||||
await client.retryAlert("alert-1", "retry-1");
|
||||
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.listAIConfigDiffs({ state: "pending" });
|
||||
await client.approveAIConfigDiff("diff-1", "approve-1");
|
||||
|
||||
expect(calls.map((call) => `${call.method} ${call.url}`)).toEqual([
|
||||
"GET /api/v1/production/capacity",
|
||||
"GET /api/v1/alerts?state=active",
|
||||
"POST /api/v1/alerts/alert-1/acknowledge",
|
||||
"POST /api/v1/alerts/alert-1/resolve",
|
||||
"POST /api/v1/alerts/alert-1/retry",
|
||||
"GET /api/v1/plugin-lifecycles?pluginId=game.scum",
|
||||
"POST /api/v1/plugin-lifecycles/game.scum/actions",
|
||||
"GET /api/v1/ai/config-diffs?state=pending",
|
||||
"POST /api/v1/ai/config-diffs/diff-1/approve"
|
||||
]);
|
||||
const serialized = JSON.stringify(calls);
|
||||
expect(serialized).not.toMatch(/apiKey|token|secret|providerBaseUrl|runSocket|runEndpointUrl|hostPath|credential|dsn|rcon/i);
|
||||
expect(calls[6]?.body).toEqual({ serverInstanceId: "server-1", operation: "upgrade", targetVersion: "1.2.0", idempotencyKey: "upgrade-1", confirmed: false });
|
||||
});
|
||||
});
|
||||
+472
-1
@@ -13,6 +13,199 @@ export type DependencyState = "unknown" | "present" | "missing" | "installing" |
|
||||
export type RunUpdatePhase = "queued" | "downloading" | "staged" | "restart-requested" | "activating" | "succeeded" | "rolled-back" | "failed";
|
||||
export type ServerLifecycleAction = "create" | "start" | "stop" | "status";
|
||||
|
||||
export type GameClientBridgeCommandState = "pending" | "claimed" | "succeeded" | "failed" | "cancelled" | "expired";
|
||||
export type GameClientBridgeApprovalState = "not_required" | "pending" | "approved" | "rejected";
|
||||
export type GameClientBridgeApprovalLevel = "none" | "operator" | "platform-admin";
|
||||
export type GameClientBridgeResultStatus = "succeeded" | "failed" | "cancelled";
|
||||
export type GameClientBridgeJsonValue = string | number | boolean | null | GameClientBridgeJsonValue[] | GameClientBridgeJsonObject;
|
||||
|
||||
export interface GameClientBridgeJsonObject {
|
||||
[key: string]: GameClientBridgeJsonValue;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandDeclarationResponse {
|
||||
type: string;
|
||||
title: string;
|
||||
permission: string;
|
||||
approvalLevel: GameClientBridgeApprovalLevel;
|
||||
payloadSchemaRef: string;
|
||||
resultSchemaRef?: string;
|
||||
timeoutSeconds: number;
|
||||
maxPayloadBytes: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotDeclarationResponse {
|
||||
type: string;
|
||||
schemaVersion: string;
|
||||
schemaRef: string;
|
||||
keepForSeconds: number;
|
||||
maxRecords: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeQueryTemplateDeclarationResponse {
|
||||
key: string;
|
||||
title: string;
|
||||
permission: string;
|
||||
engine: "sqlite";
|
||||
transportKey: string;
|
||||
targetKey: string;
|
||||
parameterSchemaRef: string;
|
||||
resultSchemaRef: string;
|
||||
maxRows: number;
|
||||
timeoutSeconds: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgePageContractResponse {
|
||||
pageKey: string;
|
||||
commandTypes?: string[];
|
||||
snapshotTypes?: string[];
|
||||
queryTemplateKeys?: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCompanionDeclarationResponse {
|
||||
profileKey: string;
|
||||
configTemplateKey: string;
|
||||
configSchemaRef: string;
|
||||
configFormat: "yaml";
|
||||
platformBaseUrlSource: "run-control";
|
||||
registrationProof: "hmac-sha256";
|
||||
proofMaterialSource: "component-package";
|
||||
proofMaterialEnv: string;
|
||||
sessionMode: "component-session";
|
||||
tlsPolicy: "verify-system-roots";
|
||||
heartbeatIntervalSeconds: number;
|
||||
commandPollIntervalSeconds: number;
|
||||
requestTimeoutSeconds: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeManifestResponse {
|
||||
commands: GameClientBridgeCommandDeclarationResponse[];
|
||||
snapshots: GameClientBridgeSnapshotDeclarationResponse[];
|
||||
queryTemplates?: GameClientBridgeQueryTemplateDeclarationResponse[];
|
||||
commandRetentionSeconds: number;
|
||||
maxCommands: number;
|
||||
pages?: GameClientBridgePageContractResponse[];
|
||||
companion?: GameClientBridgeCompanionDeclarationResponse;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeProfileDeclarationResponse {
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
commandTypes: string[];
|
||||
snapshotTypes: string[];
|
||||
queryTemplateKeys: string[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeStatusResponse {
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
profiles: GameClientBridgeProfileDeclarationResponse[];
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandResultResponse {
|
||||
status: GameClientBridgeResultStatus;
|
||||
summary?: string;
|
||||
payload?: GameClientBridgeJsonObject;
|
||||
completedAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandCancellationResponse {
|
||||
requestedBy?: string;
|
||||
reason?: string;
|
||||
cancelledAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandResponse {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
commandType: string;
|
||||
priority: number;
|
||||
state: GameClientBridgeCommandState;
|
||||
approvalState: GameClientBridgeApprovalState;
|
||||
requesterId?: string;
|
||||
resultSummary?: string;
|
||||
result?: GameClientBridgeCommandResultResponse;
|
||||
cancellation?: GameClientBridgeCommandCancellationResponse;
|
||||
auditReferences?: string[];
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandListResponse {
|
||||
items: GameClientBridgeCommandResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCommandFilterRequest {
|
||||
profileKey?: string;
|
||||
state?: GameClientBridgeCommandState;
|
||||
commandType?: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeQueueRequest {
|
||||
profileKey: string;
|
||||
commandType: string;
|
||||
payload: GameClientBridgeJsonObject;
|
||||
idempotencyKey: string;
|
||||
priority?: number;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCancelRequest {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeCancelResponse {
|
||||
commandId: string;
|
||||
state: GameClientBridgeCommandState;
|
||||
cancellation: GameClientBridgeCommandCancellationResponse;
|
||||
auditReferences?: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeRetentionResponse {
|
||||
keepForSeconds: number;
|
||||
maxRecords?: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotResponse {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
type: string;
|
||||
schemaVersion: string;
|
||||
streamKey: string;
|
||||
sequence: number;
|
||||
observedAt: string;
|
||||
payload: GameClientBridgeJsonObject;
|
||||
retention: GameClientBridgeRetentionResponse;
|
||||
auditReferences?: string[];
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotListResponse {
|
||||
items: GameClientBridgeSnapshotResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface GameClientBridgeSnapshotQuery {
|
||||
profileKey?: string;
|
||||
type?: string;
|
||||
streamKey?: string;
|
||||
observedAfter?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface PluginPermissionsResponse {
|
||||
ai: boolean;
|
||||
logs: boolean;
|
||||
@@ -74,6 +267,17 @@ export interface RuntimeLogSourceResponse {
|
||||
retentionDays?: number;
|
||||
}
|
||||
|
||||
export interface RuntimeLogEventResponse {
|
||||
key: string;
|
||||
title: string;
|
||||
sourceKey: string;
|
||||
eventType: string;
|
||||
permission: string;
|
||||
schemaRef: string;
|
||||
retentionDays: number;
|
||||
severity: "info" | "notice" | "warning" | "critical";
|
||||
}
|
||||
|
||||
export interface RuntimeTransportProfileResponse {
|
||||
key: string;
|
||||
kind: string;
|
||||
@@ -84,11 +288,18 @@ export interface RuntimeTransportProfileResponse {
|
||||
export interface RuntimeClientManagerProfileResponse {
|
||||
key: string;
|
||||
displayName?: string;
|
||||
version?: string;
|
||||
revision?: string;
|
||||
repository: { url: string; revisionPolicy: string; branch?: string; tag?: string; revision?: string };
|
||||
supportedTargets: Array<{ os: string; arch: string }>;
|
||||
build: { system: string; workspaceRef?: string; entryRef?: string };
|
||||
configTemplates?: Array<{ key: string; templateRef: string; outputRef: string }>;
|
||||
outputArtifacts: string[];
|
||||
deployment?: { mode: string; executableRef: string; arguments: string[]; autoStart: boolean; requiredRunCapabilities: string[] };
|
||||
lifecycle?: { actions: string[]; startupTimeoutSeconds: number; stopTimeoutSeconds: number };
|
||||
health?: { mode: string; intervalSeconds: number; degradedAfterSeconds: number; offlineAfterSeconds: number; requiredCapabilities: string[] };
|
||||
compatibility?: { minimumVersion?: string; maximumVersion?: string; allowDowngrade: boolean };
|
||||
updatePolicy?: { strategy: string; requireApproval: boolean; healthConfirmationSeconds: number; retainPrevious: boolean };
|
||||
}
|
||||
|
||||
export interface GamePluginRuntimeProfilesResponse {
|
||||
@@ -97,6 +308,7 @@ export interface GamePluginRuntimeProfilesResponse {
|
||||
dependencyProbes?: RuntimeDependencyProbeResponse[];
|
||||
installPlans?: RuntimeInstallPlanResponse[];
|
||||
logSources?: RuntimeLogSourceResponse[];
|
||||
logEvents?: RuntimeLogEventResponse[];
|
||||
transportProfiles?: RuntimeTransportProfileResponse[];
|
||||
clientManagers?: RuntimeClientManagerProfileResponse[];
|
||||
}
|
||||
@@ -119,8 +331,10 @@ export interface GamePluginResponse {
|
||||
pages: GamePluginPageResponse[];
|
||||
tags: string[];
|
||||
aiPurposes: string[];
|
||||
productionLifecycle: PluginProductionLifecycleDeclaration;
|
||||
validationViolations?: string[];
|
||||
runtimeProfiles?: GamePluginRuntimeProfilesResponse;
|
||||
gameClientBridge?: GameClientBridgeManifestResponse;
|
||||
status: GamePluginStatus;
|
||||
}
|
||||
|
||||
@@ -149,7 +363,9 @@ export interface MarketplacePluginResponse {
|
||||
pages: GamePluginPageResponse[];
|
||||
tags: string[];
|
||||
aiPurposes: string[];
|
||||
productionLifecycle: PluginProductionLifecycleDeclaration;
|
||||
validationViolations?: string[];
|
||||
gameClientBridge?: GameClientBridgeManifestResponse;
|
||||
status: GamePluginStatus;
|
||||
source: string;
|
||||
}
|
||||
@@ -476,6 +692,131 @@ export interface ClientManagerDownloadRequest {
|
||||
profileKey?: string;
|
||||
}
|
||||
|
||||
export type ClientManagerLifecycleStatus =
|
||||
| "requested"
|
||||
| "building"
|
||||
| "available"
|
||||
| "deploying"
|
||||
| "installed"
|
||||
| "registering"
|
||||
| "online"
|
||||
| "degraded"
|
||||
| "offline"
|
||||
| "updating"
|
||||
| "rolling_back"
|
||||
| "stopping"
|
||||
| "uninstalled"
|
||||
| "failed";
|
||||
|
||||
export type ClientManagerLifecycleOperation = "deploy" | "start" | "stop" | "restart" | "status" | "update" | "rollback" | "uninstall";
|
||||
|
||||
export interface ClientManagerLifecycleActionResponse {
|
||||
operation: ClientManagerLifecycleOperation;
|
||||
available: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerLifecycleJobResponse {
|
||||
id: string;
|
||||
state: JobState;
|
||||
progress: JobProgressBody;
|
||||
attempt: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerDistributionSummaryResponse {
|
||||
id: string;
|
||||
artifactId: string;
|
||||
sourceRevision: string;
|
||||
targetOs: string;
|
||||
targetArch: string;
|
||||
checksum: string;
|
||||
keyGeneration: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerInstallationResponse {
|
||||
id: string;
|
||||
serverInstanceId: string;
|
||||
pluginId: string;
|
||||
profileKey: string;
|
||||
targetOs: string;
|
||||
targetArch: string;
|
||||
status: ClientManagerLifecycleStatus;
|
||||
phase: string;
|
||||
desiredVersion?: string;
|
||||
activeVersion?: string;
|
||||
previousVersion?: string;
|
||||
desiredRevision?: string;
|
||||
activeRevision?: string;
|
||||
previousRevision?: string;
|
||||
desiredArtifactId?: string;
|
||||
activeArtifactId?: string;
|
||||
previousArtifactId?: string;
|
||||
keyGeneration: number;
|
||||
deploymentGeneration: number;
|
||||
currentJobId?: string;
|
||||
lastSuccessfulJobId?: string;
|
||||
lastOperation?: ClientManagerLifecycleOperation;
|
||||
health: "unknown" | "healthy" | "degraded" | "unhealthy" | "offline";
|
||||
healthReason?: string;
|
||||
lastSeenAt?: string;
|
||||
retryable: boolean;
|
||||
requiresRedeploy: boolean;
|
||||
installedAt?: string;
|
||||
uninstalledAt?: string;
|
||||
updatedAt: string;
|
||||
distribution?: ClientManagerDistributionSummaryResponse;
|
||||
job?: ClientManagerLifecycleJobResponse;
|
||||
actions: ClientManagerLifecycleActionResponse[];
|
||||
}
|
||||
|
||||
export interface ClientManagerInstallationListResponse {
|
||||
items: ClientManagerInstallationResponse[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ClientManagerDeployRequest {
|
||||
profileKey: string;
|
||||
distributionId: string;
|
||||
expectedDeploymentGeneration?: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerControlRequest {
|
||||
profileKey: string;
|
||||
operation: "start" | "stop" | "restart" | "status" | "rollback";
|
||||
expectedDeploymentGeneration: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerUpdateRequest {
|
||||
profileKey: string;
|
||||
distributionId: string;
|
||||
expectedDeploymentGeneration: number;
|
||||
approved: boolean;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerRetryRequest {
|
||||
profileKey: string;
|
||||
expectedDeploymentGeneration: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerRevokeSessionRequest {
|
||||
profileKey: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface ClientManagerUninstallRequest {
|
||||
profileKey: string;
|
||||
expectedDeploymentGeneration: number;
|
||||
confirmed: boolean;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface ComponentKeyResetRequest {
|
||||
componentKind: "run" | "client-manager" | string;
|
||||
componentKey?: string;
|
||||
@@ -625,7 +966,7 @@ export interface AiProviderResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: AiProviderKind;
|
||||
baseUrl: string;
|
||||
baseUrlConfigured: boolean;
|
||||
apiKeyConfigured: boolean;
|
||||
models: string[];
|
||||
defaultModel?: string;
|
||||
@@ -1050,8 +1391,138 @@ export interface AIConfigRecommendationResponse {
|
||||
key: string;
|
||||
suggestedConfig?: string;
|
||||
diffSummary: string;
|
||||
diffId: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface PluginProductionLifecycleDeclaration {
|
||||
operations: PluginLifecycleOperation[];
|
||||
dependencyPolicy: "required" | "optional";
|
||||
approvalRequired: Array<"disable" | "rollback" | "retire">;
|
||||
}
|
||||
|
||||
export type CapacityAdmissionState = "accepted" | "deferred" | "denied";
|
||||
|
||||
export interface CapacityAdmissionDecisionResponse {
|
||||
accepted: boolean;
|
||||
state: CapacityAdmissionState;
|
||||
reason: string;
|
||||
retryAfterSeconds?: number;
|
||||
serverInstanceId?: string;
|
||||
runEndpointId?: string;
|
||||
capability: string;
|
||||
targetKey?: string;
|
||||
maxJobs: number;
|
||||
runningJobs: number;
|
||||
queuedJobs: number;
|
||||
pressureCodes?: string[];
|
||||
checkedAt: string;
|
||||
alertId?: string;
|
||||
auditEventId?: string;
|
||||
}
|
||||
|
||||
export interface EndpointCapacityProjectionResponse {
|
||||
runEndpointId: string;
|
||||
displayName: string;
|
||||
status: RunEndpointStatus;
|
||||
capabilities: string[];
|
||||
maxJobs: number;
|
||||
runningJobs: number;
|
||||
queuedJobs: number;
|
||||
logBacklogBatches?: number;
|
||||
artifactBacklogChunks?: number;
|
||||
pressureCodes?: string[];
|
||||
summary?: string;
|
||||
lastHeartbeatAt: string;
|
||||
lastAdmissionDecision?: CapacityAdmissionState;
|
||||
lastAdmissionReason?: string;
|
||||
lastAdmissionCheckedAt?: string;
|
||||
}
|
||||
|
||||
export interface ProductionCapacitySummaryResponse {
|
||||
endpoints: EndpointCapacityProjectionResponse[];
|
||||
totalMaxJobs: number;
|
||||
totalRunningJobs: number;
|
||||
totalQueuedJobs: number;
|
||||
activeAlerts: number;
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
export type AlertState = "active" | "acknowledged" | "resolved";
|
||||
export interface AlertResponse {
|
||||
id: string;
|
||||
sourceKind: string;
|
||||
sourceId: string;
|
||||
ruleKey: string;
|
||||
severity: "info" | "warning" | "critical";
|
||||
state: AlertState;
|
||||
title: string;
|
||||
message: string;
|
||||
occurrenceCount: number;
|
||||
retryable: boolean;
|
||||
retryAfterSeconds?: number;
|
||||
lastJobId?: string;
|
||||
lastAuditEventId?: string;
|
||||
lastSeenAt: string;
|
||||
acknowledgedBy?: string;
|
||||
acknowledgedAt?: string;
|
||||
resolvedBy?: string;
|
||||
resolvedAt?: string;
|
||||
resolutionNote?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface AlertListResponse { items: AlertResponse[]; count: number; }
|
||||
export interface AlertRetryResponse { status: string; alert: AlertResponse; decision: CapacityAdmissionDecisionResponse; }
|
||||
|
||||
export type PluginLifecycleOperation = "install" | "enable" | "disable" | "upgrade" | "rollback" | "retire" | "dependency-check";
|
||||
export interface PluginLifecycleInstallationResponse {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
serverInstanceId: string;
|
||||
currentVersion?: string;
|
||||
targetVersion?: string;
|
||||
previousVersion?: string;
|
||||
desiredState: string;
|
||||
currentState: string;
|
||||
lastOperation?: PluginLifecycleOperation;
|
||||
compatibility?: string;
|
||||
dependencyState?: string;
|
||||
jobId?: string;
|
||||
alertId?: string;
|
||||
auditEventId?: string;
|
||||
failureReason?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface PluginLifecycleListResponse { items: PluginLifecycleInstallationResponse[]; count: number; }
|
||||
export interface PluginLifecycleActionRequest { serverInstanceId: string; operation: PluginLifecycleOperation; targetVersion?: string; idempotencyKey: string; confirmed: boolean; }
|
||||
export interface PluginLifecycleActionResponse { status: string; installation: PluginLifecycleInstallationResponse; job: JobResponse; decision: CapacityAdmissionDecisionResponse; alert?: AlertResponse; }
|
||||
|
||||
export interface AIConfigDiffPreviewResponse {
|
||||
id: string;
|
||||
requestId: string;
|
||||
createdBy: string;
|
||||
serverInstanceId: string;
|
||||
pluginId?: string;
|
||||
providerId?: string;
|
||||
model?: string;
|
||||
key: string;
|
||||
configVersion: number;
|
||||
currentConfigChecksum?: string;
|
||||
proposedConfig?: string;
|
||||
diffSummary: string;
|
||||
state: "pending" | "approved" | "cancelled" | "expired";
|
||||
expiresAt: string;
|
||||
approvedBy?: string;
|
||||
approvedAt?: string;
|
||||
jobId?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface AIConfigDiffListResponse { items: AIConfigDiffPreviewResponse[]; count: number; }
|
||||
export interface AIConfigDiffApprovalResponse { preview: AIConfigDiffPreviewResponse; dispatch: ServerConfigWriteDispatchResponse; }
|
||||
|
||||
export interface AIInvocationSafeErrorResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
|
||||
Reference in New Issue
Block a user